@fortemi/core 2026.7.15 → 2026.8.0

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
@@ -926,6 +926,85 @@ var migration0022 = {
926
926
  `
927
927
  };
928
928
 
929
+ // src/migrations/0023_source_metadata_purge.ts
930
+ var migration0023 = {
931
+ version: 23,
932
+ name: "0023_source_metadata_purge",
933
+ sql: `
934
+ CREATE TABLE IF NOT EXISTS source_identity (
935
+ id TEXT PRIMARY KEY,
936
+ tenant_id TEXT NOT NULL DEFAULT 'default',
937
+ archive_id TEXT,
938
+ namespace TEXT NOT NULL,
939
+ external_id TEXT NOT NULL,
940
+ external_id_hash TEXT NOT NULL,
941
+ source_schema_version TEXT NOT NULL,
942
+ content_digest TEXT NOT NULL,
943
+ import_run_id TEXT NOT NULL,
944
+ caller_stable_id TEXT,
945
+ note_id TEXT NOT NULL REFERENCES note(id) ON DELETE CASCADE,
946
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
947
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
948
+ UNIQUE (tenant_id, archive_id, namespace, external_id)
949
+ );
950
+
951
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_source_identity_scope_key
952
+ ON source_identity(tenant_id, COALESCE(archive_id, ''), namespace, external_id);
953
+ CREATE INDEX IF NOT EXISTS idx_source_identity_note ON source_identity(note_id);
954
+ CREATE INDEX IF NOT EXISTS idx_source_identity_import_run ON source_identity(import_run_id);
955
+ CREATE INDEX IF NOT EXISTS idx_source_identity_hash ON source_identity(external_id_hash);
956
+
957
+ CREATE TABLE IF NOT EXISTS source_import_run (
958
+ id TEXT PRIMARY KEY,
959
+ tenant_id TEXT NOT NULL DEFAULT 'default',
960
+ archive_id TEXT,
961
+ namespace TEXT NOT NULL,
962
+ started_at TIMESTAMPTZ NOT NULL DEFAULT now(),
963
+ completed_at TIMESTAMPTZ,
964
+ checkpoint JSONB NOT NULL DEFAULT '{}',
965
+ receipt JSONB NOT NULL DEFAULT '{}'
966
+ );
967
+
968
+ CREATE TABLE IF NOT EXISTS metadata_index_path (
969
+ path TEXT PRIMARY KEY,
970
+ value_type TEXT NOT NULL,
971
+ indexed_at TIMESTAMPTZ NOT NULL DEFAULT now()
972
+ );
973
+
974
+ INSERT INTO metadata_index_path (path, value_type) VALUES
975
+ ('provider', 'string'),
976
+ ('model', 'string'),
977
+ ('role', 'string'),
978
+ ('event_kind', 'string'),
979
+ ('sensitivity', 'string'),
980
+ ('import_run_id', 'string')
981
+ ON CONFLICT (path) DO NOTHING;
982
+
983
+ CREATE INDEX IF NOT EXISTS idx_note_metadata_provider
984
+ ON note_revised_current ((ai_metadata ->> 'provider'));
985
+ CREATE INDEX IF NOT EXISTS idx_note_metadata_model
986
+ ON note_revised_current ((ai_metadata ->> 'model'));
987
+ CREATE INDEX IF NOT EXISTS idx_note_metadata_role
988
+ ON note_revised_current ((ai_metadata ->> 'role'));
989
+ CREATE INDEX IF NOT EXISTS idx_note_metadata_event_kind
990
+ ON note_revised_current ((ai_metadata ->> 'event_kind'));
991
+ CREATE INDEX IF NOT EXISTS idx_note_metadata_sensitivity
992
+ ON note_revised_current ((ai_metadata ->> 'sensitivity'));
993
+
994
+ CREATE TABLE IF NOT EXISTS deletion_receipt (
995
+ id TEXT PRIMARY KEY,
996
+ operation_key TEXT NOT NULL UNIQUE,
997
+ tenant_id TEXT NOT NULL DEFAULT 'default',
998
+ archive_id TEXT,
999
+ selector_hash TEXT NOT NULL,
1000
+ outcome TEXT NOT NULL,
1001
+ counts JSONB NOT NULL,
1002
+ completed_at TIMESTAMPTZ NOT NULL DEFAULT now(),
1003
+ policy JSONB NOT NULL DEFAULT '{}'
1004
+ );
1005
+ `
1006
+ };
1007
+
929
1008
  // src/migrations/index.ts
930
1009
  var allMigrations = [
931
1010
  migration0001,
@@ -949,7 +1028,8 @@ var allMigrations = [
949
1028
  migration0019,
950
1029
  migration0020,
951
1030
  migration0021,
952
- migration0022
1031
+ migration0022,
1032
+ migration0023
953
1033
  ];
954
1034
 
955
1035
  // src/data-archive.ts
@@ -1732,7 +1812,7 @@ var EmbeddingSetsRepository = class {
1732
1812
  const set = await this.get(setId);
1733
1813
  if (set.kind !== "virtual") throw new Error(`Embedding set is not virtual: ${setId}`);
1734
1814
  const definition = this.definitionFromRow(set);
1735
- const now = (/* @__PURE__ */ new Date()).toISOString();
1815
+ const now2 = (/* @__PURE__ */ new Date()).toISOString();
1736
1816
  const materialization = definition.materialization ? { ...definition.materialization, freshness: "stale" } : null;
1737
1817
  await this.db.query(
1738
1818
  `UPDATE embedding_set
@@ -1741,7 +1821,7 @@ var EmbeddingSetsRepository = class {
1741
1821
  [
1742
1822
  setId,
1743
1823
  jsonParam(materialization),
1744
- jsonParam({ status: "stale", sourceHash: definition.materialization?.inputHash, checkedAt: now, reason })
1824
+ jsonParam({ status: "stale", sourceHash: definition.materialization?.inputHash, checkedAt: now2, reason })
1745
1825
  ]
1746
1826
  );
1747
1827
  }
@@ -2004,6 +2084,90 @@ var EmbeddingSetsRepository = class {
2004
2084
  }
2005
2085
  };
2006
2086
 
2087
+ // src/repositories/metadata-predicates.ts
2088
+ var REGISTERED_METADATA_PATHS = [
2089
+ "provider",
2090
+ "model",
2091
+ "role",
2092
+ "event_kind",
2093
+ "sensitivity",
2094
+ "import_run_id"
2095
+ ];
2096
+ var REGISTERED_SET = new Set(REGISTERED_METADATA_PATHS);
2097
+ var MAX_PREDICATES = 8;
2098
+ var MAX_IN_VALUES = 32;
2099
+ var MAX_VALUE_LENGTH = 256;
2100
+ function assertRegisteredPath(path) {
2101
+ if (!REGISTERED_SET.has(path) || path.includes(".") || path.includes("/")) {
2102
+ throw new Error(`Unsupported metadata predicate path: ${path}`);
2103
+ }
2104
+ }
2105
+ function assertBoundedValue(value) {
2106
+ if (typeof value === "string" && value.length > MAX_VALUE_LENGTH) {
2107
+ throw new Error("Metadata predicate value exceeds the 256 character bound");
2108
+ }
2109
+ }
2110
+ function jsonAccessor(path) {
2111
+ if (path === "import_run_id") return "si.import_run_id";
2112
+ return `c.ai_metadata ->> '${path}'`;
2113
+ }
2114
+ function buildMetadataPredicateConditions(options, startIdx) {
2115
+ const predicates = options.metadataPredicates ?? [];
2116
+ if (predicates.length > MAX_PREDICATES) {
2117
+ throw new Error(`Metadata predicate count exceeds the ${MAX_PREDICATES} predicate bound`);
2118
+ }
2119
+ const conditions = [];
2120
+ const params = [];
2121
+ const joins = [];
2122
+ let idx = startIdx;
2123
+ let needsSourceJoin = options.tenant_id !== void 0 || options.archive_id !== void 0;
2124
+ if (options.tenant_id !== void 0) {
2125
+ conditions.push(`COALESCE(si.tenant_id, 'default') = $${idx++}`);
2126
+ params.push(options.tenant_id);
2127
+ }
2128
+ if (options.archive_id !== void 0) {
2129
+ conditions.push(`si.archive_id IS NOT DISTINCT FROM $${idx++}`);
2130
+ params.push(options.archive_id);
2131
+ }
2132
+ for (const predicate of predicates) {
2133
+ assertRegisteredPath(predicate.path);
2134
+ const lhs = jsonAccessor(predicate.path);
2135
+ if (predicate.path === "import_run_id") needsSourceJoin = true;
2136
+ if (predicate.op === "eq") {
2137
+ assertBoundedValue(predicate.value);
2138
+ conditions.push(`${lhs} IS NOT DISTINCT FROM $${idx++}`);
2139
+ params.push(predicate.value == null ? null : String(predicate.value));
2140
+ } else if (predicate.op === "in") {
2141
+ if (predicate.value.length > MAX_IN_VALUES) {
2142
+ throw new Error(`Metadata predicate membership exceeds the ${MAX_IN_VALUES} value bound`);
2143
+ }
2144
+ for (const value of predicate.value) assertBoundedValue(value);
2145
+ conditions.push(`${lhs} = ANY($${idx++})`);
2146
+ params.push(predicate.value.map((value) => value == null ? null : String(value)));
2147
+ } else if (predicate.op === "range") {
2148
+ if (predicate.gte === void 0 && predicate.lte === void 0) {
2149
+ throw new Error("Metadata range predicate requires gte or lte");
2150
+ }
2151
+ if (predicate.gte !== void 0) {
2152
+ assertBoundedValue(predicate.gte);
2153
+ conditions.push(`${lhs} >= $${idx++}`);
2154
+ params.push(String(predicate.gte));
2155
+ }
2156
+ if (predicate.lte !== void 0) {
2157
+ assertBoundedValue(predicate.lte);
2158
+ conditions.push(`${lhs} <= $${idx++}`);
2159
+ params.push(String(predicate.lte));
2160
+ }
2161
+ } else {
2162
+ conditions.push(predicate.value === false ? `${lhs} IS NULL` : `${lhs} IS NOT NULL`);
2163
+ }
2164
+ }
2165
+ if (needsSourceJoin) {
2166
+ joins.push("LEFT JOIN source_identity si ON si.note_id = n.id");
2167
+ }
2168
+ return { conditions, joins, params, nextIdx: idx };
2169
+ }
2170
+
2007
2171
  // src/repositories/search-repository.ts
