@fortemi/core 2026.7.1 → 2026.7.3

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/README.md CHANGED
@@ -227,17 +227,31 @@ scan projection.
227
227
  Static semantic search uses an optional sidecar contract:
228
228
 
229
229
  ```ts
230
- import { queryAiwgHybridIndex } from '@fortemi/core/aiwg-index'
230
+ import { buildAiwgStaticEmbeddingSet, queryAiwgHybridIndex } from '@fortemi/core/aiwg-index'
231
231
 
232
232
  const embeddingSet = await fetch('/search/aiwg-index/embeddings.json').then((res) => res.json())
233
233
  const queryEmbedding = await hostEmbed('workspace health check')
234
234
  const semanticResults = queryAiwgHybridIndex(index, embeddingSet, 'health check', queryEmbedding)
235
+
236
+ // Node/CLI generation path. Supply any Node-safe model backend; @fortemi/core
237
+ // only writes the shared embedding-set graph format and does not require DOM or WebGL.
238
+ const cliEmbeddingSet = await buildAiwgStaticEmbeddingSet(index, {
239
+ id: 'aiwg-cli-embeddings',
240
+ backend: {
241
+ model: 'local-node-embedder',
242
+ dimensions: 384,
243
+ embed: async (input) => nodeEmbed(input),
244
+ },
245
+ })
235
246
  ```
236
247
 
237
248
  `aiwg.fortemi.embedding.set.v1` records the model, dimensions, granularity
238
249
  (`title-summary`, `body`, `chunked-body`, or a project value), source record IDs,
239
250
  and per-vector input hashes. Hosts provide query embeddings; `@fortemi/core` does
240
- not add a model runtime dependency for static AIWG search.
251
+ not add a model runtime dependency for static AIWG search. The builder uses the
252
+ same record text projection as static search, including attachment
253
+ `extracted_text`, and records only attachment metadata references, never raw blob
254
+ bytes.
241
255
 
242
256
  ## What You Get
243
257
 
@@ -1,4 +1,4 @@
1
- type AiwgFortemiKnownRecordType = 'crm.contact' | 'crm.organization' | 'crm.event' | 'crm.interaction' | 'aiwg.artifact' | 'docs.page';
1
+ type AiwgFortemiKnownRecordType = 'crm.contact' | 'crm.organization' | 'crm.event' | 'crm.interaction' | 'aiwg.artifact' | 'aiwg.kb.page';
2
2
  type AiwgFortemiRecordType = AiwgFortemiKnownRecordType | `aiwg.${string}` | `research.${string}` | `docs.${string}` | string;
3
3
  type AiwgFortemiRecordSchemaVersion = 'aiwg.fortemi.index.record.v1' | 'aiwg.fortemi.index.record.v2';
4
4
  type AiwgFortemiIndexExportSchemaVersion = 'aiwg.fortemi.index.export.v1' | 'aiwg.fortemi.index.export.v2';
@@ -94,6 +94,17 @@ interface AiwgFortemiRecordEmbedding {
94
94
  source_path?: string;
95
95
  metadata?: Record<string, unknown>;
96
96
  }
