@fortemi/core 2026.7.14 → 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: "",
@@ -3493,6 +3734,7 @@ function createFortemi(config) {
3493
3734
 
3494
3735
  // src/server-compatibility.ts
3495
3736
  var FORTEMI_COMPATIBILITY_PATH = "/api/v1/system/compatibility";
3737
+ var FORTEMI_SERVER_COMPATIBILITY_REVISION = "2026-07-06";
3496
3738
  var FORTEMI_COMPATIBILITY_STATES = [
3497
3739
  "available",
3498
3740
  "degraded",
@@ -3944,8 +4186,8 @@ async function migrateLegacyBlobStore(archiveName, target, indexedDbFactory) {
3944
4186
 
3945
4187
  // src/blob-store.ts
3946
4188
  var MemoryBlobStore = class {
3947
- constructor(now = Date.now) {
3948
- this.now = now;
4189
+ constructor(now2 = Date.now) {
4190
+ this.now = now2;
3949
4191
  }
3950
4192
  entries = /* @__PURE__ */ new Map();
3951
4193
  async put(bytes) {
@@ -4018,12 +4260,12 @@ function toChecksum(hex) {
4018
4260
  return CHECKSUM_PREFIX + hex;
4019
4261
  }
4020
4262
  var BytecaskBlobStore = class {
4021
- constructor(facade, adapter, index, probe, now = Date.now) {
4263
+ constructor(facade, adapter, index, probe, now2 = Date.now) {
4022
4264
  this.facade = facade;
4023
4265
  this.adapter = adapter;
4024
4266
  this.index = index;
4025
4267
  this.probe = probe;
4026
- this.now = now;
4268
+ this.now = now2;
4027
4269
  }
4028
4270
  async put(bytes) {
4029
4271
  return toChecksum(await this.facade.put(bytes));
@@ -4158,6 +4400,482 @@ function createLazyBlobStore(archiveName, options) {
4158
4400
  };
4159
4401
  }
4160
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
+
4161
4879
  // src/repositories/graph-repository.ts
4162
4880
  var SIMILARITY_GRAPH_ALGORITHM = "knn-batched-v1";
4163
4881
  var DEFAULT_GRAPH_BATCH_SIZE = 64;
@@ -6957,11 +7675,11 @@ var FallbackRouter = class {
6957
7675
  // -------------------------------------------------------------------------
6958
7676
  /** Get providers not currently in cooldown */
6959
7677
  getAvailableProviders() {
6960
- const now = Date.now();
7678
+ const now2 = Date.now();
6961
7679
  return this.providers.filter((p) => {
6962
7680
  const cd = this.cooldownMap.get(p.id);
6963
7681
  if (!cd) return true;
6964
- if (now >= cd.expiresAt) {
7682
+ if (now2 >= cd.expiresAt) {
6965
7683
  this.cooldownMap.delete(p.id);
6966
7684
  return true;
6967
7685
  }
@@ -6970,10 +7688,10 @@ var FallbackRouter = class {
6970
7688
  }
6971
7689
  /** Get providers in cooldown with their expiry info */
6972
7690
  getCoolingDown() {
6973
- const now = Date.now();
7691
+ const now2 = Date.now();
6974
7692
  const result = [];
6975
7693
  for (const [id, entry] of this.cooldownMap) {
6976
- if (now < entry.expiresAt) {
7694
+ if (now2 < entry.expiresAt) {
6977
7695
  result.push({ providerId: id, category: entry.category, expiresAt: entry.expiresAt });
6978
7696
  }
6979
7697
  }
@@ -27958,13 +28676,13 @@ async function liveRepresentationLosses(db) {
27958
28676
  const result = await db.query(
27959
28677
  `SELECT COUNT(*) AS count FROM ${table} WHERE deleted_at IS NOT NULL`
27960
28678
  );
27961
- const count = Number(result.rows[0]?.count ?? 0);
27962
- if (count > 0) {
28679
+ const count2 = Number(result.rows[0]?.count ?? 0);
28680
+ if (count2 > 0) {
27963
28681
  losses.push({
27964
28682
  code: "unrepresentable-live-tombstone",
27965
28683
  component,
27966
- count,
27967
- 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`,
27968
28686
  action: "reject",
27969
28687
  reason: "full-v1-live-production"
27970
28688
  });
@@ -27996,15 +28714,15 @@ async function liveRepresentationLosses(db) {
27996
28714
  for (const row of vectorDimensions.rows) {
27997
28715
  const dimension = Number(row.dimension);
27998
28716
  if (dimension === 768) continue;
27999
- const count = Number(row.count);
28717
+ const count2 = Number(row.count);
28000
28718
  losses.push({
28001
28719
  code: "unrepresentable-live-embedding-dimension",
28002
28720
  component: "embeddings",
28003
- count,
28721
+ count: count2,
28004
28722
  field_path: "/vector",
28005
28723
  source_state: "value",
28006
28724
  destination_capability: "full-v1 requires exactly 768 vector dimensions",
28007
- message: `${count} embedding vector(s) have ${dimension} dimensions`,
28725
+ message: `${count2} embedding vector(s) have ${dimension} dimensions`,
28008
28726
  action: "reject",
28009
28727
  reason: "full-v1-live-production"
28010
28728
  });
@@ -28564,16 +29282,27 @@ async function collectCoreV1Losses(db, options) {
28564
29282
  { component: "community_assignments", table: "community_assignment" }
28565
29283
  ];
28566
29284
  for (const { component, table } of componentCounts) {
28567
- const count = await rowCount(db, `SELECT COUNT(*) AS count FROM ${table}`);
28568
- if (count > 0) {
29285
+ const count2 = await rowCount(db, `SELECT COUNT(*) AS count FROM ${table}`);
29286
+ if (count2 > 0) {
28569
29287
  losses.push({
28570
29288
  code: "component-outside-profile",
28571
29289
  component,
28572
- count,
28573
- 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.`
28574
29292
  });
28575
29293
  }
28576
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
+ }
28577
29306
  const nullRevisions = await rowCount(
28578
29307
  db,
28579
29308
  `SELECT COUNT(*) AS count
@@ -30717,13 +31446,13 @@ function noteSearchText(note) {
30717
31446
  }
30718
31447
  function countOccurrences(haystack, needle) {
30719
31448
  if (!needle) return 0;
30720
- let count = 0;
31449
+ let count2 = 0;
30721
31450
  let index = haystack.indexOf(needle);
30722
31451
  while (index !== -1) {
30723
- count += 1;
31452
+ count2 += 1;
30724
31453
  index = haystack.indexOf(needle, index + needle.length);
30725
31454
  }
30726
- return count;
31455
+ return count2;
30727
31456
  }
30728
31457
  function noteMatchesTokens(note, tokens) {
30729
31458
  if (tokens.length === 0) return true;
@@ -31201,7 +31930,7 @@ function hasPositiveInteger(value) {
31201
31930
  }
31202
31931
  function isFacetCounts(value) {
31203
31932
  if (!value || typeof value !== "object" || Array.isArray(value)) return false;
31204
- 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)));
31205
31934
  }
31206
31935
  function isPlainRecord(value) {
31207
31936
  return !!value && typeof value === "object" && !Array.isArray(value);
@@ -34796,13 +35525,20 @@ var RECORD_COLLECTIONS = [
34796
35525
  "collection_note",
34797
35526
  "attachment",
34798
35527
  "attachment_blob",
34799
- "shard_manifest"
35528
+ "shard_manifest",
35529
+ "source_identity",
35530
+ "source_import_run",
35531
+ "deletion_receipt"
34800
35532
  ];
34801
35533
  var RECORD_STORE_CAPABILITIES = {
34802
35534
  crud: true,
34803
35535
  journal: true,
34804
35536
  atomicBatch: true,
34805
35537
  boundedTextScan: true,
35538
+ sourceAddressedUpsert: true,
35539
+ deletionReceipts: true,
35540
+ typedMetadataPredicates: false,
35541
+ evidenceLocators: true,
34806
35542
  fullTextSearch: false,
34807
35543
  vectorSearch: false,
34808
35544
  sqlJoins: false
@@ -35991,7 +36727,7 @@ async function buildRecordShardArchive(store, options, profile) {
35991
36727
  (attachment) => exportedNoteIds.has(attachment.note_id)
35992
36728
  );
35993
36729
  const projectedAttachmentCount = browserNotes.reduce(
35994
- (count, note) => count + (note.attachments?.length ?? 0),
36730
+ (count2, note) => count2 + (note.attachments?.length ?? 0),
35995
36731
  0
35996
36732
  );
35997
36733
  const sourceNoteById = new Map(notes.map((note) => [note.id, note]));
@@ -36145,6 +36881,17 @@ async function buildRecordShardArchive(store, options, profile) {
36145
36881
  message: `${collectionStateLosses} collection lifecycle state(s) are outside record-v1 and were omitted.`
36146
36882
  });
36147
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
+ }
36148
36895
  }
36149
36896
  let manifest = isRecordV1 ? {
36150
36897
  version: recordSchemaVersion2,
@@ -36427,9 +37174,9 @@ async function importShardToRecords(store, data, options) {
36427
37174
  }
36428
37175
  for (const component of manifest.components ?? []) {
36429
37176
  if (UNSUPPORTED_COMPONENTS.includes(component)) {
36430
- const count = manifest.counts?.[component];
37177
+ const count2 = manifest.counts?.[component];
36431
37178
  const key = component === "communities" ? "communities" : component;
36432
- skipped[key] = (skipped[key] ?? 0) + (typeof count === "number" ? count : 0);
37179
+ skipped[key] = (skipped[key] ?? 0) + (typeof count2 === "number" ? count2 : 0);
36433
37180
  warnings.push(
36434
37181
  `Shard component '${component}' is not supported by the canonical record tier and was skipped. Import into a PGlite-backed store to preserve it.`
36435
37182
  );
@@ -36829,9 +37576,302 @@ async function importShardToRecords(store, data, options) {
36829
37576
  };
36830
37577
  }
36831
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
+
36832
37872
  // src/index.ts
36833
- var VERSION = "2026.7.14";
37873
+ var VERSION = "2026.8.0";
36834
37874
 
36835
- 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, 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 };
36836
37876
  //# sourceMappingURL=index.js.map
36837
37877
  //# sourceMappingURL=index.js.map