2008
2172
  var ATTACHMENT_TEXT_JOIN2 = `
2009
2173
  LEFT JOIN (
@@ -2072,6 +2236,41 @@ var SearchRepository = class {
2072
2236
  attachEmbeddingStatus(results, embeddingSet) {
2073
2237
  return results.map((r) => ({ ...r, has_embedding: embeddingSet.has(r.id) }));
2074
2238
  }
2239
+ async fetchLocatorMap(noteIds, metadataPaths = []) {
2240
+ const locators = /* @__PURE__ */ new Map();
2241
+ if (noteIds.length === 0) return locators;
2242
+ const result = await this.db.query(
2243
+ `SELECT note_id, namespace, external_id_hash, import_run_id, source_schema_version
2244
+ FROM source_identity
2245
+ WHERE note_id = ANY($1)
2246
+ ORDER BY created_at ASC`,
2247
+ [noteIds]
2248
+ );
2249
+ for (const row of result.rows) {
2250
+ const existing = locators.get(row.note_id) ?? [];
2251
+ existing.push({
2252
+ note_id: row.note_id,
2253
+ chunk: { kind: "current", index: 0 },
2254
+ source: {
2255
+ namespace: row.namespace,
2256
+ external_id_hash: row.external_id_hash,
2257
+ import_run_id: row.import_run_id,
2258
+ schema_version: row.source_schema_version
2259
+ },
2260
+ metadata_paths: [...metadataPaths]
2261
+ });
2262
+ locators.set(row.note_id, existing);
2263
+ }
2264
+ for (const noteId of noteIds) {
2265
+ if (!locators.has(noteId)) {
2266
+ locators.set(noteId, [{ note_id: noteId, chunk: { kind: "current", index: 0 }, metadata_paths: [...metadataPaths] }]);
2267
+ }
2268
+ }
2269
+ return locators;
2270
+ }
2271
+ metadataPaths(options) {
2272
+ return [...new Set((options.metadataPredicates ?? []).map((predicate) => predicate.path))];
2273
+ }
2075
2274
  async search(query, options = {}, queryEmbedding) {
2076
2275
  const { limit = 20, offset = 0 } = options;
2077
2276
  const mode = options.mode ?? "auto";
@@ -2098,17 +2297,21 @@ var SearchRepository = class {
2098
2297
  const resolvedEmbeddingSet = await this.resolveEmbeddingSet(options);
2099
2298
  const tsqFn = this.tsqueryFn(query);
2100
2299
  const { conditions, params, nextIdx } = buildNoteConditions(options, 2);
2300
+ const metadata = buildMetadataPredicateConditions(options, nextIdx);
2301
+ conditions.push(...metadata.conditions);
2302
+ params.push(...metadata.params);
2101
2303
  conditions.unshift(
2102
2304
  `(n.tsv @@ ${tsqFn}('english', $1) OR
2103
2305
  ${COMBINED_TEXT_VECTOR_SQL2} @@ ${tsqFn}('english', $1))`
2104
2306
  );
2105
- let paramIdx = this.scopeToResolvedEmbeddingSet(conditions, params, nextIdx, resolvedEmbeddingSet);
2307
+ let paramIdx = this.scopeToResolvedEmbeddingSet(conditions, params, metadata.nextIdx, resolvedEmbeddingSet);
2106
2308
  const allParams = [query, ...params];
2107
2309
  const where = conditions.join(" AND ");
2108
2310
  const countResult = await this.db.query(
2109
2311
  `SELECT COUNT(*) as count
2110
2312
  FROM note n
2111
2313
  LEFT JOIN note_revised_current c ON c.note_id = n.id
2314
+ ${metadata.joins.join("\n")}
2112
2315
  ${ATTACHMENT_TEXT_JOIN2}
2113
2316
  WHERE ${where}`,
2114
2317
  allParams
@@ -2129,6 +2332,7 @@ var SearchRepository = class {
2129
2332
  ) as snippet
2130
2333
  FROM note n
2131
2334
  LEFT JOIN note_revised_current c ON c.note_id = n.id
2335
+ ${metadata.joins.join("\n")}
2132
2336
  ${ATTACHMENT_TEXT_JOIN2}
2133
2337
  WHERE ${where}
2134
2338
  ORDER BY rank DESC, n.created_at DESC
@@ -2145,12 +2349,14 @@ var SearchRepository = class {
2145
2349
  const idsResult = await this.db.query(
2146
2350
  `SELECT n.id FROM note n
2147
2351
  LEFT JOIN note_revised_current c ON c.note_id = n.id
2352
+ ${metadata.joins.join("\n")}
2148
2353
  ${ATTACHMENT_TEXT_JOIN2}
2149
2354
  WHERE ${where}`,
2150
2355
  allParams
2151
2356
  );
2152
2357
  facets = await this.fetchFacets(idsResult.rows.map((r) => r.id));
2153
2358
  }
2359
+ const locatorMap = await this.fetchLocatorMap(resultIds, this.metadataPaths(options));
2154
2360
  const baseResults = result.rows.map((r) => ({
2155
2361
  id: r.id,
2156
2362
  title: r.title,
@@ -2158,7 +2364,8 @@ var SearchRepository = class {
2158
2364
  rank: r.rank,
2159
2365
  created_at: r.created_at,
2160
2366
  updated_at: r.updated_at,
2161
- tags: tagMap.get(r.id) ?? []
2367
+ tags: tagMap.get(r.id) ?? [],
2368
+ locators: locatorMap.get(r.id) ?? []
2162
2369
  }));
2163
2370
  return {
2164
2371
  results: this.attachEmbeddingStatus(baseResults, embeddingSet),
@@ -2176,12 +2383,17 @@ var SearchRepository = class {
2176
2383
  const vectorStr = `[${queryEmbedding.join(",")}]`;
2177
2384
  const resolvedEmbeddingSet = await this.resolveEmbeddingSet(options);
2178
2385
  const { conditions, params, nextIdx } = buildNoteConditions(options, 1);
2179
- let paramIdx = this.scopeToResolvedEmbeddingRows(conditions, params, nextIdx, resolvedEmbeddingSet);
2386
+ const metadata = buildMetadataPredicateConditions(options, nextIdx);
2387
+ conditions.push(...metadata.conditions);
2388
+ params.push(...metadata.params);
2389
+ let paramIdx = this.scopeToResolvedEmbeddingRows(conditions, params, metadata.nextIdx, resolvedEmbeddingSet);
2180
2390
  const where = conditions.join(" AND ");
2181
2391
  const countResult = await this.db.query(
2182
2392
  `SELECT COUNT(*) as count
2183
2393
  FROM embedding e
2184
2394
  JOIN note n ON n.id = e.note_id
2395
+ LEFT JOIN note_revised_current c ON c.note_id = n.id
2396
+ ${metadata.joins.join("\n")}
2185
2397
  WHERE ${where}`,
2186
2398
  params
2187
2399
  );
@@ -2196,6 +2408,7 @@ var SearchRepository = class {
2196
2408
  FROM embedding e
2197
2409
  JOIN note n ON n.id = e.note_id
2198
2410
  LEFT JOIN note_revised_current c ON c.note_id = n.id
2411
+ ${metadata.joins.join("\n")}
2199
2412
  ${ATTACHMENT_TEXT_JOIN2}
2200
2413
  WHERE ${where}
2201
2414
  ORDER BY e.vector <=> $${vecIdx}::vector ASC
@@ -2204,9 +2417,15 @@ var SearchRepository = class {
2204
2417
  );
2205
2418
  const tagMap = await this.fetchTagMap(result.rows.map((r) => r.id));
2206
2419
  const facets = options.include_facets ? await this.fetchFacets((await this.db.query(
2207
- `SELECT n.id FROM embedding e JOIN note n ON n.id = e.note_id WHERE ${where}`,
2420
+ `SELECT n.id
2421
+ FROM embedding e
2422
+ JOIN note n ON n.id = e.note_id
2423
+ LEFT JOIN note_revised_current c ON c.note_id = n.id
2424
+ ${metadata.joins.join("\n")}
2425
+ WHERE ${where}`,
2208
2426
  params
2209
2427
  )).rows.map((r) => r.id)) : void 0;
2428
+ const locatorMap = await this.fetchLocatorMap(result.rows.map((r) => r.id), this.metadataPaths(options));
2210
2429
  return {
2211
2430
  results: result.rows.map((r) => ({
2212
2431
  id: r.id,
@@ -2216,7 +2435,8 @@ var SearchRepository = class {
2216
2435
  created_at: r.created_at,
2217
2436
  updated_at: r.updated_at,
2218
2437
  tags: tagMap.get(r.id) ?? [],
2219
- has_embedding: true
2438
+ has_embedding: true,
2439
+ locators: locatorMap.get(r.id) ?? []
2220
2440
  })),
2221
2441
  total,
2222
2442
  query: "",
@@ -2234,12 +2454,15 @@ var SearchRepository = class {
2234
2454
  const tsqFn = this.tsqueryFn(query);
2235
2455
  const resolvedEmbeddingSet = await this.resolveEmbeddingSet(options);
2236
2456
  const textCond = buildNoteConditions(options, 2);
2457
+ const textMeta = buildMetadataPredicateConditions(options, textCond.nextIdx);
2237
2458
  const textConditions = [
2238
2459
  ...textCond.conditions,
2460
+ ...textMeta.conditions,
2239
2461
  `(n.tsv @@ ${tsqFn}('english', $1) OR
2240
2462
  ${COMBINED_TEXT_VECTOR_SQL2} @@ ${tsqFn}('english', $1))`
2241
2463
  ];
2242
- this.scopeToResolvedEmbeddingSet(textConditions, textCond.params, textCond.nextIdx, resolvedEmbeddingSet);
2464
+ textCond.params.push(...textMeta.params);
2465
+ this.scopeToResolvedEmbeddingSet(textConditions, textCond.params, textMeta.nextIdx, resolvedEmbeddingSet);
2243
2466
  const textWhere = textConditions.join(" AND ");
2244
2467
  const textParams = [query, ...textCond.params];
2245
2468
  const textResult = await this.db.query(
@@ -2250,6 +2473,7 @@ var SearchRepository = class {
2250
2473
  ) as rank
2251
2474
  FROM note n
2252
2475
  LEFT JOIN note_revised_current c ON c.note_id = n.id
2476
+ ${textMeta.joins.join("\n")}
2253
2477
  ${ATTACHMENT_TEXT_JOIN2}
2254
2478
  WHERE ${textWhere}
2255
2479
  ORDER BY rank DESC
@@ -2257,13 +2481,18 @@ var SearchRepository = class {
2257
2481
  textParams
2258
2482
  );
2259
2483
  const vecCond = buildNoteConditions(options, 1);
2260
- vecCond.nextIdx = this.scopeToResolvedEmbeddingRows(vecCond.conditions, vecCond.params, vecCond.nextIdx, resolvedEmbeddingSet);
2484
+ const vecMeta = buildMetadataPredicateConditions(options, vecCond.nextIdx);
2485
+ vecCond.conditions.push(...vecMeta.conditions);
2486
+ vecCond.params.push(...vecMeta.params);
2487
+ vecCond.nextIdx = this.scopeToResolvedEmbeddingRows(vecCond.conditions, vecCond.params, vecMeta.nextIdx, resolvedEmbeddingSet);
2261
2488
  const vecWhere = vecCond.conditions.join(" AND ");
2262
2489
  const vecVecIdx = vecCond.nextIdx;
2263
2490
  const vectorResult = await this.db.query(
2264
2491
  `SELECT n.id, (e.vector <=> $${vecVecIdx}::vector) as distance
2265
2492
  FROM embedding e
2266
2493
  JOIN note n ON n.id = e.note_id
2494
+ LEFT JOIN note_revised_current c ON c.note_id = n.id
2495
+ ${vecMeta.joins.join("\n")}
2267
2496
  WHERE ${vecWhere}