97
+ interface AiwgFortemiAttachmentReference {
98
+ id: string;
99
+ path: string;
100
+ mime: string | null;
101
+ checksum: string;
102
+ bytes: number;
103
+ }
104
+ interface AiwgFortemiBinarySource {
105
+ extracted_text: string;
106
+ attachment: AiwgFortemiAttachmentReference;
107
+ }
97
108
  interface AiwgFortemiRecord {
98
109
  schema_version: AiwgFortemiRecordSchemaVersion;
99
110
  id: string;
@@ -108,6 +119,7 @@ interface AiwgFortemiRecord {
108
119
  provenance: AiwgFortemiProvenance[];
109
120
  search?: AiwgFortemiSearchProjection;
110
121
  chunks?: AiwgFortemiChunk[];
122
+ binary_sources?: AiwgFortemiBinarySource[];
111
123
  embeddings?: AiwgFortemiRecordEmbedding[];
112
124
  compatibility?: Record<string, unknown>;
113
125
  /** Optional rich SKOS metadata for static consumers that need labels/definitions without opening a shard. */
@@ -129,10 +141,7 @@ interface AiwgFortemiIndexExport {
129
141
  source: {
130
142
  repo: string;
131
143
  privacy: AiwgPrivacyClassification;
132
- origin?: string;
133
- generated?: boolean;
134
- checksum?: string;
135
- updated_at?: string;
144
+ graph?: Record<string, unknown>;
136
145
  };
137
146
  items: AiwgFortemiRecord[];
138
147
  compatibility?: Record<string, unknown>;
@@ -153,6 +162,7 @@ interface AiwgFortemiChunkManifest {
153
162
  schema_version: 'aiwg.fortemi.index.chunk-manifest.v1';
154
163
  generated_at: string;
155
164
  source: AiwgFortemiIndexExport['source'];
165
+ source_export_schema_version?: AiwgFortemiIndexExportSchemaVersion;
156
166
  total: number;
157
167
  part_size: number;
158
168
  facets?: Record<string, Record<string, number>>;
@@ -321,6 +331,30 @@ interface AiwgStaticEmbeddingSet {
321
331
  input_hash_algorithm?: string;
322
332
  embeddings: AiwgStaticEmbeddingRecord[];
323
333
  }
334
+ interface AiwgHeadlessEmbeddingBackend {
335
+ model: string;
336
+ dimensions: number;
337
+ embed(input: string, record: AiwgFortemiRecord): number[] | Promise<number[]>;
338
+ }
339
+ interface AiwgPrivacyFilterOptions {
340
+ /** Include records classified `private` (default false). */
341
+ includePrivate?: boolean;
342
+ /** Include records flagged `pii` (default false). */
343
+ includePii?: boolean;
344
+ }
345
+ /** Drop `private`/`pii` records unless explicitly opted in (SEC6, default-safe). */
346
+ declare function filterAiwgRecordsByPrivacy(records: AiwgFortemiRecord[], options?: AiwgPrivacyFilterOptions): AiwgFortemiRecord[];
347
+ interface BuildAiwgStaticEmbeddingSetOptions {
348
+ id: string;
349
+ backend: AiwgHeadlessEmbeddingBackend;
350
+ records?: AiwgFortemiRecord[];
351
+ generatedAt?: string | Date;
352
+ granularity?: AiwgStaticEmbeddingSet['granularity'];
353
+ metric?: AiwgStaticEmbeddingSet['metric'];
354
+ textForRecord?: (record: AiwgFortemiRecord) => string;
355
+ /** Privacy filtering (SEC6). Default-safe: excludes `private`/`pii` records. */
356
+ privacy?: AiwgPrivacyFilterOptions;
357
+ }
324
358
  interface AiwgStaticSemanticQueryOptions {
325
359
  limit?: number;
326
360
  offset?: number;
@@ -385,17 +419,21 @@ declare function validateAiwgFortemiChunkManifest(value: unknown): AiwgChunkedIn
385
419
  declare function assertAiwgFortemiChunkManifest(value: unknown): AiwgFortemiChunkManifest;
386
420
  declare function validateAiwgFortemiChunkPart(value: unknown, partRef?: AiwgFortemiChunkPartRef, manifest?: AiwgFortemiChunkManifest): AiwgChunkedIndexValidationResult;
387
421
  declare function assertAiwgFortemiChunkPart(value: unknown, partRef?: AiwgFortemiChunkPartRef, manifest?: AiwgFortemiChunkManifest): AiwgFortemiChunkPart;
422
+ declare function resolveAiwgFetchUrl(href: string, baseUrl?: string | URL): string;
388
423
  declare function createAiwgFetchChunkLoader(baseUrl?: string | URL): AiwgChunkedIndexLoader;
389
424
  declare function encodeAiwgDetailId(id: string, encoding?: AiwgDetailIdEncoding): string;
390
425
  declare function aiwgDetailHrefForId(detail: AiwgFortemiChunkDetailRef, id: string): string;
391
426
  declare function createAiwgFetchDetailLoader(baseUrl?: string | URL): AiwgChunkedIndexDetailLoader;
392
427
  declare function getAiwgFortemiFacets(items: AiwgFortemiRecord[]): Record<string, Record<string, number>>;
428
+ declare function buildAiwgStaticEmbeddingSet(index: AiwgFortemiIndexExport, options: BuildAiwgStaticEmbeddingSetOptions): Promise<AiwgStaticEmbeddingSet>;
393
429
  interface AiwgChunkedIndexBuildOptions {
394
430
  partSize?: number;
395
431
  projection?: Array<keyof AiwgFortemiRecord>;
396
432
  detailHref?: string;
397
433
  idEncoding?: AiwgDetailIdEncoding;
398
434
  generatedAt?: string;
435
+ /** Privacy filtering (SEC6). Default-safe: excludes `private`/`pii` records. */
436
+ privacy?: AiwgPrivacyFilterOptions;
399
437
  }
400
438
  interface AiwgChunkedIndexBuildResult {
401
439
  manifest: AiwgFortemiChunkManifest;
@@ -415,7 +453,10 @@ declare function validateAiwgStaticEmbeddingSet(value: unknown): AiwgChunkedInde
415
453
  declare function assertAiwgStaticEmbeddingSet(value: unknown): AiwgStaticEmbeddingSet;
416
454
  declare function queryAiwgSemanticIndex(index: AiwgFortemiIndexExport, embeddingSet: AiwgStaticEmbeddingSet, queryEmbedding: number[], options?: AiwgStaticSemanticQueryOptions): AiwgStaticSemanticResult[];
417
455
  declare function queryAiwgHybridIndex(index: AiwgFortemiIndexExport, embeddingSet: AiwgStaticEmbeddingSet, query: string, queryEmbedding: number[], options?: AiwgStaticHybridQueryOptions): AiwgStaticSemanticResult[];
418
- declare function findAiwgStaticDuplicatePairs(index: AiwgFortemiIndexExport, embeddingSet: AiwgStaticEmbeddingSet, threshold?: number): AiwgStaticDuplicatePair[];
456
+ declare const DEFAULT_AIWG_DUPLICATE_SCAN_MAX_EMBEDDINGS = 5000;
457
+ declare function findAiwgStaticDuplicatePairs(index: AiwgFortemiIndexExport, embeddingSet: AiwgStaticEmbeddingSet, threshold?: number, options?: {
458
+ maxEmbeddings?: number;
459
+ }): AiwgStaticDuplicatePair[];
419
460
  declare function createAiwgReviewDecisionExport(source: Pick<AiwgFortemiIndexExport, 'schema_version'>, decisions: AiwgReviewDecision[], generatedAt?: string): AiwgReviewDecisionExport;
420
461
  declare function createAiwgIndexController(initialIndex?: AiwgFortemiIndexExport): AiwgIndexController;
421
462
  declare function aiwgFortemiIndexToCommunityGraph(index: AiwgFortemiIndexExport, options?: AiwgIndexGraphOptions): {
@@ -434,4 +475,4 @@ declare function aiwgFortemiIndexToCommunityGraph(index: AiwgFortemiIndexExport,
434
475
  }[];
435
476
  };
436
477
 
437
- export { AIWG_SCAN_REQUIRED_FIELDS, type AiwgChunkedIndexBuildOptions, type AiwgChunkedIndexBuildResult, type AiwgChunkedIndexDetailLoader, type AiwgChunkedIndexLoadOptions, type AiwgChunkedIndexLoader, type AiwgChunkedIndexProgress, type AiwgChunkedIndexProgressPhase, type AiwgChunkedIndexQueryOptions, type AiwgChunkedIndexQueryResult, type AiwgChunkedIndexValidationResult, type AiwgDetailIdEncoding, type AiwgFortemiChunk, type AiwgFortemiChunkDetailRef, type AiwgFortemiChunkManifest, type AiwgFortemiChunkPart, type AiwgFortemiChunkPartRef, type AiwgFortemiIndexExport, type AiwgFortemiIndexExportSchemaVersion, type AiwgFortemiKnownRecordType, type AiwgFortemiProjectedRecord, type AiwgFortemiProvenance, type AiwgFortemiProvenanceEvent, type AiwgFortemiRecord, type AiwgFortemiRecordEmbedding, type AiwgFortemiRecordSchemaVersion, type AiwgFortemiRecordSource, type AiwgFortemiRecordType, type AiwgFortemiRelationship, type AiwgFortemiRelationshipDirection, type AiwgFortemiSearchProjection, type AiwgFortemiSkosConcept, type AiwgFortemiSkosRelation, type AiwgFortemiSkosRelationType, type AiwgIndexController, type AiwgIndexControllerListener, type AiwgIndexControllerSnapshot, type AiwgIndexGraphOptions, type AiwgIndexQueryMatch, type AiwgIndexQueryOptions, type AiwgIndexQueryRankedItem, type AiwgIndexQueryResult, type AiwgIndexQueryWeights, type AiwgIndexValidationResult, type AiwgPrivacyClassification, type AiwgProvenanceConfidence, type AiwgRelationshipDirection, type AiwgRelationshipEdgeSummary, type AiwgRelationshipNodeSummary, type AiwgRelationshipQueryOptions, type AiwgRelationshipSetOperation, type AiwgRelationshipSetOptions, type AiwgRelationshipSetResult, type AiwgRelationshipTraversalOptions, type AiwgRelationshipTraversalResult, type AiwgReviewAction, type AiwgReviewDecision, type AiwgReviewDecisionExport, type AiwgReviewInput, type AiwgStaticDuplicatePair, type AiwgStaticEmbeddingRecord, type AiwgStaticEmbeddingSet, type AiwgStaticHybridQueryOptions, type AiwgStaticSemanticQueryOptions, type AiwgStaticSemanticResult, aiwgDetailHrefForId, aiwgFortemiIndexToCommunityGraph, assertAiwgFortemiChunkManifest, assertAiwgFortemiChunkPart, assertAiwgFortemiIndexExport, assertAiwgStaticEmbeddingSet, buildAiwgChunkedIndex, createAiwgFetchChunkLoader, createAiwgFetchDetailLoader, createAiwgIndexController, createAiwgReviewDecisionExport, encodeAiwgDetailId, findAiwgStaticDuplicatePairs, getAiwgFortemiFacets, queryAiwgFortemiIndex, queryAiwgHybridIndex, queryAiwgSemanticIndex, validateAiwgFortemiChunkManifest, validateAiwgFortemiChunkPart, validateAiwgFortemiIndexExport, validateAiwgStaticEmbeddingSet };
478
+ export { AIWG_SCAN_REQUIRED_FIELDS, type AiwgChunkedIndexBuildOptions, type AiwgChunkedIndexBuildResult, type AiwgChunkedIndexDetailLoader, type AiwgChunkedIndexLoadOptions, type AiwgChunkedIndexLoader, type AiwgChunkedIndexProgress, type AiwgChunkedIndexProgressPhase, type AiwgChunkedIndexQueryOptions, type AiwgChunkedIndexQueryResult, type AiwgChunkedIndexValidationResult, type AiwgDetailIdEncoding, type AiwgFortemiAttachmentReference, type AiwgFortemiBinarySource, type AiwgFortemiChunk, type AiwgFortemiChunkDetailRef, type AiwgFortemiChunkManifest, type AiwgFortemiChunkPart, type AiwgFortemiChunkPartRef, type AiwgFortemiIndexExport, type AiwgFortemiIndexExportSchemaVersion, type AiwgFortemiKnownRecordType, type AiwgFortemiProjectedRecord, type AiwgFortemiProvenance, type AiwgFortemiProvenanceEvent, type AiwgFortemiRecord, type AiwgFortemiRecordEmbedding, type AiwgFortemiRecordSchemaVersion, type AiwgFortemiRecordSource, type AiwgFortemiRecordType, type AiwgFortemiRelationship, type AiwgFortemiRelationshipDirection, type AiwgFortemiSearchProjection, type AiwgFortemiSkosConcept, type AiwgFortemiSkosRelation, type AiwgFortemiSkosRelationType, type AiwgHeadlessEmbeddingBackend, type AiwgIndexController, type AiwgIndexControllerListener, type AiwgIndexControllerSnapshot, type AiwgIndexGraphOptions, type AiwgIndexQueryMatch, type AiwgIndexQueryOptions, type AiwgIndexQueryRankedItem, type AiwgIndexQueryResult, type AiwgIndexQueryWeights, type AiwgIndexValidationResult, type AiwgPrivacyClassification, type AiwgPrivacyFilterOptions, type AiwgProvenanceConfidence, type AiwgRelationshipDirection, type AiwgRelationshipEdgeSummary, type AiwgRelationshipNodeSummary, type AiwgRelationshipQueryOptions, type AiwgRelationshipSetOperation, type AiwgRelationshipSetOptions, type AiwgRelationshipSetResult, type AiwgRelationshipTraversalOptions, type AiwgRelationshipTraversalResult, type AiwgReviewAction, type AiwgReviewDecision, type AiwgReviewDecisionExport, type AiwgReviewInput, type AiwgStaticDuplicatePair, type AiwgStaticEmbeddingRecord, type AiwgStaticEmbeddingSet, type AiwgStaticHybridQueryOptions, type AiwgStaticSemanticQueryOptions, type AiwgStaticSemanticResult, type BuildAiwgStaticEmbeddingSetOptions, DEFAULT_AIWG_DUPLICATE_SCAN_MAX_EMBEDDINGS, aiwgDetailHrefForId, aiwgFortemiIndexToCommunityGraph, assertAiwgFortemiChunkManifest, assertAiwgFortemiChunkPart, assertAiwgFortemiIndexExport, assertAiwgStaticEmbeddingSet, buildAiwgChunkedIndex, buildAiwgStaticEmbeddingSet, createAiwgFetchChunkLoader, createAiwgFetchDetailLoader, createAiwgIndexController, createAiwgReviewDecisionExport, encodeAiwgDetailId, filterAiwgRecordsByPrivacy, findAiwgStaticDuplicatePairs, getAiwgFortemiFacets, queryAiwgFortemiIndex, queryAiwgHybridIndex, queryAiwgSemanticIndex, resolveAiwgFetchUrl, validateAiwgFortemiChunkManifest, validateAiwgFortemiChunkPart, validateAiwgFortemiIndexExport, validateAiwgStaticEmbeddingSet };
@@ -1,3 +1,12 @@
1
+ import { sha256 } from '@noble/hashes/sha256';
2
+ import { bytesToHex } from '@noble/hashes/utils';
3
+
4
+ // src/hash.ts
5
+ function computeHash(data) {
6
+ const digest = sha256(data);
7
+ return `sha256:${bytesToHex(digest)}`;
8
+ }
9
+
1
10
  // src/aiwg-index.ts
2
11
  var AIWG_SCAN_REQUIRED_FIELDS = [
3
12
  "schema_version",
@@ -10,6 +19,16 @@ var AIWG_SCAN_REQUIRED_FIELDS = [
10
19
  "concepts",
11
20
  "privacy"
12
21
  ];
22
+ function isPrivacyExcluded(record, options) {
23
+ const privacy = record.privacy;
24
+ if (!privacy) return false;
25
+ if (privacy.classification === "private" && !options?.includePrivate) return true;
26
+ if (privacy.pii && !options?.includePii) return true;
27
+ return false;
28
+ }
29
+ function filterAiwgRecordsByPrivacy(records, options) {
30
+ return records.filter((record) => !isPrivacyExcluded(record, options));
31
+ }
13
32
  var REQUIRED_RECORD_FIELDS = [
14
33
  "schema_version",
15
34
  "id",
@@ -36,8 +55,12 @@ function hasString(value) {
36
55
  return typeof value === "string" && value.length > 0;
37
56
  }
38
57
  function pushFacet(counts, name, value) {
39
- counts[name] ??= {};
40
- counts[name][value] = (counts[name][value] ?? 0) + 1;
58
+ let bucket = counts[name];
59
+ if (bucket === void 0) {
60
+ bucket = /* @__PURE__ */ Object.create(null);
61
+ counts[name] = bucket;
62
+ }
63
+ bucket[value] = (bucket[value] ?? 0) + 1;
41
64
  }
42
65
  function hasNonNegativeInteger(value) {
43
66
  return Number.isInteger(value) && typeof value === "number" && value >= 0;
@@ -159,6 +182,57 @@ function validateOptionalRichMetadata(item, index, errors) {
159
182
  errors.push("items[" + index + "].compatibility must be an object");
160
183
  }
161
184
  }
185
+ function isPrivacyClassification(value) {
186
+ return value === "private" || value === "sanitized" || value === "public";
187
+ }
188
+ function isProvenanceConfidence(value) {
189
+ return value === "source" || value === "candidate" || value === "reviewed" || value === "rejected";
190
+ }
191
+ function validateProvenanceItems(item, index, errors) {
192
+ if (!Array.isArray(item.provenance)) return;
193
+ for (const [provIndex, prov] of item.provenance.entries()) {
194
+ const at = "items[" + index + "].provenance[" + provIndex + "]";
195
+ if (!isPlainRecord(prov)) {
196
+ errors.push(at + " must be an object");
197
+ continue;
198
+ }
199
+ if (!hasString(prov.field)) errors.push(at + ".field is required");
200
+ if (!hasString(prov.source)) errors.push(at + ".source is required");
201
+ if (!hasString(prov.path)) errors.push(at + ".path is required");
202
+ if (!isProvenanceConfidence(prov.confidence)) errors.push(at + ".confidence must be one of source, candidate, reviewed, rejected");
203
+ if (!isPrivacyClassification(prov.privacy)) errors.push(at + ".privacy must be one of private, sanitized, public");
204
+ }
205
+ }
206
+ var V2_ONLY_RECORD_FIELDS = ["search", "chunks", "embeddings", "skos_concepts", "skos_relations", "compatibility"];
207
+ var V2_ONLY_SOURCE_FIELDS = ["origin", "generated", "checksum", "updated_at"];
208
+ var V2_ONLY_RELATIONSHIP_FIELDS = ["target_path", "direction", "metadata"];
209
+ function forbidV2FieldsOnV1Record(item, index, errors) {
210
+ if (item.schema_version !== "aiwg.fortemi.index.record.v1") return;
211
+ const at = "items[" + index + "]";
212
+ const bag = item;
213
+ const v2msg = " is a v2-only field and must be absent on a record.v1 record";
214
+ for (const field of V2_ONLY_RECORD_FIELDS) {
215
+ if (bag[field] !== void 0) errors.push(at + "." + field + v2msg);
216
+ }
217
+ if (isPlainRecord(item.source)) {
218
+ const src = item.source;
219
+ for (const field of V2_ONLY_SOURCE_FIELDS) {
220
+ if (src[field] !== void 0) errors.push(at + ".source." + field + v2msg);
221
+ }
222
+ }
223
+ if (isPlainRecord(item.privacy) && item.privacy.locality !== void 0) {
224
+ errors.push(at + ".privacy.locality" + v2msg);
225
+ }
226
+ if (Array.isArray(item.relationships)) {
227
+ for (const [relIndex, rel] of item.relationships.entries()) {
228
+ if (!isPlainRecord(rel)) continue;
229
+ const relBag = rel;
230
+ for (const field of V2_ONLY_RELATIONSHIP_FIELDS) {
231
+ if (relBag[field] !== void 0) errors.push(at + ".relationships[" + relIndex + "]." + field + v2msg);
232
+ }
233
+ }
234
+ }
235
+ }
162
236
  function validateAiwgFortemiIndexExport(value) {
163
237
  const errors = [];
164
238
  const counts = {};
@@ -169,10 +243,21 @@ function validateAiwgFortemiIndexExport(value) {
169
243
  if (!hasString(data?.generated_at)) errors.push("generated_at is required");
170
244
  if (!hasString(data?.source?.repo)) errors.push("source.repo is required");
171
245
  if (!hasString(data?.source?.privacy)) errors.push("source.privacy is required");
246
+ else if (!isPrivacyClassification(data?.source?.privacy)) {
247
+ errors.push("source.privacy must be one of private, sanitized, public");
248
+ }
172
249
  if (!Array.isArray(data?.items)) errors.push("items must be an array");
173
250
  if (data.compatibility !== void 0 && !isPlainRecord(data.compatibility)) {
174
251
  errors.push("compatibility must be an object");
175
252
  }
253
+ if (data?.schema_version === "aiwg.fortemi.index.export.v1") {
254
+ if (isPlainRecord(data.source) && data.source.graph !== void 0) {
255
+ errors.push("source.graph is a v2-only field and must be absent on an export.v1 export");
256
+ }
257
+ if (data.compatibility !== void 0) {
258
+ errors.push("compatibility is a v2-only field and must be absent on an export.v1 export");
259
+ }
260
+ }
176
261
  const ids = /* @__PURE__ */ new Set();
177
262
  let previousId = "";
178
263
  for (const [index, item] of (data.items ?? []).entries()) {
@@ -207,8 +292,12 @@ function validateAiwgFortemiIndexExport(value) {
207
292
  errors.push("items[" + index + "].provenance must be a non-empty array");
208
293
  }
209
294
  validateOptionalRichMetadata(item, index, errors);
295
+ validateProvenanceItems(item, index, errors);
296
+ forbidV2FieldsOnV1Record(item, index, errors);
210
297
  if (!item.privacy || typeof item.privacy.pii !== "boolean" || !hasString(item.privacy.classification)) {
211
298
  errors.push("items[" + index + "].privacy requires classification and pii");
299
+ } else if (!isPrivacyClassification(item.privacy.classification)) {
300
+ errors.push("items[" + index + "].privacy.classification must be one of private, sanitized, public");
212
301
  }
213
302
  }
214
303
  return { valid: errors.length === 0, errors, counts };
@@ -229,6 +318,9 @@ function validateAiwgFortemiChunkManifest(value) {
229
318
  if (!hasString(data?.generated_at)) errors.push("generated_at is required");
230
319
  if (!hasString(data?.source?.repo)) errors.push("source.repo is required");
231
320
  if (!hasString(data?.source?.privacy)) errors.push("source.privacy is required");
321
+ if (data?.source_export_schema_version !== void 0 && !isSupportedIndexSchemaVersion(data.source_export_schema_version)) {
322
+ errors.push("source_export_schema_version must be aiwg.fortemi.index.export.v1 or aiwg.fortemi.index.export.v2 when present");
323
+ }
232
324
  if (!hasNonNegativeInteger(data?.total)) errors.push("total must be a non-negative integer");
233
325
  if (!hasPositiveInteger(data?.part_size)) errors.push("part_size must be a positive integer");
234
326
  if (data.facets !== void 0 && !isFacetCounts(data.facets)) {
@@ -347,9 +439,35 @@ function assertAiwgFortemiChunkPart(value, partRef, manifest) {
347
439
  }
348
440
  return value;
349
441
  }
442
+ var ALLOWED_AIWG_FETCH_SCHEMES = /* @__PURE__ */ new Set(["http:", "https:", "blob:", "data:"]);
443
+ function tryParseUrl(value) {
444
+ try {
445
+ return new URL(value);
446
+ } catch {
447
+ return null;
448
+ }
449
+ }
450
+ function resolveAiwgFetchUrl(href, baseUrl) {
451
+ if (baseUrl === void 0) {
452
+ const absolute = tryParseUrl(href);
453
+ if (absolute && !ALLOWED_AIWG_FETCH_SCHEMES.has(absolute.protocol)) {
454
+ throw new Error("Refusing AIWG index fetch with disallowed scheme: " + absolute.protocol);
455
+ }
456
+ return href;
457
+ }
458
+ const base = new URL(baseUrl);
459
+ const resolved = new URL(href, base);
460
+ if (!ALLOWED_AIWG_FETCH_SCHEMES.has(resolved.protocol)) {
461
+ throw new Error("Refusing AIWG index fetch with disallowed scheme: " + resolved.protocol);
462
+ }
463
+ if (resolved.origin !== base.origin) {
464
+ throw new Error("Refusing cross-origin AIWG index fetch: " + resolved.origin + " != " + base.origin);
465
+ }
466
+ return resolved.toString();
467
+ }
350
468
  function createAiwgFetchChunkLoader(baseUrl) {
351
469
  return async (part) => {
352
- const href = baseUrl ? new URL(part.href, baseUrl).toString() : part.href;
470
+ const href = resolveAiwgFetchUrl(part.href, baseUrl);
353
471
  const response = await fetch(href);
354
472
  if (!response.ok) throw new Error("Failed to fetch AIWG index chunk " + href + ": " + response.status);
355
473
  return response.json();
@@ -369,14 +487,14 @@ function createAiwgFetchDetailLoader(baseUrl) {
369
487
  return async (id, manifest) => {
370
488
  if (!manifest.detail?.href) throw new Error("Manifest has no detail.href for record resolution");
371
489
  const relative = aiwgDetailHrefForId(manifest.detail, id);
372
- const href = baseUrl ? new URL(relative, baseUrl).toString() : relative;
490
+ const href = resolveAiwgFetchUrl(relative, baseUrl);
373
491
  const response = await fetch(href);
374
492
  if (!response.ok) throw new Error("Failed to fetch AIWG index detail " + href + ": " + response.status);
375
493
  return response.json();
376
494
  };
377
495
  }
378
496
  function getAiwgFortemiFacets(items) {
379
- const result = {};
497
+ const result = /* @__PURE__ */ Object.create(null);
380
498
  for (const item of items) {
381
499
  pushFacet(result, "type", item.type);
382
500
  pushFacet(result, "privacy", item.privacy.classification);
@@ -392,7 +510,55 @@ function recordTitle(item) {
392
510
  return item.title ?? item.search?.title ?? item.search?.name ?? item.id;
393
511
  }
394
512
  function recordText(item) {
395
- return item.text ?? item.search?.body ?? item.search?.summary ?? item.chunks?.map((chunk) => chunk.text ?? chunk.body ?? chunk.summary ?? "").filter(Boolean).join("\n") ?? "";
513
+ const base = item.text ?? item.search?.body ?? item.search?.summary ?? item.chunks?.map((chunk) => chunk.text ?? chunk.body ?? chunk.summary ?? "").filter(Boolean).join("\n") ?? "";
514
+ const extractedText = "binary_sources" in item ? item.binary_sources?.map((source) => source.extracted_text).filter(Boolean).join("\n") ?? "" : "";
515
+ return [base, extractedText].filter(Boolean).join("\n");
516
+ }
517
+ function defaultEmbeddingInput(record, granularity) {
518
+ const title = recordTitle(record);
519
+ const text = recordText(record);
520
+ if (granularity === "title-summary") {
521
+ const extractedText = record.binary_sources?.map((source) => source.extracted_text).filter(Boolean).join("\n") ?? "";
522
+ return [title, record.search?.summary ?? "", extractedText].filter(Boolean).join("\n");
523
+ }
524
+ return [title, text].filter(Boolean).join("\n");
525
+ }
526
+ function generatedAtString(value) {
527
+ if (value instanceof Date) return value.toISOString();
528
+ return value ?? (/* @__PURE__ */ new Date()).toISOString();
529
+ }
530
+ async function buildAiwgStaticEmbeddingSet(index, options) {
531
+ assertAiwgFortemiIndexExport(index);
532
+ const granularity = options.granularity ?? "body";
533
+ const records = filterAiwgRecordsByPrivacy(options.records ?? index.items, options.privacy);
534
+ const embeddings = [];
535
+ for (const record of records) {
536
+ const input = options.textForRecord?.(record) ?? defaultEmbeddingInput(record, granularity);
537
+ const embedding = await options.backend.embed(input, record);
538
+ if (embedding.length !== options.backend.dimensions) {
539
+ throw new Error(`Embedding for ${record.id} has ${embedding.length} dimensions; expected ${options.backend.dimensions}`);
540
+ }
541
+ embeddings.push({
542
+ record_id: record.id,
543
+ embedding,
544
+ granularity,
545
+ input_hash: computeHash(new TextEncoder().encode(input)),
546
+ source_path: record.source.path
547
+ });
548
+ }
549
+ const embeddingSet = {
550
+ schema_version: "aiwg.fortemi.embedding.set.v1",
551
+ id: options.id,
552
+ model: options.backend.model,
553
+ dimensions: options.backend.dimensions,
554
+ generated_at: generatedAtString(options.generatedAt),
555
+ granularity,
556
+ ...options.metric ? { metric: options.metric } : {},
557
+ input_hash_algorithm: "sha256",
558
+ embeddings
559
+ };
560
+ assertAiwgStaticEmbeddingSet(embeddingSet);
561
+ return embeddingSet;
396
562
  }
397
563
  function recordSearchValues(item) {
398
564
  const search = item.search;
@@ -425,7 +591,7 @@ function buildAiwgChunkedIndex(index, options = {}) {
425
591
  const projection = options.projection;
426
592
  const idEncoding = options.idEncoding ?? "base64url";
427
593
  const detailHref = options.detailHref ?? "detail/{id}.json";
428
- const items = index.items;
594
+ const items = filterAiwgRecordsByPrivacy(index.items, options.privacy);
429
595
  const pad = (value) => String(value).padStart(4, "0");
430
596
  const project = (record) => {
431
597
  if (!projection) return record;
@@ -453,6 +619,7 @@ function buildAiwgChunkedIndex(index, options = {}) {
453
619
  schema_version: "aiwg.fortemi.index.chunk-manifest.v1",
454
620
  generated_at: options.generatedAt ?? index.generated_at,
455
621
  source: index.source,
622
+ source_export_schema_version: index.schema_version,
456
623
  total: items.length,
457
624
  part_size: partSize,
458
625
  facets: getAiwgFortemiFacets(items),
@@ -763,8 +930,15 @@ function queryAiwgHybridIndex(index, embeddingSet, query, queryEmbedding, option
763
930
  };
764
931
  }).filter((result) => result !== null && result.score >= (options.minScore ?? -1)).sort((left, right) => right.score - left.score || left.item.id.localeCompare(right.item.id)).slice(offset, offset + limit);
765
932
  }
766
- function findAiwgStaticDuplicatePairs(index, embeddingSet, threshold = 0.9) {
933
+ var DEFAULT_AIWG_DUPLICATE_SCAN_MAX_EMBEDDINGS = 5e3;
934
+ function findAiwgStaticDuplicatePairs(index, embeddingSet, threshold = 0.9, options) {
767
935
  assertAiwgStaticEmbeddingSet(embeddingSet);
936
+ const maxEmbeddings = options?.maxEmbeddings ?? DEFAULT_AIWG_DUPLICATE_SCAN_MAX_EMBEDDINGS;
937
+ if (embeddingSet.embeddings.length > maxEmbeddings) {
938
+ throw new Error(
939
+ "Embedding set too large for duplicate scan: " + embeddingSet.embeddings.length + " > " + maxEmbeddings + " (raise options.maxEmbeddings to override for trusted input)"
940
+ );
941
+ }
768
942
  const byId = new Map(index.items.map((item) => [item.id, item]));
769
943
  const pairs = [];
770
944
  for (let leftIndex = 0; leftIndex < embeddingSet.embeddings.length; leftIndex += 1) {
@@ -1229,7 +1403,7 @@ function createAiwgIndexController(initialIndex) {
1229
1403
  notify();
1230
1404
  },
1231
1405
  createReviewDecisionExport(generatedAt) {
1232
- const source = index ?? (chunked ? { schema_version: "aiwg.fortemi.index.export.v1" } : null);
1406
+ const source = index ?? (chunked ? { schema_version: chunked.manifest.source_export_schema_version ?? "aiwg.fortemi.index.export.v1" } : null);
1233
1407
  if (!source) throw new Error("No AIWG index export or chunked manifest loaded");
1234
1408
  return createAiwgReviewDecisionExport(source, reviewDecisions, generatedAt);
1235
1409
  },
@@ -1285,6 +1459,6 @@ function communityIdsFor(item, options) {
1285
1459
  return [`type:${item.type}`];
1286
1460
  }
1287
1461
 
1288
- export { AIWG_SCAN_REQUIRED_FIELDS, aiwgDetailHrefForId, aiwgFortemiIndexToCommunityGraph, assertAiwgFortemiChunkManifest, assertAiwgFortemiChunkPart, assertAiwgFortemiIndexExport, assertAiwgStaticEmbeddingSet, buildAiwgChunkedIndex, createAiwgFetchChunkLoader, createAiwgFetchDetailLoader, createAiwgIndexController, createAiwgReviewDecisionExport, encodeAiwgDetailId, findAiwgStaticDuplicatePairs, getAiwgFortemiFacets, queryAiwgFortemiIndex, queryAiwgHybridIndex, queryAiwgSemanticIndex, validateAiwgFortemiChunkManifest, validateAiwgFortemiChunkPart, validateAiwgFortemiIndexExport, validateAiwgStaticEmbeddingSet };
1462
+ export { AIWG_SCAN_REQUIRED_FIELDS, DEFAULT_AIWG_DUPLICATE_SCAN_MAX_EMBEDDINGS, aiwgDetailHrefForId, aiwgFortemiIndexToCommunityGraph, assertAiwgFortemiChunkManifest, assertAiwgFortemiChunkPart, assertAiwgFortemiIndexExport, assertAiwgStaticEmbeddingSet, buildAiwgChunkedIndex, buildAiwgStaticEmbeddingSet, createAiwgFetchChunkLoader, createAiwgFetchDetailLoader, createAiwgIndexController, createAiwgReviewDecisionExport, encodeAiwgDetailId, filterAiwgRecordsByPrivacy, findAiwgStaticDuplicatePairs, getAiwgFortemiFacets, queryAiwgFortemiIndex, queryAiwgHybridIndex, queryAiwgSemanticIndex, resolveAiwgFetchUrl, validateAiwgFortemiChunkManifest, validateAiwgFortemiChunkPart, validateAiwgFortemiIndexExport, validateAiwgStaticEmbeddingSet };
1289
1463
  //# sourceMappingURL=aiwg-index.js.map
1290
1464
  //# sourceMappingURL=aiwg-index.js.map