2268
2497
  ORDER BY e.vector <=> $${vecVecIdx}::vector ASC
2269
2498
  LIMIT 100`,
@@ -2296,6 +2525,7 @@ var SearchRepository = class {
2296
2525
  this.fetchTagMap(pageIds),
2297
2526
  this.fetchEmbeddingStatus(pageIds, resolvedEmbeddingSet, options.embeddingSetId)
2298
2527
  ]);
2528
+ const locatorMap = await this.fetchLocatorMap(pageIds, this.metadataPaths(options));
2299
2529
  const facets = options.include_facets ? await this.fetchFacets(sortedIds) : void 0;
2300
2530
  return {
2301
2531
  results: pageIds.map((id) => {
@@ -2309,7 +2539,8 @@ var SearchRepository = class {
2309
2539
  created_at: r.created_at,
2310
2540
  updated_at: r.updated_at,
2311
2541
  tags: tagMap.get(id) ?? [],
2312
- has_embedding: embeddingSet.has(id)
2542
+ has_embedding: embeddingSet.has(id),
2543
+ locators: locatorMap.get(id) ?? []
2313
2544
  };
2314
2545
  }).filter((r) => r !== null),
2315
2546
  total,
@@ -2325,10 +2556,17 @@ var SearchRepository = class {
2325
2556
  const { limit = 20, offset = 0 } = options;
2326
2557
  const resolvedEmbeddingSet = await this.resolveEmbeddingSet(options);
2327
2558
  const { conditions, params, nextIdx } = buildNoteConditions(options, 1);
2328
- let paramIdx = this.scopeToResolvedEmbeddingSet(conditions, params, nextIdx, resolvedEmbeddingSet);
2559
+ const metadata = buildMetadataPredicateConditions(options, nextIdx);
2560
+ conditions.push(...metadata.conditions);
2561
+ params.push(...metadata.params);
2562
+ let paramIdx = this.scopeToResolvedEmbeddingSet(conditions, params, metadata.nextIdx, resolvedEmbeddingSet);
2329
2563
  const where = conditions.join(" AND ");
2330
2564
  const countResult = await this.db.query(
2331
- `SELECT COUNT(*) as count FROM note n WHERE ${where}`,
2565
+ `SELECT COUNT(*) as count
2566
+ FROM note n
2567
+ LEFT JOIN note_revised_current c ON c.note_id = n.id
2568
+ ${metadata.joins.join("\n")}
2569
+ WHERE ${where}`,
2332
2570
  params
2333
2571
  );
2334
2572
  const total = parseInt(countResult.rows[0].count, 10);
@@ -2338,6 +2576,7 @@ var SearchRepository = class {
2338
2576
  LEFT(${COMBINED_TEXT_SQL}, 200) as snippet
2339
2577
  FROM note n
2340
2578
  LEFT JOIN note_revised_current c ON c.note_id = n.id
2579
+ ${metadata.joins.join("\n")}
2341
2580
  ${ATTACHMENT_TEXT_JOIN2}
2342
2581
  WHERE ${where}
2343
2582
  ORDER BY n.created_at DESC
@@ -2346,6 +2585,7 @@ var SearchRepository = class {
2346
2585
  );
2347
2586
  const resultIds = result.rows.map((r) => r.id);
2348
2587
  const embeddingSet = await this.fetchEmbeddingStatus(resultIds, resolvedEmbeddingSet, options.embeddingSetId);
2588
+ const locatorMap = await this.fetchLocatorMap(resultIds, this.metadataPaths(options));
2349
2589
  return {
2350
2590
  results: result.rows.map((r) => ({
2351
2591
  id: r.id,
@@ -2355,7 +2595,8 @@ var SearchRepository = class {
2355
2595
  created_at: r.created_at,
2356
2596
  updated_at: r.updated_at,
2357
2597
  tags: [],
2358
- has_embedding: embeddingSet.has(r.id)
2598
+ has_embedding: embeddingSet.has(r.id),
2599
+ locators: locatorMap.get(r.id) ?? []
2359
2600
  })),
2360
2601
  total,
2361
2602
  query: "",
@@ -3945,8 +4186,8 @@ async function migrateLegacyBlobStore(archiveName, target, indexedDbFactory) {
3945
4186
 
3946
4187
  // src/blob-store.ts
3947
4188
  var MemoryBlobStore = class {
3948
- constructor(now = Date.now) {
3949
- this.now = now;
4189
+ constructor(now2 = Date.now) {
4190
+ this.now = now2;
3950
4191
  }
3951
4192
  entries = /* @__PURE__ */ new Map();
3952
4193
  async put(bytes) {
@@ -4019,12 +4260,12 @@ function toChecksum(hex) {
4019
4260
  return CHECKSUM_PREFIX + hex;
4020
4261
  }
4021
4262
  var BytecaskBlobStore = class {
4022
- constructor(facade, adapter, index, probe, now = Date.now) {
4263
+ constructor(facade, adapter, index, probe, now2 = Date.now) {
4023
4264
  this.facade = facade;
4024
4265
  this.adapter = adapter;
4025
4266
  this.index = index;
4026
4267
  this.probe = probe;
4027
- this.now = now;
4268
+ this.now = now2;
4028
4269
  }
4029
4270
  async put(bytes) {
4030
4271
  return toChecksum(await this.facade.put(bytes));
@@ -4159,6 +4400,482 @@ function createLazyBlobStore(archiveName, options) {
4159
4400
  };
4160
4401
  }
4161
4402
 
4403
+ // src/repositories/source-upsert-repository.ts
4404
+ var DEFAULT_MAX_ITEMS = 500;
4405
+ function assertSource(input) {
4406
+ if (!input.namespace || input.namespace.length > 128) throw new Error("Source namespace is required and must be <= 128 characters");
4407
+ if (!input.external_id || input.external_id.length > 1024) throw new Error("Source external_id is required and must be <= 1024 characters");
4408
+ if (!input.source_schema_version || input.source_schema_version.length > 64) {
4409
+ throw new Error("Source schema version is required and must be <= 64 characters");
4410
+ }
4411
+ if (!input.import_run_id || input.import_run_id.length > 128) throw new Error("Source import_run_id is required and must be <= 128 characters");
4412
+ }
4413
+ function sourceHash(source) {
4414
+ return computeHash(new TextEncoder().encode([
4415
+ source.tenant_id ?? "default",
4416
+ source.archive_id ?? "",
4417
+ source.namespace,
4418
+ source.external_id
4419
+ ].join("\0")));
4420
+ }
4421
+ function contentDigest(content) {
4422
+ return computeHash(new TextEncoder().encode(content));
4423
+ }
4424
+ async function insertNote(tx, input, noteId, digest) {
4425
+ const originalId = generateId();
4426
+ if (input.source.archive_id) {
4427
+ await tx.query(
4428
+ `INSERT INTO archive (id, name)
4429
+ VALUES ($1, $2)
4430
+ ON CONFLICT (id) DO NOTHING`,
4431
+ [input.source.archive_id, input.source.archive_id]
4432
+ );
4433
+ }
4434
+ await tx.query(
4435
+ `INSERT INTO note (id, archive_id, title, format, source, visibility)
4436
+ VALUES ($1, $2, $3, $4, $5, $6)`,
4437
+ [
4438
+ noteId,
4439
+ input.source.archive_id ?? null,
4440
+ input.title ?? null,
4441
+ input.format ?? "markdown",
4442
+ `source:${input.source.namespace}`,
4443
+ input.visibility ?? "private"
4444
+ ]
4445
+ );
4446
+ await tx.query(
4447
+ `INSERT INTO note_original (id, note_id, content, content_hash)
4448
+ VALUES ($1, $2, $3, $4)`,
4449
+ [originalId, noteId, input.content, digest]
4450
+ );
4451
+ await tx.query(
4452
+ `INSERT INTO note_revised_current (note_id, content, ai_metadata)
4453
+ VALUES ($1, $2, $3::jsonb)`,
4454
+ [noteId, input.content, JSON.stringify(input.metadata ?? null)]
4455
+ );
4456
+ }
4457
+ async function updateNote(tx, input, noteId, outcome) {
4458
+ if (input.source.archive_id) {
4459
+ await tx.query(
4460
+ `INSERT INTO archive (id, name)
4461
+ VALUES ($1, $2)
4462
+ ON CONFLICT (id) DO NOTHING`,
4463
+ [input.source.archive_id, input.source.archive_id]
4464
+ );
4465
+ }
4466
+ if (outcome === "versioned") {
4467
+ const count2 = await tx.query(
4468
+ `SELECT COUNT(*) AS count FROM note_revision WHERE note_id = $1`,
4469
+ [noteId]
4470
+ );
4471
+ const current = await tx.query(
4472
+ `SELECT content, ai_metadata FROM note_revised_current WHERE note_id = $1`,
4473
+ [noteId]
4474
+ );
4475
+ const nextRevision = Number.parseInt(count2.rows[0]?.count ?? "0", 10) + 1;
4476
+ if (current.rows[0]) {
4477
+ await tx.query(
4478
+ `INSERT INTO note_revision (id, note_id, revision_number, type, content, ai_metadata)
4479
+ VALUES ($1, $2, $3, 'source-import', $4, $5::jsonb)`,
4480
+ [
4481
+ generateId(),
4482
+ noteId,
4483
+ nextRevision,
4484
+ current.rows[0].content,
4485
+ JSON.stringify(current.rows[0].ai_metadata ?? null)
4486
+ ]
4487
+ );
4488
+ }
4489
+ }
4490
+ await tx.query(
4491
+ `UPDATE note
4492
+ SET title = $1, format = $2, visibility = $3, archive_id = $4, updated_at = now(), deleted_at = NULL
4493
+ WHERE id = $5`,
4494
+ [
4495
+ input.title ?? null,
4496
+ input.format ?? "markdown",
4497
+ input.visibility ?? "private",
4498
+ input.source.archive_id ?? null,
4499
+ noteId
4500
+ ]
4501
+ );
4502
+ await tx.query(
4503
+ `UPDATE note_revised_current
4504
+ SET content = $1, ai_metadata = $2::jsonb, is_user_edited = false, updated_at = now()
4505
+ WHERE note_id = $3`,
4506
+ [input.content, JSON.stringify(input.metadata ?? null), noteId]
4507
+ );
4508
+ }
4509
+ var SourceUpsertRepository = class {
4510
+ constructor(db, events) {
4511
+ this.db = db;
4512
+ this.events = events;
4513
+ }
4514
+ async upsertBatch(items, options = {}) {
4515
+ const maxItems = options.maxItems ?? DEFAULT_MAX_ITEMS;
4516
+ if (items.length > maxItems) throw new Error(`Source upsert batch exceeds the ${maxItems} item bound`);
4517
+ if (items.length === 0) {
4518
+ return {
4519
+ import_run_id: "",
4520
+ dry_run: options.dryRun === true,
4521
+ outcomes: [],
4522
+ counts: { inserted: 0, unchanged: 0, versioned: 0, replaced: 0, conflict: 0, rejected: 0 }
4523
+ };
4524
+ }
4525
+ const outcomes = [];
4526
+ for (const [index, item] of items.entries()) {
4527
+ try {
4528
+ assertSource(item.source);
4529
+ outcomes.push({
4530
+ index,
4531
+ outcome: "rejected",
4532
+ external_id_hash: sourceHash(item.source),
4533
+ content_digest: contentDigest(item.content)
4534
+ });
4535
+ } catch (error) {
4536
+ outcomes.push({
4537
+ index,
4538
+ outcome: "rejected",
4539
+ external_id_hash: item.source ? sourceHash({ ...item.source, external_id: item.source.external_id ?? "" }) : "",
4540
+ content_digest: contentDigest(item.content ?? ""),
4541
+ reason: error instanceof Error ? error.message : String(error)
4542
+ });
4543
+ }
4544
+ }
4545
+ if (outcomes.some((outcome) => outcome.reason)) {
4546
+ return this.finish(items[0].source?.import_run_id ?? "", options.dryRun === true, outcomes);
4547
+ }
4548
+ if (options.dryRun) {
4549
+ const preview = [];
4550
+ for (const [index, item] of items.entries()) {
4551
+ const externalIdHash = sourceHash(item.source);
4552
+ const digest = contentDigest(item.content);
4553
+ const existing = await this.db.query(
4554
+ `SELECT note_id, content_digest
4555
+ FROM source_identity
4556
+ WHERE tenant_id = $1
4557
+ AND archive_id IS NOT DISTINCT FROM $2
4558
+ AND namespace = $3
4559
+ AND external_id = $4
4560
+ LIMIT 1`,
4561
+ [item.source.tenant_id ?? "default", item.source.archive_id ?? null, item.source.namespace, item.source.external_id]
4562
+ );
4563
+ if (existing.rows.length === 0) {
4564
+ preview.push({ index, outcome: "inserted", external_id_hash: externalIdHash, content_digest: digest });
4565
+ } else if (existing.rows[0].content_digest === digest) {
4566
+ preview.push({ index, outcome: "unchanged", note_id: existing.rows[0].note_id, external_id_hash: externalIdHash, content_digest: digest });
4567
+ } else if ((item.policy ?? "version") === "conflict") {
4568
+ preview.push({ index, outcome: "conflict", note_id: existing.rows[0].note_id, external_id_hash: externalIdHash, content_digest: digest });
4569
+ } else {
4570
+ preview.push({
4571
+ index,
4572
+ outcome: item.policy === "replace" ? "replaced" : "versioned",
4573
+ note_id: existing.rows[0].note_id,
4574
+ external_id_hash: externalIdHash,
4575
+ content_digest: digest
4576
+ });
4577
+ }
4578
+ }
4579
+ return this.finish(items[0].source.import_run_id, true, preview);
4580
+ }
4581
+ await this.db.transaction(async (tx) => {
4582
+ for (const [index, item] of items.entries()) {
4583
+ const externalIdHash = sourceHash(item.source);
4584
+ const digest = contentDigest(item.content);
4585
+ const existing = await tx.query(
4586
+ `SELECT note_id, content_digest
4587
+ FROM source_identity
4588
+ WHERE tenant_id = $1
4589
+ AND archive_id IS NOT DISTINCT FROM $2
4590
+ AND namespace = $3
4591
+ AND external_id = $4
4592
+ LIMIT 1`,
4593
+ [item.source.tenant_id ?? "default", item.source.archive_id ?? null, item.source.namespace, item.source.external_id]
4594
+ );
4595
+ if (existing.rows.length === 0) {
4596
+ const noteId = item.source.caller_stable_id ?? generateId();
4597
+ await insertNote(tx, item, noteId, digest);
4598
+ await tx.query(
4599
+ `INSERT INTO source_identity
4600
+ (id, tenant_id, archive_id, namespace, external_id, external_id_hash,
4601
+ source_schema_version, content_digest, import_run_id, caller_stable_id, note_id)
4602
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)`,
4603
+ [
4604
+ generateId(),
4605
+ item.source.tenant_id ?? "default",
4606
+ item.source.archive_id ?? null,
4607
+ item.source.namespace,
4608
+ item.source.external_id,
4609
+ externalIdHash,
4610
+ item.source.source_schema_version,
4611
+ digest,
4612
+ item.source.import_run_id,
4613
+ item.source.caller_stable_id ?? null,
4614
+ noteId
4615
+ ]
4616
+ );
4617
+ outcomes[index] = { index, outcome: "inserted", note_id: noteId, external_id_hash: externalIdHash, content_digest: digest };
4618
+ continue;
4619
+ }
4620
+ const row = existing.rows[0];
4621
+ if (row.content_digest === digest) {
4622
+ outcomes[index] = { index, outcome: "unchanged", note_id: row.note_id, external_id_hash: externalIdHash, content_digest: digest };
4623
+ continue;
4624
+ }
4625
+ const policy = item.policy ?? "version";
4626
+ if (policy === "conflict") {
4627
+ outcomes[index] = { index, outcome: "conflict", note_id: row.note_id, external_id_hash: externalIdHash, content_digest: digest };
4628
+ continue;
4629
+ }
4630
+ const outcome = policy === "replace" ? "replaced" : "versioned";
4631
+ await updateNote(tx, item, row.note_id, outcome);
4632
+ await tx.query(
4633
+ `UPDATE source_identity
4634
+ SET source_schema_version = $1, content_digest = $2, import_run_id = $3, updated_at = now()
4635
+ WHERE note_id = $4
4636
+ AND tenant_id = $5
4637
+ AND archive_id IS NOT DISTINCT FROM $6
4638
+ AND namespace = $7
4639
+ AND external_id = $8`,
4640
+ [
4641
+ item.source.source_schema_version,
4642
+ digest,
4643
+ item.source.import_run_id,
4644
+ row.note_id,
4645
+ item.source.tenant_id ?? "default",
4646
+ item.source.archive_id ?? null,
4647
+ item.source.namespace,
4648
+ item.source.external_id
4649
+ ]
4650
+ );
4651
+ outcomes[index] = { index, outcome, note_id: row.note_id, external_id_hash: externalIdHash, content_digest: digest };
4652
+ }
4653
+ if (hasMaterialChange(outcomes)) {
4654
+ await tx.query(
4655
+ `INSERT INTO source_import_run (id, tenant_id, archive_id, namespace, completed_at, checkpoint, receipt)
4656
+ VALUES ($1, $2, $3, $4, now(), $5::jsonb, $6::jsonb)
4657
+ ON CONFLICT (id) DO UPDATE
4658
+ SET completed_at = EXCLUDED.completed_at,
4659
+ checkpoint = EXCLUDED.checkpoint,
4660
+ receipt = EXCLUDED.receipt`,
4661
+ [
4662
+ items[0].source.import_run_id,
4663
+ items[0].source.tenant_id ?? "default",
4664
+ items[0].source.archive_id ?? null,
4665
+ items[0].source.namespace,
4666
+ JSON.stringify({ item_count: items.length }),
4667
+ JSON.stringify({ counts: countOutcomes(outcomes) })
4668
+ ]
4669
+ );
4670
+ }
4671
+ });
4672
+ if (hasMaterialChange(outcomes)) {
4673
+ this.events?.emit("source.upserted", { importRunId: items[0].source.import_run_id, counts: countOutcomes(outcomes) });
4674
+ }
4675
+ return this.finish(items[0].source.import_run_id, false, outcomes);
4676
+ }
4677
+ finish(importRunId, dryRun, outcomes) {
4678
+ return { import_run_id: importRunId, dry_run: dryRun, outcomes, counts: countOutcomes(outcomes) };
4679
+ }
4680
+ };
4681
+ function hasMaterialChange(outcomes) {
4682
+ return outcomes.some((outcome) => outcome.outcome === "inserted" || outcome.outcome === "versioned" || outcome.outcome === "replaced");
4683
+ }
4684
+ function countOutcomes(outcomes) {
4685
+ return {
4686
+ inserted: outcomes.filter((outcome) => outcome.outcome === "inserted").length,
4687
+ unchanged: outcomes.filter((outcome) => outcome.outcome === "unchanged").length,
4688
+ versioned: outcomes.filter((outcome) => outcome.outcome === "versioned").length,
4689
+ replaced: outcomes.filter((outcome) => outcome.outcome === "replaced").length,
4690
+ conflict: outcomes.filter((outcome) => outcome.outcome === "conflict").length,
4691
+ rejected: outcomes.filter((outcome) => outcome.outcome === "rejected").length
4692
+ };
4693
+ }
4694
+
4695
+ // src/repositories/lifecycle-purge-repository.ts
4696
+ function zeroCounts() {
4697
+ return {
4698
+ notes: 0,
4699
+ revisions: 0,
4700
+ links: 0,
4701
+ tags: 0,
4702
+ embeddings: 0,
4703
+ attachments: 0,
4704
+ blobs: 0,
4705
+ graph_edges: 0,
4706
+ provenance_edges: 0,
4707
+ source_identities: 0
4708
+ };
4709
+ }
4710
+ function selectorHash(selector) {
4711
+ return computeHash(new TextEncoder().encode(JSON.stringify({
4712
+ tenant_id: selector.tenant_id ?? "default",
4713
+ archive_id: selector.archive_id ?? null,
4714
+ note_ids: [...selector.note_ids ?? []].sort(),
4715
+ source: selector.source ? {
4716
+ namespace: selector.source.namespace,
4717
+ external_id_hash: selector.source.external_id ? computeHash(new TextEncoder().encode(selector.source.external_id)) : null
4718
+ } : null
4719
+ })));
4720
+ }
4721
+ function buildSelectorWhere(selector, startIdx) {
4722
+ const clauses = [];
4723
+ const params = [];
4724
+ let idx = startIdx;
4725
+ if (selector.note_ids?.length) {
4726
+ clauses.push(`n.id = ANY($${idx++})`);
4727
+ params.push([...selector.note_ids]);
4728
+ }
4729
+ if (selector.tenant_id !== void 0 || selector.archive_id !== void 0 || selector.source) {
4730
+ clauses.push(`EXISTS (
4731
+ SELECT 1 FROM source_identity si
4732
+ WHERE si.note_id = n.id
4733
+ AND si.tenant_id = $${idx++}
4734
+ AND si.archive_id IS NOT DISTINCT FROM $${idx++}
4735
+ ${selector.source ? `AND si.namespace = $${idx++}` : ""}
4736
+ ${selector.source?.external_id ? `AND si.external_id = $${idx++}` : ""}
4737
+ )`);
4738
+ params.push(selector.tenant_id ?? "default", selector.archive_id ?? null);
4739
+ if (selector.source) params.push(selector.source.namespace);
4740
+ if (selector.source?.external_id) params.push(selector.source.external_id);
4741
+ }
4742
+ if (clauses.length === 0) throw new Error("Purge selector must target note_ids or source identity");
4743
+ return { sql: clauses.join(" AND "), params };
4744
+ }
4745
+ async function selectedNoteIds(db, selector) {
4746
+ const where = buildSelectorWhere(selector, 1);
4747
+ const result = await db.query(
4748
+ `SELECT n.id FROM note n WHERE ${where.sql} ORDER BY n.id`,
4749
+ where.params
4750
+ );
4751
+ return result.rows.map((row) => row.id);
4752
+ }
4753
+ var LifecyclePurgeRepository = class {
4754
+ constructor(db, events) {
4755
+ this.db = db;
4756
+ this.events = events;
4757
+ }
4758
+ async preview(selector) {
4759
+ const noteIds = await selectedNoteIds(this.db, selector);
4760
+ return { selector_hash: selectorHash(selector), counts: await this.count(noteIds) };
4761
+ }
4762
+ async purge(selector, operationKey) {
4763
+ const existing = await this.db.query(
4764
+ `SELECT id, operation_key, tenant_id, archive_id, selector_hash, outcome, counts, completed_at, policy
4765
+ FROM deletion_receipt
4766
+ WHERE operation_key = $1`,
4767
+ [operationKey]
4768
+ );
4769
+ if (existing.rows[0]) return existing.rows[0];
4770
+ const hash = selectorHash(selector);
4771
+ let receipt;
4772
+ await this.db.transaction(async (tx) => {
4773
+ const noteIds = await selectedNoteIds(tx, selector);
4774
+ const counts = await this.count(noteIds, tx);
4775
+ await this.deleteSelected(tx, noteIds);
4776
+ receipt = {
4777
+ id: generateId(),
4778
+ operation_key: operationKey,
4779
+ tenant_id: selector.tenant_id ?? "default",
4780
+ archive_id: selector.archive_id ?? null,
4781
+ selector_hash: hash,
4782
+ outcome: "completed",
4783
+ counts,
4784
+ completed_at: (/* @__PURE__ */ new Date()).toISOString(),
4785
+ policy: {
4786
+ authority: "fortemi#1092",
4787
+ mode: "terminal-purge",
4788
+ receipt_contains_content: false
4789
+ }
4790
+ };
4791
+ await tx.query(
4792
+ `INSERT INTO deletion_receipt
4793
+ (id, operation_key, tenant_id, archive_id, selector_hash, outcome, counts, completed_at, policy)
4794
+ VALUES ($1, $2, $3, $4, $5, 'completed', $6::jsonb, $7, $8::jsonb)`,
4795
+ [
4796
+ receipt.id,
4797
+ receipt.operation_key,
4798
+ receipt.tenant_id,
4799
+ receipt.archive_id,
4800
+ receipt.selector_hash,
4801
+ JSON.stringify(receipt.counts),
4802
+ receipt.completed_at,
4803
+ JSON.stringify(receipt.policy)
4804
+ ]
4805
+ );
4806
+ });
4807
+ const completedReceipt = receipt;
4808
+ if (!completedReceipt) throw new Error("Purge transaction did not produce a receipt");
4809
+ this.events?.emit("purge.completed", { receiptId: completedReceipt.id, counts: completedReceipt.counts });
4810
+ return completedReceipt;
4811
+ }
4812
+ async count(noteIds, db = this.db) {
4813
+ const counts = zeroCounts();
4814
+ if (noteIds.length === 0) return counts;
4815
+ const params = [noteIds];
4816
+ const rows = await Promise.all([
4817
+ db.query("SELECT COUNT(*) AS count FROM note WHERE id = ANY($1)", params),
4818
+ db.query("SELECT COUNT(*) AS count FROM note_revision WHERE note_id = ANY($1)", params),
4819
+ db.query("SELECT COUNT(*) AS count FROM link WHERE source_note_id = ANY($1) OR target_note_id = ANY($1)", params),
4820
+ db.query("SELECT COUNT(*) AS count FROM note_tag WHERE note_id = ANY($1)", params),
4821
+ db.query("SELECT COUNT(*) AS count FROM embedding WHERE note_id = ANY($1)", params),
4822
+ db.query("SELECT COUNT(*) AS count FROM attachment WHERE note_id = ANY($1)", params),
4823
+ db.query(
4824
+ `SELECT COUNT(*) AS count FROM attachment_blob ab
4825
+ WHERE EXISTS (SELECT 1 FROM attachment a WHERE a.blob_id = ab.id AND a.note_id = ANY($1))`,
4826
+ params
4827
+ ),
4828
+ db.query("SELECT COUNT(*) AS count FROM graph_edge_artifact WHERE from_note_id = ANY($1) OR to_note_id = ANY($1)", params),
4829
+ db.query(
4830
+ `SELECT COUNT(*) AS count FROM provenance_edge
4831
+ WHERE (entity_type = 'note' AND entity_id = ANY($1))
4832
+ OR (attributes ->> 'note_id') = ANY($1)`,
4833
+ params
4834
+ ),
4835
+ db.query("SELECT COUNT(*) AS count FROM source_identity WHERE note_id = ANY($1)", params)
4836
+ ]);
4837
+ const values = rows.map((row) => Number.parseInt(row.rows[0]?.count ?? "0", 10));
4838
+ [
4839
+ counts.notes,
4840
+ counts.revisions,
4841
+ counts.links,
4842
+ counts.tags,
4843
+ counts.embeddings,
4844
+ counts.attachments,
4845
+ counts.blobs,
4846
+ counts.graph_edges,
4847
+ counts.provenance_edges,
4848
+ counts.source_identities
4849
+ ] = values;
4850
+ return counts;
4851
+ }
4852
+ async deleteSelected(tx, noteIds) {
4853
+ if (noteIds.length === 0) return;
4854
+ const params = [noteIds];
4855
+ await tx.query("DELETE FROM community_assignment WHERE note_id = ANY($1)", params);
4856
+ await tx.query("DELETE FROM graph_edge_artifact WHERE from_note_id = ANY($1) OR to_note_id = ANY($1)", params);
4857
+ await tx.query("DELETE FROM embedding_set_member WHERE note_id = ANY($1)", params);
4858
+ await tx.query("DELETE FROM embedding WHERE note_id = ANY($1)", params);
4859
+ await tx.query("DELETE FROM attachment_embedding WHERE attachment_id IN (SELECT id FROM attachment WHERE note_id = ANY($1))", params);
4860
+ await tx.query("DELETE FROM attachment WHERE note_id = ANY($1)", params);
4861
+ await tx.query(
4862
+ `DELETE FROM attachment_blob ab
4863
+ WHERE NOT EXISTS (SELECT 1 FROM attachment a WHERE a.blob_id = ab.id)`
4864
+ );
4865
+ await tx.query("DELETE FROM source_identity WHERE note_id = ANY($1)", params);
4866
+ 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"]);
4867
+ await tx.query("DELETE FROM job_queue WHERE note_id = ANY($1)", params);
4868
+ await tx.query("DELETE FROM collection_note WHERE note_id = ANY($1)", params);
4869
+ await tx.query("DELETE FROM note_tag WHERE note_id = ANY($1)", params);
4870
+ await tx.query("DELETE FROM link WHERE source_note_id = ANY($1) OR target_note_id = ANY($1)", params);
4871
+ await tx.query("DELETE FROM note_revision WHERE note_id = ANY($1)", params);
4872
+ await tx.query("DELETE FROM note_revised_current WHERE note_id = ANY($1)", params);
4873
+ await tx.query("DELETE FROM note_original WHERE note_id = ANY($1)", params);
4874
+ await tx.query("DELETE FROM shard_field_presence WHERE component = $2 AND record_id = ANY($1)", [noteIds, "notes"]);
4875
+ await tx.query("DELETE FROM note WHERE id = ANY($1)", params);
4876
+ }
4877
+ };
4878
+
4162
4879
  // src/repositories/graph-repository.ts
4163
4880
  var SIMILARITY_GRAPH_ALGORITHM = "knn-batched-v1";
4164
4881
  var DEFAULT_GRAPH_BATCH_SIZE = 64;
@@ -6958,11 +7675,11 @@ var FallbackRouter = class {
6958
7675
  // -------------------------------------------------------------------------
6959
7676
  /** Get providers not currently in cooldown */
6960
7677
  getAvailableProviders() {
6961
- const now = Date.now();
7678
+ const now2 = Date.now();
6962
7679
  return this.providers.filter((p) => {
6963
7680
  const cd = this.cooldownMap.get(p.id);
6964
7681
  if (!cd) return true;
6965
- if (now >= cd.expiresAt) {
7682
+ if (now2 >= cd.expiresAt) {
6966
7683
  this.cooldownMap.delete(p.id);
6967
7684
  return true;
6968
7685
  }
@@ -6971,10 +7688,10 @@ var FallbackRouter = class {
6971
7688
  }
6972
7689
  /** Get providers in cooldown with their expiry info */
6973
7690
  getCoolingDown() {
6974
- const now = Date.now();
7691
+ const now2 = Date.now();
6975
7692
  const result = [];
6976
7693
  for (const [id, entry] of this.cooldownMap) {
6977
- if (now < entry.expiresAt) {
7694
+ if (now2 < entry.expiresAt) {
6978
7695
  result.push({ providerId: id, category: entry.category, expiresAt: entry.expiresAt });
6979
7696
  }
6980
7697
  }
@@ -27959,13 +28676,13 @@ async function liveRepresentationLosses(db) {
27959
28676
  const result = await db.query(
27960
28677
  `SELECT COUNT(*) AS count FROM ${table} WHERE deleted_at IS NOT NULL`
27961
28678
  );
27962
- const count = Number(result.rows[0]?.count ?? 0);
27963
- if (count > 0) {
28679
+ const count2 = Number(result.rows[0]?.count ?? 0);
28680
+ if (count2 > 0) {
27964
28681
  losses.push({
27965
28682
  code: "unrepresentable-live-tombstone",
27966
28683
  component,
27967
- count,
27968
- message: `${count} ${table} tombstone(s) have no full-v1 wire field`,
28684
+ count: count2,
28685
+ message: `${count2} ${table} tombstone(s) have no full-v1 wire field`,
27969
28686
  action: "reject",
27970
28687
  reason: "full-v1-live-production"
27971
28688
  });
@@ -27997,15 +28714,15 @@ async function liveRepresentationLosses(db) {
27997
28714
  for (const row of vectorDimensions.rows) {
27998
28715
  const dimension = Number(row.dimension);
27999
28716
  if (dimension === 768) continue;
28000
- const count = Number(row.count);
28717
+ const count2 = Number(row.count);
28001
28718
  losses.push({
28002
28719
  code: "unrepresentable-live-embedding-dimension",
28003
28720
  component: "embeddings",
28004
- count,
28721
+ count: count2,
28005
28722
  field_path: "/vector",
28006
28723
  source_state: "value",
28007
28724
  destination_capability: "full-v1 requires exactly 768 vector dimensions",
28008
- message: `${count} embedding vector(s) have ${dimension} dimensions`,
28725
+ message: `${count2} embedding vector(s) have ${dimension} dimensions`,
28009
28726
  action: "reject",
28010
28727
  reason: "full-v1-live-production"
28011
28728
  });
@@ -28565,16 +29282,27 @@ async function collectCoreV1Losses(db, options) {
28565
29282
  { component: "community_assignments", table: "community_assignment" }
28566
29283
  ];
28567
29284
  for (const { component, table } of componentCounts) {
28568
- const count = await rowCount(db, `SELECT COUNT(*) AS count FROM ${table}`);
28569
- if (count > 0) {
29285
+ const count2 = await rowCount(db, `SELECT COUNT(*) AS count FROM ${table}`);
29286
+ if (count2 > 0) {
28570
29287
  losses.push({
28571
29288
  code: "component-outside-profile",
28572
29289
  component,
28573
- count,
28574
- message: `${count} ${component} record(s) are outside core-v1 and were omitted.`
29290
+ count: count2,
29291
+ message: `${count2} ${component} record(s) are outside core-v1 and were omitted.`
28575
29292
  });
28576
29293
  }
28577
29294
  }
29295
+ const sourceIdentities = await rowCount(db, "SELECT COUNT(*) AS count FROM source_identity");
29296
+ if (sourceIdentities > 0) {
29297
+ losses.push({
29298
+ code: "source-identity-outside-profile",
29299
+ count: sourceIdentities,
29300
+ field_path: "source_identity",
29301
+ action: "omit",
29302
+ destination_capability: "core-v1 does not declare source-addressed identity mappings",
29303
+ message: `${sourceIdentities} source identity mapping(s) are outside core-v1 and were omitted.`
29304
+ });
29305
+ }
28578
29306
  const nullRevisions = await rowCount(
28579
29307
  db,
28580
29308
  `SELECT COUNT(*) AS count
@@ -30718,13 +31446,13 @@ function noteSearchText(note) {
30718
31446
  }
30719
31447
  function countOccurrences(haystack, needle) {
30720
31448
  if (!needle) return 0;
30721
- let count = 0;
31449
+ let count2 = 0;
30722
31450
  let index = haystack.indexOf(needle);
30723
31451
  while (index !== -1) {
30724
- count += 1;
31452
+ count2 += 1;
30725
31453
  index = haystack.indexOf(needle, index + needle.length);
30726
31454
  }
30727
- return count;
31455
+ return count2;
30728
31456
  }
30729
31457
  function noteMatchesTokens(note, tokens) {
30730
31458
  if (tokens.length === 0) return true;
@@ -31202,7 +31930,7 @@ function hasPositiveInteger(value) {
31202
31930
  }
31203
31931
  function isFacetCounts(value) {
31204
31932
  if (!value || typeof value !== "object" || Array.isArray(value)) return false;
31205
- return Object.values(value).every((counts) => !!counts && typeof counts === "object" && !Array.isArray(counts) && Object.values(counts).every((count) => hasNonNegativeInteger(count)));
31933
+ return Object.values(value).every((counts) => !!counts && typeof counts === "object" && !Array.isArray(counts) && Object.values(counts).every((count2) => hasNonNegativeInteger(count2)));
31206
31934
  }
31207
31935
  function isPlainRecord(value) {
31208
31936
  return !!value && typeof value === "object" && !Array.isArray(value);
@@ -34797,13 +35525,20 @@ var RECORD_COLLECTIONS = [
34797
35525
  "collection_note",
34798
35526
  "attachment",
34799
35527
  "attachment_blob",
34800
- "shard_manifest"
35528
+ "shard_manifest",
35529
+ "source_identity",
35530
+ "source_import_run",
35531
+ "deletion_receipt"
34801
35532
  ];
34802
35533
  var RECORD_STORE_CAPABILITIES = {
34803
35534
  crud: true,
34804
35535
  journal: true,
34805
35536
  atomicBatch: true,
34806
35537
  boundedTextScan: true,
35538
+ sourceAddressedUpsert: true,
35539
+ deletionReceipts: true,
35540
+ typedMetadataPredicates: false,
35541
+ evidenceLocators: true,
34807
35542
  fullTextSearch: false,
34808
35543
  vectorSearch: false,
34809
35544
  sqlJoins: false
@@ -35992,7 +36727,7 @@ async function buildRecordShardArchive(store, options, profile) {
35992
36727
  (attachment) => exportedNoteIds.has(attachment.note_id)
35993
36728
  );
35994
36729
  const projectedAttachmentCount = browserNotes.reduce(
35995
- (count, note) => count + (note.attachments?.length ?? 0),
36730
+ (count2, note) => count2 + (note.attachments?.length ?? 0),
35996
36731
  0
35997
36732
  );
35998
36733
  const sourceNoteById = new Map(notes.map((note) => [note.id, note]));
@@ -36146,6 +36881,17 @@ async function buildRecordShardArchive(store, options, profile) {
36146
36881
  message: `${collectionStateLosses} collection lifecycle state(s) are outside record-v1 and were omitted.`
36147
36882
  });
36148
36883
  }
36884
+ const sourceIdentities = await store.list("source_identity");
36885
+ if (sourceIdentities.length > 0) {
36886
+ losses.push({
36887
+ code: "source-identity-outside-profile",
36888
+ count: sourceIdentities.length,
36889
+ field_path: "source_identity",
36890
+ action: "omit",
36891
+ destination_capability: "record-v1 does not declare source-addressed identity mappings",
36892
+ message: `${sourceIdentities.length} source identity mapping(s) are outside record-v1 and were omitted.`
36893
+ });
36894
+ }
36149
36895
  }
36150
36896
  let manifest = isRecordV1 ? {
36151
36897
  version: recordSchemaVersion2,
@@ -36428,9 +37174,9 @@ async function importShardToRecords(store, data, options) {
36428
37174
  }
36429
37175
  for (const component of manifest.components ?? []) {
36430
37176
  if (UNSUPPORTED_COMPONENTS.includes(component)) {
36431
- const count = manifest.counts?.[component];
37177
+ const count2 = manifest.counts?.[component];
36432
37178
  const key = component === "communities" ? "communities" : component;
36433
- skipped[key] = (skipped[key] ?? 0) + (typeof count === "number" ? count : 0);
37179
+ skipped[key] = (skipped[key] ?? 0) + (typeof count2 === "number" ? count2 : 0);
36434
37180
  warnings.push(
36435
37181
  `Shard component '${component}' is not supported by the canonical record tier and was skipped. Import into a PGlite-backed store to preserve it.`
36436
37182
  );
@@ -36830,9 +37576,302 @@ async function importShardToRecords(store, data, options) {
36830
37576
  };
36831
37577
  }
36832
37578
 
37579
+ // src/records/source-upsert.ts
37580
+ function now() {
37581
+ return (/* @__PURE__ */ new Date()).toISOString();
37582
+ }
37583
+ function contentDigest2(content) {
37584
+ return computeHash(new TextEncoder().encode(content));
37585
+ }
37586
+ function sourceHash2(source) {
37587
+ return computeHash(new TextEncoder().encode([
37588
+ source.tenant_id ?? "default",
37589
+ source.archive_id ?? "",
37590
+ source.namespace,
37591
+ source.external_id
37592
+ ].join("\0")));
37593
+ }
37594
+ function countOutcomes2(outcomes) {
37595
+ return {
37596
+ inserted: outcomes.filter((outcome) => outcome.outcome === "inserted").length,
37597
+ unchanged: outcomes.filter((outcome) => outcome.outcome === "unchanged").length,
37598
+ versioned: outcomes.filter((outcome) => outcome.outcome === "versioned").length,
37599
+ replaced: outcomes.filter((outcome) => outcome.outcome === "replaced").length,
37600
+ conflict: outcomes.filter((outcome) => outcome.outcome === "conflict").length,
37601
+ rejected: outcomes.filter((outcome) => outcome.outcome === "rejected").length
37602
+ };
37603
+ }
37604
+ async function findSource(store, source) {
37605
+ const identities = await store.list("source_identity");
37606
+ return identities.find((identity) => identity.tenant_id === (source.tenant_id ?? "default") && identity.archive_id === (source.archive_id ?? null) && identity.namespace === source.namespace && identity.external_id === source.external_id) ?? null;
37607
+ }
37608
+ async function upsertRecordStoreSources(store, items, options = {}) {
37609
+ const maxItems = options.maxItems ?? 500;
37610
+ if (items.length > maxItems) throw new Error(`Source upsert batch exceeds the ${maxItems} item bound`);
37611
+ if (!store.applyBatch) throw new Error("RecordStore source upsert requires atomic applyBatch() support");
37612
+ if (items.length === 0) {
37613
+ return {
37614
+ import_run_id: "",
37615
+ dry_run: options.dryRun === true,
37616
+ outcomes: [],
37617
+ counts: { inserted: 0, unchanged: 0, versioned: 0, replaced: 0, conflict: 0, rejected: 0 }
37618
+ };
37619
+ }
37620
+ const outcomes = [];
37621
+ const mutations = [];
37622
+ const stamp = now();
37623
+ for (const [index, item] of items.entries()) {
37624
+ const external_id_hash = sourceHash2(item.source);
37625
+ const content_digest = contentDigest2(item.content);
37626
+ const existing = await findSource(store, item.source);
37627
+ if (!existing) {
37628
+ const noteId = item.source.caller_stable_id ?? generateId();
37629
+ outcomes.push({ index, outcome: "inserted", note_id: noteId, external_id_hash, content_digest });
37630
+ if (!options.dryRun) {
37631
+ const note2 = {
37632
+ id: noteId,
37633
+ archive_id: item.source.archive_id ?? null,
37634
+ title: item.title ?? null,
37635
+ format: item.format ?? "markdown",
37636
+ source: `source:${item.source.namespace}`,
37637
+ visibility: item.visibility ?? "private",
37638
+ revision_mode: "standard",
37639
+ is_starred: false,
37640
+ is_pinned: false,
37641
+ is_archived: false,
37642
+ created_at: stamp,
37643
+ updated_at: stamp,
37644
+ deleted_at: null
37645
+ };
37646
+ const original = {
37647
+ id: generateId(),
37648
+ note_id: noteId,
37649
+ content: item.content,
37650
+ content_hash: content_digest,
37651
+ created_at: stamp
37652
+ };
37653
+ const current2 = {
37654
+ id: noteId,
37655
+ content: item.content,
37656
+ ai_metadata: item.metadata ?? null,
37657
+ generation_count: 0,
37658
+ model: null,
37659
+ is_user_edited: false,
37660
+ updated_at: stamp
37661
+ };
37662
+ const identity = {
37663
+ id: generateId(),
37664
+ tenant_id: item.source.tenant_id ?? "default",
37665
+ archive_id: item.source.archive_id ?? null,
37666
+ namespace: item.source.namespace,
37667
+ external_id: item.source.external_id,
37668
+ external_id_hash,
37669
+ source_schema_version: item.source.source_schema_version,
37670
+ content_digest,
37671
+ import_run_id: item.source.import_run_id,
37672
+ caller_stable_id: item.source.caller_stable_id ?? null,
37673
+ note_id: noteId,
37674
+ created_at: stamp,
37675
+ updated_at: stamp
37676
+ };
37677
+ mutations.push(
37678
+ { op: "put", collection: "note", record: note2 },
37679
+ { op: "put", collection: "note_original", record: original },
37680
+ { op: "put", collection: "note_revised_current", record: current2 },
37681
+ { op: "put", collection: "source_identity", record: identity }
37682
+ );
37683
+ }
37684
+ continue;
37685
+ }
37686
+ if (existing.content_digest === content_digest) {
37687
+ outcomes.push({ index, outcome: "unchanged", note_id: existing.note_id, external_id_hash, content_digest });
37688
+ continue;
37689
+ }
37690
+ const policy = item.policy ?? "version";
37691
+ if (policy === "conflict") {
37692
+ outcomes.push({ index, outcome: "conflict", note_id: existing.note_id, external_id_hash, content_digest });
37693
+ continue;
37694
+ }
37695
+ const note = await store.get("note", existing.note_id);
37696
+ const current = await store.get("note_revised_current", existing.note_id);
37697
+ if (!note || !current) {
37698
+ outcomes.push({ index, outcome: "rejected", note_id: existing.note_id, external_id_hash, content_digest, reason: "source identity points to a missing note" });
37699
+ continue;
37700
+ }
37701
+ const outcome = policy === "replace" ? "replaced" : "versioned";
37702
+ outcomes.push({ index, outcome, note_id: existing.note_id, external_id_hash, content_digest });
37703
+ if (!options.dryRun) {
37704
+ mutations.push(
37705
+ {
37706
+ op: "put",
37707
+ collection: "note",
37708
+ record: {
37709
+ ...note,
37710
+ title: item.title ?? null,
37711
+ archive_id: item.source.archive_id ?? null,
37712
+ format: item.format ?? "markdown",
37713
+ visibility: item.visibility ?? "private",
37714
+ deleted_at: null,
37715
+ updated_at: stamp
37716
+ }
37717
+ },
37718
+ {
37719
+ op: "put",
37720
+ collection: "note_revised_current",
37721
+ record: { ...current, content: item.content, ai_metadata: item.metadata ?? null, is_user_edited: false, updated_at: stamp }
37722
+ },
37723
+ {
37724
+ op: "put",
37725
+ collection: "source_identity",
37726
+ record: { ...existing, source_schema_version: item.source.source_schema_version, content_digest, import_run_id: item.source.import_run_id, updated_at: stamp }
37727
+ }
37728
+ );
37729
+ }
37730
+ }
37731
+ if (!options.dryRun && hasMaterialChange2(outcomes)) {
37732
+ const run = {
37733
+ id: items[0].source.import_run_id,
37734
+ tenant_id: items[0].source.tenant_id ?? "default",
37735
+ archive_id: items[0].source.archive_id ?? null,
37736
+ namespace: items[0].source.namespace,
37737
+ started_at: stamp,
37738
+ completed_at: stamp,
37739
+ checkpoint: { item_count: items.length },
37740
+ receipt: { counts: countOutcomes2(outcomes) }
37741
+ };
37742
+ mutations.push({ op: "put", collection: "source_import_run", record: run });
37743
+ await store.applyBatch(mutations);
37744
+ }
37745
+ return {
37746
+ import_run_id: items[0].source.import_run_id,
37747
+ dry_run: options.dryRun === true,
37748
+ outcomes,
37749
+ counts: countOutcomes2(outcomes)
37750
+ };
37751
+ }
37752
+ function hasMaterialChange2(outcomes) {
37753
+ return outcomes.some((outcome) => outcome.outcome === "inserted" || outcome.outcome === "versioned" || outcome.outcome === "replaced");
37754
+ }
37755
+
37756
+ // src/records/lifecycle-purge.ts
37757
+ function hashSelector(selector) {
37758
+ return computeHash(new TextEncoder().encode(JSON.stringify({
37759
+ tenant_id: selector.tenant_id ?? "default",
37760
+ archive_id: selector.archive_id ?? null,
37761
+ note_ids: [...selector.note_ids ?? []].sort(),
37762
+ source: selector.source ? {
37763
+ namespace: selector.source.namespace,
37764
+ external_id_hash: selector.source.external_id ? computeHash(new TextEncoder().encode(selector.source.external_id)) : null
37765
+ } : null
37766
+ })));
37767
+ }
37768
+ function zeroCounts2() {
37769
+ return {
37770
+ notes: 0,
37771
+ revisions: 0,
37772
+ links: 0,
37773
+ tags: 0,
37774
+ embeddings: 0,
37775
+ attachments: 0,
37776
+ blobs: 0,
37777
+ graph_edges: 0,
37778
+ provenance_edges: 0,
37779
+ source_identities: 0
37780
+ };
37781
+ }
37782
+ async function selectedNoteIds2(store, selector) {
37783
+ const notes = await store.list("note");
37784
+ if (selector.note_ids?.length) return notes.filter((note) => selector.note_ids.includes(note.id)).map((note) => note.id);
37785
+ if (!selector.source) throw new Error("Purge selector must target note_ids or source identity");
37786
+ const identities = await store.list("source_identity");
37787
+ 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);
37788
+ }
37789
+ async function count(store, noteIds) {
37790
+ const counts = zeroCounts2();
37791
+ if (noteIds.length === 0) return counts;
37792
+ const noteSet = new Set(noteIds);
37793
+ counts.notes = (await store.list("note")).filter((record) => noteSet.has(record.id)).length;
37794
+ counts.revisions = 0;
37795
+ counts.links = (await store.list("link")).filter((record) => noteSet.has(record.source_note_id) || noteSet.has(record.target_note_id)).length;
37796
+ counts.tags = (await store.list("note_tag")).filter((record) => noteSet.has(record.note_id)).length;
37797
+ counts.attachments = (await store.list("attachment")).filter((record) => noteSet.has(record.note_id)).length;
37798
+ const purgedBlobIds = new Set((await store.list("attachment")).filter((record) => noteSet.has(record.note_id)).map((record) => record.blob_id));
37799
+ counts.blobs = (await store.list("attachment_blob")).filter((record) => purgedBlobIds.has(record.id)).length;
37800
+ counts.source_identities = (await store.list("source_identity")).filter((record) => noteSet.has(record.note_id)).length;
37801
+ return counts;
37802
+ }
37803
+ async function previewRecordStorePurge(store, selector) {
37804
+ return { selector_hash: hashSelector(selector), counts: await count(store, await selectedNoteIds2(store, selector)) };
37805
+ }
37806
+ async function purgeRecordStoreGraph(store, selector, operationKey) {
37807
+ if (!store.applyBatch) throw new Error("RecordStore purge requires atomic applyBatch() support");
37808
+ const prior = (await store.list("deletion_receipt")).find((receipt2) => receipt2.operation_key === operationKey);
37809
+ if (prior) return {
37810
+ id: prior.id,
37811
+ operation_key: prior.operation_key,
37812
+ tenant_id: prior.tenant_id,
37813
+ archive_id: prior.archive_id,
37814
+ selector_hash: prior.selector_hash,
37815
+ outcome: "completed",
37816
+ counts: prior.counts,
37817
+ completed_at: prior.completed_at,
37818
+ policy: prior.policy
37819
+ };
37820
+ const noteIds = await selectedNoteIds2(store, selector);
37821
+ const noteSet = new Set(noteIds);
37822
+ const counts = await count(store, noteIds);
37823
+ const mutations = [];
37824
+ for (const collection of ["note_revised_current", "note_original", "note_tag", "collection_note", "attachment", "source_identity"]) {
37825
+ for (const record of await store.list(collection)) {
37826
+ const noteId = collection === "note_revised_current" ? record.id : "note_id" in record && typeof record.note_id === "string" ? record.note_id : null;
37827
+ if (noteId && noteSet.has(noteId)) {
37828
+ mutations.push({ op: "delete", collection, id: record.id });
37829
+ }
37830
+ }
37831
+ }
37832
+ for (const record of await store.list("link")) {
37833
+ if (noteSet.has(record.source_note_id) || noteSet.has(record.target_note_id)) mutations.push({ op: "delete", collection: "link", id: record.id });
37834
+ }
37835
+ for (const record of await store.list("note")) {
37836
+ if (noteSet.has(record.id)) mutations.push({ op: "delete", collection: "note", id: record.id });
37837
+ }
37838
+ const liveBlobIds = new Set((await store.list("attachment")).filter((record) => !noteSet.has(record.note_id)).map((record) => record.blob_id));
37839
+ for (const record of await store.list("attachment_blob")) {
37840
+ if (!liveBlobIds.has(record.id)) mutations.push({ op: "delete", collection: "attachment_blob", id: record.id });
37841
+ }
37842
+ const receipt = {
37843
+ id: generateId(),
37844
+ operation_key: operationKey,
37845
+ tenant_id: selector.tenant_id ?? "default",
37846
+ archive_id: selector.archive_id ?? null,
37847
+ selector_hash: hashSelector(selector),
37848
+ outcome: "completed",
37849
+ counts,
37850
+ completed_at: (/* @__PURE__ */ new Date()).toISOString(),
37851
+ policy: {
37852
+ authority: "fortemi#1092",
37853
+ mode: "terminal-purge",
37854
+ receipt_contains_content: false
37855
+ }
37856
+ };
37857
+ mutations.push({ op: "put", collection: "deletion_receipt", record: receipt });
37858
+ await store.applyBatch(mutations);
37859
+ return {
37860
+ id: receipt.id,
37861
+ operation_key: receipt.operation_key,
37862
+ tenant_id: receipt.tenant_id,
37863
+ archive_id: receipt.archive_id,
37864
+ selector_hash: receipt.selector_hash,
37865
+ outcome: "completed",
37866
+ counts,
37867
+ completed_at: receipt.completed_at,
37868
+ policy: receipt.policy
37869
+ };
37870
+ }
37871
+
36833
37872
  // src/index.ts
36834
- var VERSION = "2026.7.15";
37873
+ var VERSION = "2026.8.0";
36835
37874
 
36836
- export { AIWG_SCAN_REQUIRED_FIELDS, AllowlistTrustStore, ArchiveManager, AttachmentsRepository, CORE_V1_COMPONENTS, CURRENT_MIGRATION_HEAD, CURRENT_SHARD_VERSION, CanonicalAttachmentsRepository, CanonicalNotesRepository, CapabilityManager, CaptureKnowledgeInputSchema, CollectionsRepository, CommunitiesRepository, DB_SNAPSHOT_SCHEMA_VERSION, DbSnapshotVersionError, EMBED_REQUEST_KIND, EMBED_RESPONSE_KIND, EmbeddingSetsRepository, FORTEMI_COMPATIBILITY_PATH, FORTEMI_COMPATIBILITY_STATES, FORTEMI_REQUIRED_COMPATIBILITY_CAPABILITIES, FORTEMI_SERVER_COMPATIBILITY_REVISION, FallbackRouter, FortemiToolManifest, GetNoteInputSchema, GraphRepository, IdbRecordStore, JOB_CAPABILITIES, JOB_PRIORITIES, JobQueueWorker, LOCAL_ENDPOINTS, LinksRepository, ListNotesInputSchema, ManageArchiveInputSchema, ManageAttachmentsInputSchema, ManageCapabilitiesInputSchema, ManageCollectionsInputSchema, ManageLinksInputSchema, ManageNoteInputSchema, ManageTagsInputSchema, MemoryBlobStore, MemoryRecordStore, MigrationRunner, NotesRepository, OpenAICompatibleProvider, PGliteStorageBackend, PGliteStorageBackendFactory, PGliteWorkerClient, PGliteWorkerStorageBackend, PGliteWorkerStorageBackendFactory, ProvenanceRepository, ProviderRegistry, RECORD_COLLECTIONS, RECORD_SCHEMA_VERSION, RECORD_STORE_CAPABILITIES, SHARD_FORMAT, SIGNATURE_ENTRY, SIGNING_ENVELOPE_VERSION, SUPPORTED_PGLITE_VERSION, SearchInputSchema, SearchRepository, SkosRepository, TagsRepository, TransactionProxy, TypedEventBus, VERSION, aiRevisionHandler, aiwgFortemiIndexFromKnowledgeShard, aiwgFortemiIndexToCommunityGraph, aiwgFortemiIndexToKnowledgeShard, aiwgFortemiIndexToKnowledgeShardWithReport, allMigrations, appendPluginScript, assertAiwgFortemiChunkManifest, assertAiwgFortemiChunkPart, assertAiwgFortemiIndexExport, assertAiwgStaticEmbeddingSet, assertShardComponentRecord, buildAiwgChunkedIndex, buildAiwgStaticEmbeddingSet, buildNoteConditions, buildPluginCsp, captureKnowledge, chunkText, classifyError, classifyModel, clearPrefetchedShard, collectionFromShard, collectionToShard, computeBlobHash, computeHash, computeSri, conceptTaggingHandler, cosineSimilarity, createAiwgFetchChunkLoader, createAiwgFetchDetailLoader, createAiwgIndexController, createAiwgReviewDecisionExport, createBlobStore, createCosineSemanticProvider, createCspReportHandler, createFortemi, createLazyBlobStore, createLegacyProvider, createPGliteBackend, createPGliteInstance, createRecordBackend, createRecordStore, createRemoteBackend, createRoutes, createShardBackend, createShardCapabilityReport, createWorkerEmbedFunction, defaultStorageBackendFactory, detectCommunities, detectGpuCapabilities, detectInferenceCapabilities, discoverLocalProviders, dropAttachmentProjection, dropNoteProjection, dumpDbSnapshot, embeddingConfigToShard, embeddingFromShard, embeddingGenerationHandler, embeddingSetFromShard, embeddingSetMemberToShard, embeddingSetToShard, embeddingToShard, enqueueFullWorkflow, enqueueJob, enqueueNoteCreationJobs, estimateModelFit, estimateVramMB, estimateVramTier, exportShard, exportShardFromRecords, exportShardFromRecordsWithReport, exportShardWithReport, fetchAndValidateFortemiCompatibility, fetchPluginScript, filterAiwgRecordsByPrivacy, findAiwgStaticDuplicatePairs, formatFortemiCompatibilitySummary, fortemiCompatibilityUrl, fortemiManifest, fromPrefetched, generateId, getAiwgFortemiFacets, getAiwgFortemiIndexExportSchema, getEmbedFunction, getFortemiBridge, getFortemiSecretStore, getJobQueueStatus, getKnowledgeShardContractReceipt, getKnowledgeShardProfileRegistry, getKnowledgeShardSchema, getLlmFunction, getNote, getPrefetchedSha256, handleEmbedRequests, hasFortemiSecureSecrets, importShard, importShardToRecords, isPluginScriptAllowed, isShardPrefetched, isShardSigningSupported, linkFromShard, linkToShard, linkingHandler, listNotes, manageArchive, manageAttachments, manageCapabilities, manageCollections, manageLinks, manageNote, manageTags, matchRoute, migrateLegacyBlobStore, noteFromShard, noteSkosTagToShard, noteToShard, openShard, packTarGz, parseCspReport, prefetchShard, profileSupportError, projectAttachments, projectNotes, projectRecords, provenanceEdgeToShard, queryAiwgFortemiIndex, queryAiwgHybridIndex, queryAiwgSemanticIndex, registerLlmCapability, registerSemanticCapability, registerSemanticCapabilityWorker, registerServiceWorker, restoreDbSnapshot, searchTool, selectBackend, selectLlmModel, setEmbedFunction, setLlmFunction, sha256Hex, sidecarBlobDigests, signShard, skosConceptToShard, skosRelationToShard, skosSchemeToShard, suggestTags, tagsToShard, templateToShard, titleGenerationHandler, unpackTarGz, unregisterLlmCapability, unregisterSemanticCapability, urlLinkToShard, validateAiwgFortemiChunkManifest, validateAiwgFortemiChunkPart, validateAiwgFortemiIndexExport, validateAiwgFortemiIndexExportSchema, validateAiwgFortemiProjectedRecordSchema, validateAiwgStaticEmbeddingSet, validateChecksums, validateCoreV1ShardArchive, validateFortemiCompatibilityResponse, validateFullV1ShardArchive, validateRecordV1ShardArchive, validateShardArchive, validateShardComponentRecord, validateShardManifest, verifyDbSnapshotMeta, verifyShardSignature, verifySri };
37875
+ export { AIWG_SCAN_REQUIRED_FIELDS, AllowlistTrustStore, ArchiveManager, AttachmentsRepository, CORE_V1_COMPONENTS, CURRENT_MIGRATION_HEAD, CURRENT_SHARD_VERSION, CanonicalAttachmentsRepository, CanonicalNotesRepository, CapabilityManager, CaptureKnowledgeInputSchema, CollectionsRepository, CommunitiesRepository, DB_SNAPSHOT_SCHEMA_VERSION, DbSnapshotVersionError, EMBED_REQUEST_KIND, EMBED_RESPONSE_KIND, EmbeddingSetsRepository, FORTEMI_COMPATIBILITY_PATH, FORTEMI_COMPATIBILITY_STATES, FORTEMI_REQUIRED_COMPATIBILITY_CAPABILITIES, FORTEMI_SERVER_COMPATIBILITY_REVISION, FallbackRouter, FortemiToolManifest, GetNoteInputSchema, GraphRepository, IdbRecordStore, JOB_CAPABILITIES, JOB_PRIORITIES, JobQueueWorker, LOCAL_ENDPOINTS, LifecyclePurgeRepository, LinksRepository, ListNotesInputSchema, ManageArchiveInputSchema, ManageAttachmentsInputSchema, ManageCapabilitiesInputSchema, ManageCollectionsInputSchema, ManageLinksInputSchema, ManageNoteInputSchema, ManageTagsInputSchema, MemoryBlobStore, MemoryRecordStore, MigrationRunner, NotesRepository, OpenAICompatibleProvider, PGliteStorageBackend, PGliteStorageBackendFactory, PGliteWorkerClient, PGliteWorkerStorageBackend, PGliteWorkerStorageBackendFactory, ProvenanceRepository, ProviderRegistry, RECORD_COLLECTIONS, RECORD_SCHEMA_VERSION, RECORD_STORE_CAPABILITIES, REGISTERED_METADATA_PATHS, SHARD_FORMAT, SIGNATURE_ENTRY, SIGNING_ENVELOPE_VERSION, SUPPORTED_PGLITE_VERSION, SearchInputSchema, SearchRepository, SkosRepository, SourceUpsertRepository, TagsRepository, TransactionProxy, TypedEventBus, VERSION, aiRevisionHandler, aiwgFortemiIndexFromKnowledgeShard, aiwgFortemiIndexToCommunityGraph, aiwgFortemiIndexToKnowledgeShard, aiwgFortemiIndexToKnowledgeShardWithReport, allMigrations, appendPluginScript, assertAiwgFortemiChunkManifest, assertAiwgFortemiChunkPart, assertAiwgFortemiIndexExport, assertAiwgStaticEmbeddingSet, assertShardComponentRecord, buildAiwgChunkedIndex, buildAiwgStaticEmbeddingSet, buildMetadataPredicateConditions, buildNoteConditions, buildPluginCsp, captureKnowledge, chunkText, classifyError, classifyModel, clearPrefetchedShard, collectionFromShard, collectionToShard, computeBlobHash, computeHash, computeSri, conceptTaggingHandler, cosineSimilarity, createAiwgFetchChunkLoader, createAiwgFetchDetailLoader, createAiwgIndexController, createAiwgReviewDecisionExport, createBlobStore, createCosineSemanticProvider, createCspReportHandler, createFortemi, createLazyBlobStore, createLegacyProvider, createPGliteBackend, createPGliteInstance, createRecordBackend, createRecordStore, createRemoteBackend, createRoutes, createShardBackend, createShardCapabilityReport, createWorkerEmbedFunction, defaultStorageBackendFactory, detectCommunities, detectGpuCapabilities, detectInferenceCapabilities, discoverLocalProviders, dropAttachmentProjection, dropNoteProjection, dumpDbSnapshot, embeddingConfigToShard, embeddingFromShard, embeddingGenerationHandler, embeddingSetFromShard, embeddingSetMemberToShard, embeddingSetToShard, embeddingToShard, enqueueFullWorkflow, enqueueJob, enqueueNoteCreationJobs, estimateModelFit, estimateVramMB, estimateVramTier, exportShard, exportShardFromRecords, exportShardFromRecordsWithReport, exportShardWithReport, fetchAndValidateFortemiCompatibility, fetchPluginScript, filterAiwgRecordsByPrivacy, findAiwgStaticDuplicatePairs, formatFortemiCompatibilitySummary, fortemiCompatibilityUrl, fortemiManifest, fromPrefetched, generateId, getAiwgFortemiFacets, getAiwgFortemiIndexExportSchema, getEmbedFunction, getFortemiBridge, getFortemiSecretStore, getJobQueueStatus, getKnowledgeShardContractReceipt, getKnowledgeShardProfileRegistry, getKnowledgeShardSchema, getLlmFunction, getNote, getPrefetchedSha256, handleEmbedRequests, hasFortemiSecureSecrets, importShard, importShardToRecords, isPluginScriptAllowed, isShardPrefetched, isShardSigningSupported, linkFromShard, linkToShard, linkingHandler, listNotes, manageArchive, manageAttachments, manageCapabilities, manageCollections, manageLinks, manageNote, manageTags, matchRoute, migrateLegacyBlobStore, noteFromShard, noteSkosTagToShard, noteToShard, openShard, packTarGz, parseCspReport, prefetchShard, previewRecordStorePurge, profileSupportError, projectAttachments, projectNotes, projectRecords, provenanceEdgeToShard, purgeRecordStoreGraph, queryAiwgFortemiIndex, queryAiwgHybridIndex, queryAiwgSemanticIndex, registerLlmCapability, registerSemanticCapability, registerSemanticCapabilityWorker, registerServiceWorker, restoreDbSnapshot, searchTool, selectBackend, selectLlmModel, setEmbedFunction, setLlmFunction, sha256Hex, sidecarBlobDigests, signShard, skosConceptToShard, skosRelationToShard, skosSchemeToShard, suggestTags, tagsToShard, templateToShard, titleGenerationHandler, unpackTarGz, unregisterLlmCapability, unregisterSemanticCapability, upsertRecordStoreSources, urlLinkToShard, validateAiwgFortemiChunkManifest, validateAiwgFortemiChunkPart, validateAiwgFortemiIndexExport, validateAiwgFortemiIndexExportSchema, validateAiwgFortemiProjectedRecordSchema, validateAiwgStaticEmbeddingSet, validateChecksums, validateCoreV1ShardArchive, validateFortemiCompatibilityResponse, validateFullV1ShardArchive, validateRecordV1ShardArchive, validateShardArchive, validateShardComponentRecord, validateShardManifest, verifyDbSnapshotMeta, verifyShardSignature, verifySri };
36837
37876
  //# sourceMappingURL=index.js.map
36838
37877
  //# sourceMappingURL=index.js.map