@fortemi/core 2026.7.3 → 2026.7.4

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.
@@ -1,5 +1,7 @@
1
1
  import { sha256 } from '@noble/hashes/sha256';
2
+ import '@noble/hashes/blake3';
2
3
  import { bytesToHex } from '@noble/hashes/utils';
4
+ import Ajv2020 from 'ajv/dist/2020.js';
3
5
 
4
6
  // src/hash.ts
5
7
  function computeHash(data) {
@@ -7,6 +9,720 @@ function computeHash(data) {
7
9
  return `sha256:${bytesToHex(digest)}`;
8
10
  }
9
11
 
12
+ // schemas/aiwg-fortemi-index-export.schema.json
13
+ var aiwg_fortemi_index_export_schema_default = {
14
+ $schema: "https://json-schema.org/draft/2020-12/schema",
15
+ $id: "https://aiwg.io/schemas/aiwg-fortemi-index-export.json",
16
+ title: "AIWG Fortemi Index Export",
17
+ type: "object",
18
+ additionalProperties: false,
19
+ required: ["schema_version", "generated_at", "source", "items"],
20
+ properties: {
21
+ schema_version: {
22
+ enum: ["aiwg.fortemi.index.export.v1", "aiwg.fortemi.index.export.v2"]
23
+ },
24
+ generated_at: {
25
+ type: "string",
26
+ format: "date-time"
27
+ },
28
+ source: {
29
+ type: "object",
30
+ additionalProperties: false,
31
+ required: ["repo", "privacy"],
32
+ properties: {
33
+ repo: {
34
+ type: "string",
35
+ minLength: 1
36
+ },
37
+ privacy: {
38
+ $ref: "#/$defs/privacy"
39
+ },
40
+ graph: {
41
+ type: "string",
42
+ minLength: 1
43
+ }
44
+ }
45
+ },
46
+ compatibility: {
47
+ type: "object",
48
+ additionalProperties: false,
49
+ required: ["previous_schema_version", "strategy"],
50
+ properties: {
51
+ previous_schema_version: {
52
+ const: "aiwg.fortemi.index.export.v1"
53
+ },
54
+ strategy: {
55
+ const: "supported"
56
+ }
57
+ }
58
+ },
59
+ items: {
60
+ type: "array",
61
+ items: {
62
+ $ref: "#/$defs/record"
63
+ }
64
+ }
65
+ },
66
+ allOf: [
67
+ {
68
+ if: {
69
+ properties: {
70
+ schema_version: {
71
+ const: "aiwg.fortemi.index.export.v1"
72
+ }
73
+ }
74
+ },
75
+ then: {
76
+ not: {
77
+ required: ["compatibility"]
78
+ },
79
+ properties: {
80
+ source: {
81
+ not: {
82
+ required: ["graph"]
83
+ }
84
+ },
85
+ items: {
86
+ items: {
87
+ allOf: [
88
+ {
89
+ properties: {
90
+ schema_version: {
91
+ const: "aiwg.fortemi.index.record.v1"
92
+ }
93
+ }
94
+ },
95
+ {
96
+ $ref: "#/$defs/v1RecordCompatibility"
97
+ }
98
+ ]
99
+ }
100
+ }
101
+ }
102
+ }
103
+ },
104
+ {
105
+ if: {
106
+ properties: {
107
+ schema_version: {
108
+ const: "aiwg.fortemi.index.export.v2"
109
+ }
110
+ }
111
+ },
112
+ then: {
113
+ required: ["compatibility"],
114
+ properties: {
115
+ source: {
116
+ required: ["graph"]
117
+ },
118
+ items: {
119
+ items: {
120
+ properties: {
121
+ schema_version: {
122
+ const: "aiwg.fortemi.index.record.v2"
123
+ }
124
+ }
125
+ }
126
+ }
127
+ }
128
+ }
129
+ }
130
+ ],
131
+ $defs: {
132
+ privacy: {
133
+ enum: ["private", "sanitized", "public"]
134
+ },
135
+ recordType: {
136
+ type: "string",
137
+ minLength: 1
138
+ },
139
+ stringArray: {
140
+ type: "array",
141
+ items: {
142
+ type: "string"
143
+ }
144
+ },
145
+ numberArray: {
146
+ type: "array",
147
+ items: {
148
+ type: "number"
149
+ }
150
+ },
151
+ v1RecordCompatibility: {
152
+ type: "object",
153
+ not: {
154
+ anyOf: [
155
+ {
156
+ required: ["name"]
157
+ },
158
+ {
159
+ required: ["summary"]
160
+ },
161
+ {
162
+ required: ["search"]
163
+ },
164
+ {
165
+ required: ["chunks"]
166
+ },
167
+ {
168
+ required: ["embeddings"]
169
+ },
170
+ {
171
+ required: ["compatibility"]
172
+ },
173
+ {
174
+ required: ["skos_concepts"]
175
+ },
176
+ {
177
+ required: ["skos_relations"]
178
+ },
179
+ {
180
+ required: ["provenance_events"]
181
+ }
182
+ ]
183
+ },
184
+ properties: {
185
+ source: {
186
+ not: {
187
+ anyOf: [
188
+ {
189
+ required: ["origin"]
190
+ },
191
+ {
192
+ required: ["generated"]
193
+ },
194
+ {
195
+ required: ["checksum"]
196
+ },
197
+ {
198
+ required: ["updated_at"]
199
+ }
200
+ ]
201
+ }
202
+ },
203
+ relationships: {
204
+ items: {
205
+ not: {
206
+ anyOf: [
207
+ {
208
+ required: ["target_path"]
209
+ },
210
+ {
211
+ required: ["direction"]
212
+ },
213
+ {
214
+ required: ["label"]
215
+ },
216
+ {
217
+ required: ["confidence"]
218
+ },
219
+ {
220
+ required: ["privacy"]
221
+ },
222
+ {
223
+ required: ["metadata"]
224
+ }
225
+ ]
226
+ }
227
+ }
228
+ },
229
+ privacy: {
230
+ not: {
231
+ required: ["locality"]
232
+ }
233
+ }
234
+ }
235
+ },
236
+ record: {
237
+ type: "object",
238
+ additionalProperties: false,
239
+ required: [
240
+ "schema_version",
241
+ "id",
242
+ "type",
243
+ "source",
244
+ "title",
245
+ "text",
246
+ "facets",
247
+ "tags",
248
+ "concepts",
249
+ "relationships",
250
+ "provenance",
251
+ "privacy",
252
+ "updated_at"
253
+ ],
254
+ properties: {
255
+ schema_version: {
256
+ enum: [
257
+ "aiwg.fortemi.index.record.v1",
258
+ "aiwg.fortemi.index.record.v2"
259
+ ]
260
+ },
261
+ id: {
262
+ type: "string",
263
+ minLength: 1
264
+ },
265
+ type: {
266
+ $ref: "#/$defs/recordType"
267
+ },
268
+ source: {
269
+ type: "object",
270
+ additionalProperties: false,
271
+ required: ["path", "repo_relative_path", "locator"],
272
+ properties: {
273
+ path: {
274
+ type: "string",
275
+ minLength: 1
276
+ },
277
+ repo_relative_path: {
278
+ type: "string",
279
+ minLength: 1
280
+ },
281
+ locator: {
282
+ type: "string",
283
+ minLength: 1
284
+ },
285
+ origin: {
286
+ type: "string",
287
+ minLength: 1
288
+ },
289
+ generated: {
290
+ type: "boolean"
291
+ },
292
+ checksum: {
293
+ type: "string"
294
+ },
295
+ updated_at: {
296
+ type: "string",
297
+ format: "date-time"
298
+ }
299
+ }
300
+ },
301
+ title: {
302
+ type: "string"
303
+ },
304
+ name: {
305
+ type: "string"
306
+ },
307
+ summary: {
308
+ type: "string"
309
+ },
310
+ text: {
311
+ type: "string"
312
+ },
313
+ search: {
314
+ type: "object",
315
+ additionalProperties: false,
316
+ required: [
317
+ "title",
318
+ "body",
319
+ "triggers",
320
+ "aliases",
321
+ "tags",
322
+ "frontmatter"
323
+ ],
324
+ properties: {
325
+ title: {
326
+ type: "string"
327
+ },
328
+ name: {
329
+ type: "string"
330
+ },
331
+ summary: {
332
+ type: "string"
333
+ },
334
+ body: {
335
+ type: "string"
336
+ },
337
+ triggers: {
338
+ $ref: "#/$defs/stringArray"
339
+ },
340
+ aliases: {
341
+ $ref: "#/$defs/stringArray"
342
+ },
343
+ capability: {
344
+ type: "string"
345
+ },
346
+ tags: {
347
+ $ref: "#/$defs/stringArray"
348
+ },
349
+ phase: {
350
+ type: "string"
351
+ },
352
+ type: {
353
+ type: "string"
354
+ },
355
+ frontmatter: {
356
+ type: "object"
357
+ }
358
+ }
359
+ },
360
+ facets: {
361
+ type: "object",
362
+ additionalProperties: {
363
+ $ref: "#/$defs/stringArray"
364
+ }
365
+ },
366
+ tags: {
367
+ $ref: "#/$defs/stringArray"
368
+ },
369
+ concepts: {
370
+ $ref: "#/$defs/stringArray"
371
+ },
372
+ relationships: {
373
+ type: "array",
374
+ items: {
375
+ type: "object",
376
+ additionalProperties: false,
377
+ required: ["type", "target_id"],
378
+ properties: {
379
+ type: {
380
+ type: "string",
381
+ minLength: 1
382
+ },
383
+ target_id: {
384
+ type: "string",
385
+ minLength: 1
386
+ },
387
+ source_path: {
388
+ type: "string"
389
+ },
390
+ target_path: {
391
+ type: "string"
392
+ },
393
+ direction: {
394
+ enum: ["upstream", "downstream", "related"]
395
+ },
396
+ label: {
397
+ type: "string"
398
+ },
399
+ confidence: {
400
+ type: "number"
401
+ },
402
+ privacy: {
403
+ $ref: "#/$defs/privacy"
404
+ },
405
+ metadata: {
406
+ type: "object"
407
+ }
408
+ }
409
+ }
410
+ },
411
+ provenance: {
412
+ type: "array",
413
+ minItems: 1,
414
+ items: {
415
+ type: "object",
416
+ additionalProperties: false,
417
+ required: ["field", "source", "path", "confidence", "privacy"],
418
+ properties: {
419
+ field: {
420
+ type: "string",
421
+ minLength: 1
422
+ },
423
+ source: {
424
+ type: "string",
425
+ minLength: 1
426
+ },
427
+ path: {
428
+ type: "string",
429
+ minLength: 1
430
+ },
431
+ confidence: {
432
+ enum: ["source", "candidate", "reviewed", "rejected"]
433
+ },
434
+ privacy: {
435
+ $ref: "#/$defs/privacy"
436
+ }
437
+ }
438
+ }
439
+ },
440
+ privacy: {
441
+ type: "object",
442
+ additionalProperties: false,
443
+ required: ["classification", "pii"],
444
+ properties: {
445
+ classification: {
446
+ $ref: "#/$defs/privacy"
447
+ },
448
+ pii: {
449
+ type: "boolean"
450
+ },
451
+ locality: {
452
+ enum: ["project", "framework", "external"]
453
+ }
454
+ }
455
+ },
456
+ chunks: {
457
+ type: "array",
458
+ items: {
459
+ type: "object",
460
+ additionalProperties: false,
461
+ properties: {
462
+ id: {
463
+ type: "string",
464
+ minLength: 1
465
+ },
466
+ text: {
467
+ type: "string"
468
+ },
469
+ body: {
470
+ type: "string"
471
+ },
472
+ summary: {
473
+ type: "string"
474
+ },
475
+ source_path: {
476
+ type: "string"
477
+ },
478
+ metadata: {
479
+ type: "object"
480
+ },
481
+ checksum: {
482
+ type: "string"
483
+ }
484
+ }
485
+ }
486
+ },
487
+ embeddings: {
488
+ type: "array",
489
+ items: {
490
+ type: "object",
491
+ additionalProperties: false,
492
+ properties: {
493
+ id: {
494
+ type: "string"
495
+ },
496
+ model: {
497
+ type: "string"
498
+ },
499
+ embedding: {
500
+ $ref: "#/$defs/numberArray"
501
+ },
502
+ vector: {
503
+ $ref: "#/$defs/numberArray"
504
+ },
505
+ granularity: {
506
+ type: "string"
507
+ },
508
+ source_path: {
509
+ type: "string"
510
+ },
511
+ metadata: {
512
+ type: "object"
513
+ },
514
+ chunk_id: {
515
+ type: "string"
516
+ },
517
+ vector_ref: {
518
+ type: "string"
519
+ },
520
+ input_hash: {
521
+ type: "string"
522
+ }
523
+ }
524
+ }
525
+ },
526
+ compatibility: {
527
+ type: "object"
528
+ },
529
+ skos_concepts: {
530
+ type: "array",
531
+ items: {
532
+ type: "object",
533
+ additionalProperties: false,
534
+ required: ["id", "prefLabel"],
535
+ properties: {
536
+ id: {
537
+ type: "string",
538
+ minLength: 1
539
+ },
540
+ prefLabel: {
541
+ type: "string",
542
+ minLength: 1
543
+ },
544
+ definition: {
545
+ type: "string"
546
+ },
547
+ scheme: {
548
+ type: "string"
549
+ },
550
+ notation: {
551
+ type: "string"
552
+ },
553
+ uri: {
554
+ type: "string"
555
+ },
556
+ altLabels: {
557
+ $ref: "#/$defs/stringArray"
558
+ },
559
+ metadata: {
560
+ type: "object"
561
+ }
562
+ }
563
+ }
564
+ },
565
+ skos_relations: {
566
+ type: "array",
567
+ items: {
568
+ type: "object",
569
+ additionalProperties: false,
570
+ required: ["type", "source_id", "target_id"],
571
+ properties: {
572
+ type: {
573
+ type: "string",
574
+ minLength: 1
575
+ },
576
+ source_id: {
577
+ type: "string",
578
+ minLength: 1
579
+ },
580
+ target_id: {
581
+ type: "string",
582
+ minLength: 1
583
+ },
584
+ source_path: {
585
+ type: "string"
586
+ },
587
+ metadata: {
588
+ type: "object"
589
+ }
590
+ }
591
+ }
592
+ },
593
+ provenance_events: {
594
+ type: "array",
595
+ items: {
596
+ type: "object",
597
+ additionalProperties: false,
598
+ required: ["activity"],
599
+ properties: {
600
+ id: {
601
+ type: "string"
602
+ },
603
+ activity: {
604
+ type: "string",
605
+ minLength: 1
606
+ },
607
+ agent: {
608
+ type: "string"
609
+ },
610
+ started_at: {
611
+ type: "string",
612
+ format: "date-time"
613
+ },
614
+ ended_at: {
615
+ type: "string",
616
+ format: "date-time"
617
+ },
618
+ source: {
619
+ type: "string"
620
+ },
621
+ path: {
622
+ type: "string"
623
+ },
624
+ confidence: {
625
+ enum: ["source", "candidate", "reviewed", "rejected"]
626
+ },
627
+ privacy: {
628
+ $ref: "#/$defs/privacy"
629
+ },
630
+ attributes: {
631
+ type: "object"
632
+ }
633
+ }
634
+ }
635
+ },
636
+ updated_at: {
637
+ type: "string",
638
+ format: "date-time"
639
+ }
640
+ }
641
+ }
642
+ }
643
+ };
644
+
645
+ // src/aiwg-index-schema.ts
646
+ var PROJECTED_FIELDS = [
647
+ "schema_version",
648
+ "id",
649
+ "type",
650
+ "title",
651
+ "text",
652
+ "facets",
653
+ "tags",
654
+ "concepts",
655
+ "privacy"
656
+ ];
657
+ var ajvInstance;
658
+ var exportValidator;
659
+ var projectedRecordValidator;
660
+ function getAjv() {
661
+ if (!ajvInstance) {
662
+ ajvInstance = new Ajv2020({
663
+ allErrors: true,
664
+ strict: false,
665
+ validateFormats: false
666
+ });
667
+ ajvInstance.addSchema(aiwg_fortemi_index_export_schema_default);
668
+ }
669
+ return ajvInstance;
670
+ }
671
+ function formatErrors(errors) {
672
+ return (errors ?? []).map((error) => {
673
+ const path = error.instancePath || "(root)";
674
+ return `${path} ${error.message ?? "is invalid"}`;
675
+ });
676
+ }
677
+ function getExportValidator() {
678
+ exportValidator ??= getAjv().getSchema(aiwg_fortemi_index_export_schema_default.$id) ?? getAjv().compile(aiwg_fortemi_index_export_schema_default);
679
+ return exportValidator;
680
+ }
681
+ function getProjectedRecordValidator() {
682
+ if (!projectedRecordValidator) {
683
+ const properties = Object.fromEntries(Object.keys(aiwg_fortemi_index_export_schema_default.$defs.record.properties).map((field) => [
684
+ field,
685
+ { $ref: `${aiwg_fortemi_index_export_schema_default.$id}#/$defs/record/properties/${field}` }
686
+ ]));
687
+ projectedRecordValidator = getAjv().compile({
688
+ type: "object",
689
+ required: PROJECTED_FIELDS,
690
+ properties,
691
+ additionalProperties: true,
692
+ allOf: [
693
+ {
694
+ if: {
695
+ properties: {
696
+ schema_version: { const: "aiwg.fortemi.index.record.v1" }
697
+ }
698
+ },
699
+ then: { $ref: `${aiwg_fortemi_index_export_schema_default.$id}#/$defs/v1RecordCompatibility` }
700
+ }
701
+ ]
702
+ });
703
+ }
704
+ return projectedRecordValidator;
705
+ }
706
+ function validateAiwgFortemiIndexExportSchema(value) {
707
+ const validate = getExportValidator();
708
+ const schemaValue = value && typeof value === "object" && Array.isArray(value.items) ? {
709
+ ...value,
710
+ items: value.items.map((item) => {
711
+ if (!item || typeof item !== "object" || Array.isArray(item)) return item;
712
+ const schemaRecord = { ...item };
713
+ Reflect.deleteProperty(schemaRecord, "binary_sources");
714
+ return schemaRecord;
715
+ })
716
+ } : value;
717
+ const valid = validate(schemaValue);
718
+ return { valid, errors: formatErrors(validate.errors) };
719
+ }
720
+ function validateAiwgFortemiProjectedRecordSchema(value) {
721
+ const validate = getProjectedRecordValidator();
722
+ const valid = validate(value);
723
+ return { valid, errors: formatErrors(validate.errors) };
724
+ }
725
+
10
726
  // src/aiwg-index.ts
11
727
  var AIWG_SCAN_REQUIRED_FIELDS = [
12
728
  "schema_version",
@@ -21,7 +737,7 @@ var AIWG_SCAN_REQUIRED_FIELDS = [
21
737
  ];
22
738
  function isPrivacyExcluded(record, options) {
23
739
  const privacy = record.privacy;
24
- if (!privacy) return false;
740
+ if (!privacy || !isPrivacyClassification(privacy.classification) || typeof privacy.pii !== "boolean") return true;
25
741
  if (privacy.classification === "private" && !options?.includePrivate) return true;
26
742
  if (privacy.pii && !options?.includePii) return true;
27
743
  return false;
@@ -90,6 +806,10 @@ function validateOptionalRichMetadata(item, index, errors) {
90
806
  errors.push("items[" + index + "].skos_concepts must be an array when present");
91
807
  } else {
92
808
  for (const [conceptIndex, concept] of item.skos_concepts.entries()) {
809
+ if (!isPlainRecord(concept)) {
810
+ errors.push("items[" + index + "].skos_concepts[" + conceptIndex + "] must be an object");
811
+ continue;
812
+ }
93
813
  if (!hasString(concept.id)) errors.push("items[" + index + "].skos_concepts[" + conceptIndex + "].id is required");
94
814
  if (!hasString(concept.prefLabel)) errors.push("items[" + index + "].skos_concepts[" + conceptIndex + "].prefLabel is required");
95
815
  if (!isOptionalStringArray(concept.altLabels)) errors.push("items[" + index + "].skos_concepts[" + conceptIndex + "].altLabels must be a string array");
@@ -104,6 +824,10 @@ function validateOptionalRichMetadata(item, index, errors) {
104
824
  errors.push("items[" + index + "].skos_relations must be an array when present");
105
825
  } else {
106
826
  for (const [relationIndex, relation] of item.skos_relations.entries()) {
827
+ if (!isPlainRecord(relation)) {
828
+ errors.push("items[" + index + "].skos_relations[" + relationIndex + "] must be an object");
829
+ continue;
830
+ }
107
831
  if (!hasString(relation.type)) errors.push("items[" + index + "].skos_relations[" + relationIndex + "].type is required");
108
832
  if (!hasString(relation.source_id)) errors.push("items[" + index + "].skos_relations[" + relationIndex + "].source_id is required");
109
833
  if (!hasString(relation.target_id)) errors.push("items[" + index + "].skos_relations[" + relationIndex + "].target_id is required");
@@ -118,6 +842,10 @@ function validateOptionalRichMetadata(item, index, errors) {
118
842
  errors.push("items[" + index + "].provenance_events must be an array when present");
119
843
  } else {
120
844
  for (const [eventIndex, event] of item.provenance_events.entries()) {
845
+ if (!isPlainRecord(event)) {
846
+ errors.push("items[" + index + "].provenance_events[" + eventIndex + "] must be an object");
847
+ continue;
848
+ }
121
849
  if (!hasString(event.activity)) errors.push("items[" + index + "].provenance_events[" + eventIndex + "].activity is required");
122
850
  if (event.attributes !== void 0 && !isPlainRecord(event.attributes)) {
123
851
  errors.push("items[" + index + "].provenance_events[" + eventIndex + "].attributes must be an object");
@@ -127,6 +855,10 @@ function validateOptionalRichMetadata(item, index, errors) {
127
855
  }
128
856
  if (Array.isArray(item.relationships)) {
129
857
  for (const [relationshipIndex, relationship] of item.relationships.entries()) {
858
+ if (!isPlainRecord(relationship)) {
859
+ errors.push("items[" + index + "].relationships[" + relationshipIndex + "] must be an object");
860
+ continue;
861
+ }
130
862
  if (relationship.metadata !== void 0 && !isPlainRecord(relationship.metadata)) {
131
863
  errors.push("items[" + index + "].relationships[" + relationshipIndex + "].metadata must be an object");
132
864
  }
@@ -155,7 +887,10 @@ function validateOptionalRichMetadata(item, index, errors) {
155
887
  errors.push("items[" + index + "].chunks must be an array when present");
156
888
  } else {
157
889
  for (const [chunkIndex, chunk] of item.chunks.entries()) {
158
- if (!isPlainRecord(chunk)) errors.push("items[" + index + "].chunks[" + chunkIndex + "] must be an object");
890
+ if (!isPlainRecord(chunk)) {
891
+ errors.push("items[" + index + "].chunks[" + chunkIndex + "] must be an object");
892
+ continue;
893
+ }
159
894
  if (chunk.metadata !== void 0 && !isPlainRecord(chunk.metadata)) {
160
895
  errors.push("items[" + index + "].chunks[" + chunkIndex + "].metadata must be an object");
161
896
  }
@@ -167,7 +902,10 @@ function validateOptionalRichMetadata(item, index, errors) {
167
902
  errors.push("items[" + index + "].embeddings must be an array when present");
168
903
  } else {
169
904
  for (const [embeddingIndex, embedding] of item.embeddings.entries()) {
170
- if (!isPlainRecord(embedding)) errors.push("items[" + index + "].embeddings[" + embeddingIndex + "] must be an object");
905
+ if (!isPlainRecord(embedding)) {
906
+ errors.push("items[" + index + "].embeddings[" + embeddingIndex + "] must be an object");
907
+ continue;
908
+ }
171
909
  const vector = embedding.embedding ?? embedding.vector;
172
910
  if (vector !== void 0 && (!Array.isArray(vector) || !vector.every((entry) => typeof entry === "number"))) {
173
911
  errors.push("items[" + index + "].embeddings[" + embeddingIndex + "].embedding/vector must be a number array");
@@ -234,9 +972,10 @@ function forbidV2FieldsOnV1Record(item, index, errors) {
234
972
  }
235
973
  }
236
974
  function validateAiwgFortemiIndexExport(value) {
237
- const errors = [];
238
- const counts = {};
239
- const data = value;
975
+ const errors = validateAiwgFortemiIndexExportSchema(value).errors;
976
+ const counts = /* @__PURE__ */ Object.create(null);
977
+ const data = isPlainRecord(value) ? value : {};
978
+ if (!isPlainRecord(value)) errors.push("index export must be an object");
240
979
  if (!isSupportedIndexSchemaVersion(data?.schema_version)) {
241
980
  errors.push("schema_version must be aiwg.fortemi.index.export.v1 or aiwg.fortemi.index.export.v2");
242
981
  }
@@ -260,7 +999,12 @@ function validateAiwgFortemiIndexExport(value) {
260
999
  }
261
1000
  const ids = /* @__PURE__ */ new Set();
262
1001
  let previousId = "";
263
- for (const [index, item] of (data.items ?? []).entries()) {
1002
+ const items = Array.isArray(data.items) ? data.items : [];
1003
+ for (const [index, item] of items.entries()) {
1004
+ if (!isPlainRecord(item)) {
1005
+ errors.push("items[" + index + "] must be an object");
1006
+ continue;
1007
+ }
264
1008
  for (const field of REQUIRED_RECORD_FIELDS) {
265
1009
  if (!(field in item)) errors.push("items[" + index + "]." + field + " is required");
266
1010
  }
@@ -270,7 +1014,7 @@ function validateAiwgFortemiIndexExport(value) {
270
1014
  if (!hasString(item.id)) errors.push("items[" + index + "].id is required");
271
1015
  if (hasString(item.id) && ids.has(item.id)) errors.push("duplicate id: " + item.id);
272
1016
  if (hasString(item.id)) ids.add(item.id);
273
- if (previousId && hasString(item.id) && previousId.localeCompare(item.id) > 0) {
1017
+ if (previousId && hasString(item.id) && previousId > item.id) {
274
1018
  errors.push("items must be sorted by id: " + previousId + " before " + item.id);
275
1019
  }
276
1020
  if (hasString(item.id)) previousId = item.id;
@@ -311,7 +1055,8 @@ function assertAiwgFortemiIndexExport(value) {
311
1055
  }
312
1056
  function validateAiwgFortemiChunkManifest(value) {
313
1057
  const errors = [];
314
- const data = value;
1058
+ const data = isPlainRecord(value) ? value : {};
1059
+ if (!isPlainRecord(value)) errors.push("chunk manifest must be an object");
315
1060
  if (data?.schema_version !== "aiwg.fortemi.index.chunk-manifest.v1") {
316
1061
  errors.push("schema_version must be aiwg.fortemi.index.chunk-manifest.v1");
317
1062
  }
@@ -336,7 +1081,9 @@ function validateAiwgFortemiChunkManifest(value) {
336
1081
  }
337
1082
  }
338
1083
  }
339
- if (data.detail !== void 0) {
1084
+ if (data.detail !== void 0 && !isPlainRecord(data.detail)) {
1085
+ errors.push("detail must be an object");
1086
+ } else if (data.detail !== void 0) {
340
1087
  if (!hasString(data.detail.href)) errors.push("detail.href is required");
341
1088
  else if (!data.detail.href.includes("{id}")) errors.push("detail.href must contain the {id} placeholder");
342
1089
  if (data.detail.encoding !== void 0 && data.detail.encoding !== "uri" && data.detail.encoding !== "base64url") {
@@ -347,6 +1094,10 @@ function validateAiwgFortemiChunkManifest(value) {
347
1094
  let expectedOffset = 0;
348
1095
  const parts = Array.isArray(data?.parts) ? data.parts : [];
349
1096
  for (const [index, part] of parts.entries()) {
1097
+ if (!isPlainRecord(part)) {
1098
+ errors.push("parts[" + index + "] must be an object");
1099
+ continue;
1100
+ }
350
1101
  if (!hasString(part.href)) errors.push("parts[" + index + "].href is required");
351
1102
  if (!hasNonNegativeInteger(part.offset)) errors.push("parts[" + index + "].offset must be a non-negative integer");
352
1103
  if (!hasNonNegativeInteger(part.count)) errors.push("parts[" + index + "].count must be a non-negative integer");
@@ -367,18 +1118,28 @@ function assertAiwgFortemiChunkManifest(value) {
367
1118
  }
368
1119
  return value;
369
1120
  }
370
- function validateProjectedRecords(items) {
1121
+ function validateProjectedRecords(items, sourceSchemaVersion) {
371
1122
  const errors = [];
372
1123
  const ids = /* @__PURE__ */ new Set();
373
1124
  let previousId = "";
374
1125
  for (const [index, item] of items.entries()) {
1126
+ if (!isPlainRecord(item)) {
1127
+ errors.push("items[" + index + "] must be an object");
1128
+ continue;
1129
+ }
1130
+ const schemaValidation = validateAiwgFortemiProjectedRecordSchema(item);
1131
+ errors.push(...schemaValidation.errors.map((error) => `items[${index}]${error}`));
1132
+ const expectedRecordVersion = sourceSchemaVersion === "aiwg.fortemi.index.export.v2" ? "aiwg.fortemi.index.record.v2" : "aiwg.fortemi.index.record.v1";
1133
+ if (item.schema_version !== expectedRecordVersion) {
1134
+ errors.push(`items[${index}].schema_version must match ${sourceSchemaVersion}`);
1135
+ }
375
1136
  if (!isSupportedRecordSchemaVersion(item.schema_version)) {
376
1137
  errors.push("items[" + index + "].schema_version must be aiwg.fortemi.index.record.v1 or aiwg.fortemi.index.record.v2");
377
1138
  }
378
1139
  if (!hasString(item.id)) errors.push("items[" + index + "].id is required");
379
1140
  if (hasString(item.id) && ids.has(item.id)) errors.push("duplicate id: " + item.id);
380
1141
  if (hasString(item.id)) ids.add(item.id);
381
- if (previousId && hasString(item.id) && previousId.localeCompare(item.id) > 0) {
1142
+ if (previousId && hasString(item.id) && previousId > item.id) {
382
1143
  errors.push("items must be sorted by id: " + previousId + " before " + item.id);
383
1144
  }
384
1145
  if (hasString(item.id)) previousId = item.id;
@@ -394,15 +1155,13 @@ function validateProjectedRecords(items) {
394
1155
  }
395
1156
  if (!Array.isArray(item.tags)) errors.push("items[" + index + "].tags must be an array");
396
1157
  if (!Array.isArray(item.concepts)) errors.push("items[" + index + "].concepts must be an array");
397
- if (!item.privacy || !hasString(item.privacy.classification)) {
398
- errors.push("items[" + index + "].privacy.classification is required");
399
- }
400
1158
  }
401
1159
  return errors;
402
1160
  }
403
1161
  function validateAiwgFortemiChunkPart(value, partRef, manifest) {
404
1162
  const errors = [];
405
- const data = value;
1163
+ const data = isPlainRecord(value) ? value : {};
1164
+ if (!isPlainRecord(value)) errors.push("chunk part must be an object");
406
1165
  if (data?.schema_version !== "aiwg.fortemi.index.chunk.v1") {
407
1166
  errors.push("schema_version must be aiwg.fortemi.index.chunk.v1");
408
1167
  }
@@ -419,12 +1178,16 @@ function validateAiwgFortemiChunkPart(value, partRef, manifest) {
419
1178
  }
420
1179
  if (Array.isArray(data?.items)) {
421
1180
  if (manifest?.projection) {
422
- errors.push(...validateProjectedRecords(data.items).map((error) => "items." + error));
1181
+ errors.push(...validateProjectedRecords(
1182
+ data.items,
1183
+ manifest.source_export_schema_version ?? "aiwg.fortemi.index.export.v1"
1184
+ ).map((error) => "items." + error));
423
1185
  } else {
424
1186
  const validation = validateAiwgFortemiIndexExport({
425
- schema_version: "aiwg.fortemi.index.export.v1",
1187
+ schema_version: manifest?.source_export_schema_version ?? "aiwg.fortemi.index.export.v1",
426
1188
  generated_at: manifest?.generated_at ?? "1970-01-01T00:00:00.000Z",
427
1189
  source: manifest?.source ?? { repo: "chunk", privacy: "public" },
1190
+ ...(manifest?.source_export_schema_version ?? "aiwg.fortemi.index.export.v1") === "aiwg.fortemi.index.export.v2" ? { compatibility: { previous_schema_version: "aiwg.fortemi.index.export.v1", strategy: "supported" } } : {},
428
1191
  items: data.items
429
1192
  });
430
1193
  errors.push(...validation.errors.map((error) => "items." + error));
@@ -511,15 +1274,17 @@ function recordTitle(item) {
511
1274
  }
512
1275
  function recordText(item) {
513
1276
  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") ?? "" : "";
1277
+ const extractedText = binarySourceText(item);
515
1278
  return [base, extractedText].filter(Boolean).join("\n");
516
1279
  }
1280
+ function binarySourceText(item) {
1281
+ return "binary_sources" in item ? item.binary_sources?.map((source) => source.extracted_text).filter(Boolean).join("\n") ?? "" : "";
1282
+ }
517
1283
  function defaultEmbeddingInput(record, granularity) {
518
1284
  const title = recordTitle(record);
519
1285
  const text = recordText(record);
520
1286
  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");
1287
+ return [title, record.search?.summary ?? "", binarySourceText(record)].filter(Boolean).join("\n");
523
1288
  }
524
1289
  return [title, text].filter(Boolean).join("\n");
525
1290
  }
@@ -587,6 +1352,7 @@ function recordSearchValues(item) {
587
1352
  return values.filter((value) => typeof value === "string" && value.length > 0);
588
1353
  }
589
1354
  function buildAiwgChunkedIndex(index, options = {}) {
1355
+ assertAiwgFortemiIndexExport(index);
590
1356
  const partSize = hasPositiveInteger(options.partSize) ? options.partSize : 500;
591
1357
  const projection = options.projection;
592
1358
  const idEncoding = options.idEncoding ?? "base64url";
@@ -666,35 +1432,88 @@ function queryMatches(item, q) {
666
1432
  return matches;
667
1433
  }
668
1434
  var DISCOVERY_STOPWORDS = /* @__PURE__ */ new Set([
1435
+ "the",
669
1436
  "a",
670
1437
  "an",
671
1438
  "and",
672
- "are",
673
- "as",
1439
+ "or",
1440
+ "of",
674
1441
  "for",
675
- "from",
676
- "how",
677
- "i",
1442
+ "to",
678
1443
  "in",
1444
+ "on",
1445
+ "with",
1446
+ "into",
1447
+ "from",
679
1448
  "is",
1449
+ "are",
1450
+ "be",
1451
+ "i",
1452
+ "we",
1453
+ "my",
1454
+ "it",
1455
+ "you",
680
1456
  "me",
681
- "of",
682
- "on",
683
- "or",
1457
+ "us",
1458
+ "your",
1459
+ "our",
1460
+ "this",
1461
+ "that",
1462
+ "these",
1463
+ "those",
1464
+ "there",
1465
+ "here",
1466
+ "some",
1467
+ "any",
1468
+ "all",
1469
+ "also",
684
1470
  "please",
685
- "the",
686
- "to",
687
- "use",
688
- "with"
1471
+ "about",
1472
+ "how",
1473
+ "what",
1474
+ "which",
1475
+ "where",
1476
+ "when",
1477
+ "who",
1478
+ "why",
1479
+ "find",
1480
+ "give",
1481
+ "show",
1482
+ "need",
1483
+ "want",
1484
+ "looking",
1485
+ "look",
1486
+ "help",
1487
+ "do",
1488
+ "does",
1489
+ "did",
1490
+ "can",
1491
+ "could",
1492
+ "should",
1493
+ "would",
1494
+ "will",
1495
+ "handle",
1496
+ "handles",
1497
+ "handling",
1498
+ "aiwg",
1499
+ "skill",
1500
+ "skills",
1501
+ "agent",
1502
+ "agents",
1503
+ "command",
1504
+ "commands",
1505
+ "rule",
1506
+ "rules",
1507
+ "flow",
1508
+ "flows",
1509
+ "workflow",
1510
+ "workflows"
689
1511
  ]);
690
- function normalizeDiscoveryText(value) {
691
- return value.toLowerCase().replace(/[_/]+/g, " ").replace(/[^a-z0-9.-]+/g, " ").trim();
692
- }
693
- function canonicalDiscoveryName(value) {
694
- return normalizeDiscoveryText(value).replace(/[\s.-]+/g, "");
1512
+ function normalizeDiscoveryName(value) {
1513
+ return value.toLowerCase().replace(/[-_\s]+/g, " ").trim();
695
1514
  }
696
1515
  function discoveryTokens(value) {
697
- return normalizeDiscoveryText(value).split(/\s+/).filter((token) => token.length > 1 && !DISCOVERY_STOPWORDS.has(token));
1516
+ return value.toLowerCase().split(/[^a-z0-9-]+/).filter((token) => token.length > 1 && !DISCOVERY_STOPWORDS.has(token));
698
1517
  }
699
1518
  function facetValues(item, names) {
700
1519
  return names.flatMap((name) => item.facets[name] ?? []);
@@ -704,17 +1523,58 @@ function addDiscoveryMatch(matches, match) {
704
1523
  matches.push(match);
705
1524
  }
706
1525
  }
707
- function tokenOverlapScore(tokens, value) {
708
- if (tokens.length === 0 || !value) return 0;
709
- const normalized = normalizeDiscoveryText(value);
710
- const hits = tokens.filter((token) => normalized.includes(token)).length;
711
- return hits / tokens.length;
1526
+ function damerauLevenshteinAtMostOne(left, right) {
1527
+ if (left === right) return true;
1528
+ if (Math.abs(left.length - right.length) > 1) return false;
1529
+ if (left.length === right.length) {
1530
+ let firstDiff = -1;
1531
+ let diffCount = 0;
1532
+ for (let index = 0; index < left.length; index += 1) {
1533
+ if (left[index] !== right[index]) {
1534
+ if (firstDiff < 0) firstDiff = index;
1535
+ diffCount += 1;
1536
+ }
1537
+ }
1538
+ if (diffCount === 1) return true;
1539
+ return diffCount === 2 && firstDiff + 1 < left.length && left[firstDiff] === right[firstDiff + 1] && left[firstDiff + 1] === right[firstDiff];
1540
+ }
1541
+ const shorter = left.length < right.length ? left : right;
1542
+ const longer = left.length < right.length ? right : left;
1543
+ let shorterIndex = 0;
1544
+ let longerIndex = 0;
1545
+ let edits = 0;
1546
+ while (shorterIndex < shorter.length && longerIndex < longer.length) {
1547
+ if (shorter[shorterIndex] === longer[longerIndex]) {
1548
+ shorterIndex += 1;
1549
+ longerIndex += 1;
1550
+ } else {
1551
+ edits += 1;
1552
+ if (edits > 1) return false;
1553
+ longerIndex += 1;
1554
+ }
1555
+ }
1556
+ return true;
712
1557
  }
713
- function discoveryMatches(item, query) {
1558
+ function nearDiscoveryNameMatch(query, name) {
1559
+ const queryParts = normalizeDiscoveryName(query).split(/\s+/).filter(Boolean);
1560
+ const nameParts = normalizeDiscoveryName(name).split(/\s+/).filter(Boolean);
1561
+ if (queryParts.length !== nameParts.length) return false;
1562
+ return queryParts.every((part, index) => {
1563
+ const target = nameParts[index];
1564
+ if (part === target) return true;
1565
+ if (part.length < 5 || target.length < 5) return false;
1566
+ return damerauLevenshteinAtMostOne(part, target);
1567
+ });
1568
+ }
1569
+ function lowerValues(values) {
1570
+ return values.map((value) => value.toLowerCase());
1571
+ }
1572
+ function discoveryMatches(item, query, options = {}) {
714
1573
  if (!query) return [];
715
1574
  const matches = [];
716
1575
  const tokens = discoveryTokens(query);
717
- const canonicalQuery = canonicalDiscoveryName(query);
1576
+ const lower = tokens.length > 0 ? tokens.join(" ") : query.toLowerCase().trim();
1577
+ const rawLower = query.toLowerCase().trim();
718
1578
  const idParts = item.id.split(/[:/]/);
719
1579
  const names = [
720
1580
  item.id,
@@ -740,33 +1600,66 @@ function discoveryMatches(item, query) {
740
1600
  ...item.tags
741
1601
  ].filter((value) => hasString(value));
742
1602
  const sourceValues = [item.source?.path, item.source?.repo_relative_path, item.source?.locator].filter((value) => hasString(value));
1603
+ const title = recordTitle(item);
1604
+ const summary = [item.search?.summary, recordText(item)].filter((value) => hasString(value)).join("\n");
1605
+ const type = item.search?.type ?? item.type;
1606
+ const tags = [...item.search?.tags ?? [], ...item.tags];
1607
+ const useMultiToken = tokens.length > 1;
1608
+ const minHits = useMultiToken ? options.relaxOverlap ? 1 : Math.ceil(tokens.length / 2) : 1;
1609
+ const overlapOK = (hits) => useMultiToken && hits >= minHits;
1610
+ const addInfo = (match) => addDiscoveryMatch(matches, { ...match, score: 0 });
1611
+ const addScore = (score2) => {
1612
+ if (score2 > 0) addDiscoveryMatch(matches, { field: "id", value: item.id, score: score2, reason: "aiwg discovery score" });
1613
+ };
743
1614
  for (const name of names) {
744
- const canonicalName = canonicalDiscoveryName(name);
745
- if (!canonicalName) continue;
746
- if (canonicalName === canonicalQuery) {
747
- addDiscoveryMatch(matches, { field: "id", value: name, score: 80, reason: "exact canonical name" });
748
- } else if (canonicalName.includes(canonicalQuery) || canonicalQuery.includes(canonicalName)) {
749
- addDiscoveryMatch(matches, { field: "id", value: name, score: 48, reason: "near canonical name" });
1615
+ if (normalizeDiscoveryName(query) === normalizeDiscoveryName(name)) {
1616
+ addInfo({ field: "id", value: name, reason: "exact canonical name" });
1617
+ addScore(1.001);
1618
+ return matches;
1619
+ }
1620
+ if (nearDiscoveryNameMatch(query, name)) {
1621
+ addInfo({ field: "id", value: name, reason: "near canonical name" });
1622
+ addScore(0.951);
1623
+ return matches;
750
1624
  }
751
1625
  }
752
- const title = recordTitle(item);
753
- const titleOverlap = tokenOverlapScore(tokens, title);
754
- if (titleOverlap > 0) addDiscoveryMatch(matches, { field: "title", value: title, score: 18 * titleOverlap, reason: "title token overlap" });
755
- for (const trigger of triggers) {
756
- const overlap = tokenOverlapScore(tokens, trigger);
757
- if (overlap > 0) addDiscoveryMatch(matches, { field: "facet", value: trigger, score: 34 * overlap, reason: "trigger phrase" });
1626
+ let score = 0;
1627
+ const scoreText = (field, value, reason, exactScore, overlapScore, weight, exactBonus = 0) => {
1628
+ const normalized = value.toLowerCase();
1629
+ if (normalized.includes(lower)) {
1630
+ score += exactScore * weight;
1631
+ if (normalized === lower) score += exactBonus;
1632
+ addInfo({ field, value, reason });
1633
+ } else if (useMultiToken) {
1634
+ const hits = tokens.filter((token) => normalized.includes(token)).length;
1635
+ if (overlapOK(hits)) {
1636
+ score += overlapScore * weight * (hits / tokens.length);
1637
+ addInfo({ field, value, reason });
1638
+ }
1639
+ }
1640
+ };
1641
+ for (const trigger of lowerValues(triggers)) {
1642
+ if (trigger === lower || trigger === rawLower) {
1643
+ addInfo({ field: "facet", value: trigger, reason: "trigger phrase" });
1644
+ addScore(1.0008);
1645
+ return matches;
1646
+ }
758
1647
  }
759
- for (const capability of capabilities) {
760
- const overlap = tokenOverlapScore(tokens, capability);
761
- if (overlap > 0) addDiscoveryMatch(matches, { field: "concept", value: capability, score: 22 * overlap, reason: "capability overlap" });
1648
+ for (const trigger of triggers) {
1649
+ scoreText("facet", trigger, "trigger phrase", 0.25, 0.06, 4);
762
1650
  }
763
- const text = recordText(item);
764
- const textOverlap = tokenOverlapScore(tokens, text);
765
- if (textOverlap > 0) addDiscoveryMatch(matches, { field: "text", value: text, score: 8 * textOverlap, reason: "body token overlap" });
1651
+ for (const capability of capabilities) scoreText("concept", capability, "capability overlap", 0.2, 0.1, 2);
1652
+ scoreText("title", title, "title token overlap", 0.3, 0.08, 3, 0.2);
1653
+ for (const tag of tags) scoreText("tag", tag, "tag token overlap", 0.2, 0.05, 2);
1654
+ if (summary) scoreText("text", summary, "body token overlap", 0.15, 0.04, 1);
766
1655
  for (const source of sourceValues) {
767
- const overlap = tokenOverlapScore(tokens, source);
768
- if (overlap > 0) addDiscoveryMatch(matches, { field: "source", value: source, score: 2 * overlap, reason: "path overlap" });
1656
+ scoreText("source", source, "path overlap", 0.1, 0.03, 1);
769
1657
  }
1658
+ if (type.toLowerCase().includes(lower)) {
1659
+ score += 0.1;
1660
+ addInfo({ field: "facet", value: type, reason: "type overlap" });
1661
+ }
1662
+ addScore(Math.min(score, 1));
770
1663
  return matches;
771
1664
  }
772
1665
  function rankMatches(matches, weights) {
@@ -792,13 +1685,13 @@ function createSnippet(item, matches, q, maxLength) {
792
1685
  const firstMatch = textMatch ?? titleMatch ?? matches[0];
793
1686
  return clipSnippet(firstMatch?.value ?? recordText(item), q, maxLength);
794
1687
  }
795
- function createRankedEntries(items, q, options, ordinalBase = 0) {
1688
+ function createRankedEntries(items, q, options, ordinalBase = 0, discoveryOptions = {}) {
796
1689
  const weights = { ...DEFAULT_QUERY_WEIGHTS, ...options.weights };
797
1690
  const profile = options.searchProfile ?? "default";
798
1691
  return items.map((item, ordinal) => ({
799
1692
  item,
800
1693
  ordinal: ordinalBase + ordinal,
801
- matches: profile === "aiwg-discovery" ? discoveryMatches(item, q) : queryMatches(item, q)
1694
+ matches: profile === "aiwg-discovery" ? discoveryMatches(item, q, discoveryOptions) : queryMatches(item, q)
802
1695
  })).filter(({ item, matches }) => {
803
1696
  if (q && matches.length === 0) return false;
804
1697
  if (options.types && !options.types.includes(item.type)) return false;
@@ -848,8 +1741,7 @@ function queryAiwgFortemiIndex(index, query = "", options = {}) {
848
1741
  const q = query.trim().toLowerCase();
849
1742
  const entries = createRankedEntries(index.items, q, options);
850
1743
  if (entries.length === 0 && q && options.searchProfile === "aiwg-discovery") {
851
- const relaxed = discoveryTokens(q).join(" ");
852
- return createQueryResultFromRankedEntries(createRankedEntries(index.items, relaxed, options), relaxed, options);
1744
+ return createQueryResultFromRankedEntries(createRankedEntries(index.items, q, options, 0, { relaxOverlap: true }), q, options);
853
1745
  }
854
1746
  return createQueryResultFromRankedEntries(entries, q, options);
855
1747
  }
@@ -870,7 +1762,8 @@ function cosineSimilarity(left, right) {
870
1762
  }
871
1763
  function validateAiwgStaticEmbeddingSet(value) {
872
1764
  const errors = [];
873
- const data = value;
1765
+ const data = isPlainRecord(value) ? value : {};
1766
+ if (!isPlainRecord(value)) errors.push("embedding set must be an object");
874
1767
  if (data?.schema_version !== "aiwg.fortemi.embedding.set.v1") errors.push("schema_version must be aiwg.fortemi.embedding.set.v1");
875
1768
  if (!hasString(data?.id)) errors.push("id is required");
876
1769
  if (!hasString(data?.model)) errors.push("model is required");
@@ -878,7 +1771,12 @@ function validateAiwgStaticEmbeddingSet(value) {
878
1771
  if (!hasString(data?.generated_at)) errors.push("generated_at is required");
879
1772
  if (!hasString(data?.granularity)) errors.push("granularity is required");
880
1773
  if (!Array.isArray(data?.embeddings)) errors.push("embeddings must be an array");
881
- for (const [index, embedding] of (data.embeddings ?? []).entries()) {
1774
+ const embeddings = Array.isArray(data.embeddings) ? data.embeddings : [];
1775
+ for (const [index, embedding] of embeddings.entries()) {
1776
+ if (!isPlainRecord(embedding)) {
1777
+ errors.push("embeddings[" + index + "] must be an object");
1778
+ continue;
1779
+ }
882
1780
  if (!hasString(embedding.record_id)) errors.push("embeddings[" + index + "].record_id is required");
883
1781
  if (!hasString(embedding.input_hash)) errors.push("embeddings[" + index + "].input_hash is required");
884
1782
  if (!Array.isArray(embedding.embedding)) errors.push("embeddings[" + index + "].embedding must be an array");
@@ -908,7 +1806,10 @@ function queryAiwgSemanticIndex(index, embeddingSet, queryEmbedding, options = {
908
1806
  }).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);
909
1807
  }
910
1808
  function queryAiwgHybridIndex(index, embeddingSet, query, queryEmbedding, options = {}) {
911
- const lexical = queryAiwgFortemiIndex(index, query, { ...options, rank: true });
1809
+ const lexicalOptions = { ...options };
1810
+ delete lexicalOptions.limit;
1811
+ delete lexicalOptions.offset;
1812
+ const lexical = queryAiwgFortemiIndex(index, query, { ...lexicalOptions, rank: true });
912
1813
  const semantic = queryAiwgSemanticIndex(index, embeddingSet, queryEmbedding, { limit: index.items.length });
913
1814
  const lexicalWeight = options.lexicalWeight ?? 0.5;
914
1815
  const semanticWeight = options.semanticWeight ?? 0.5;
@@ -978,6 +1879,7 @@ function matchSetCacheKey(q, options) {
978
1879
  concepts: options.concepts ?? null,
979
1880
  privacy: options.privacy ?? null,
980
1881
  rel: options.relationshipTargetId ?? null,
1882
+ searchProfile: options.searchProfile ?? "default",
981
1883
  weights: { ...DEFAULT_QUERY_WEIGHTS, ...options.weights }
982
1884
  });
983
1885
  }
@@ -1066,15 +1968,21 @@ function edgeFromRelationship(sourceId, relationship) {
1066
1968
  }
1067
1969
  function relationshipMatches(edge, options = {}) {
1068
1970
  const type = relationshipTypeFilter(options);
1069
- const direction = options.direction ?? "both";
1070
1971
  if (type && edge.type !== type) return false;
1071
1972
  if (options.relationshipDirection && edge.direction !== options.relationshipDirection) return false;
1072
1973
  if (options.sourceId && edge.source_id !== options.sourceId) return false;
1073
1974
  if (options.targetId && edge.target_id !== options.targetId) return false;
1074
- if (direction === "out" && options.targetId && edge.target_id !== options.targetId) return false;
1075
- if (direction === "in" && options.sourceId && edge.source_id !== options.sourceId) return false;
1975
+ if (options.endpointId && edge.source_id !== options.endpointId && edge.target_id !== options.endpointId) return false;
1076
1976
  return true;
1077
1977
  }
1978
+ function assertRelationshipDirectionAnchor(options) {
1979
+ if (options.direction === "out" && !options.sourceId) {
1980
+ throw new Error("relationship direction 'out' requires sourceId");
1981
+ }
1982
+ if (options.direction === "in" && !options.targetId) {
1983
+ throw new Error("relationship direction 'in' requires targetId");
1984
+ }
1985
+ }
1078
1986
  function nodeSummary(item) {
1079
1987
  return { id: item.id, type: item.type, title: recordTitle(item) };
1080
1988
  }
@@ -1082,6 +1990,7 @@ function addNode(nodes, item) {
1082
1990
  if (item) nodes.set(item.id, nodeSummary(item));
1083
1991
  }
1084
1992
  function relationshipResultFromRecords(records, options = {}) {
1993
+ assertRelationshipDirectionAnchor(options);
1085
1994
  const byId = new Map(records.map((record) => [record.id, record]));
1086
1995
  const edges = [];
1087
1996
  for (const record of records) {
@@ -1091,7 +2000,8 @@ function relationshipResultFromRecords(records, options = {}) {
1091
2000
  edges.push(edge);
1092
2001
  }
1093
2002
  }
1094
- const limitedEdges = (options.limit ? edges.slice(0, options.limit) : edges).sort((left, right) => left.source_id.localeCompare(right.source_id) || left.target_id.localeCompare(right.target_id) || left.type.localeCompare(right.type));
2003
+ const sortedEdges = edges.sort((left, right) => left.source_id.localeCompare(right.source_id) || left.target_id.localeCompare(right.target_id) || left.type.localeCompare(right.type));
2004
+ const limitedEdges = options.limit ? sortedEdges.slice(0, options.limit) : sortedEdges;
1095
2005
  const nodes = /* @__PURE__ */ new Map();
1096
2006
  for (const edge of limitedEdges) {
1097
2007
  addNode(nodes, byId.get(edge.source_id));
@@ -1108,7 +2018,8 @@ function neighborQueryOptions(id, options = {}) {
1108
2018
  return {
1109
2019
  ...options,
1110
2020
  ...direction === "out" ? { sourceId: id } : {},
1111
- ...direction === "in" ? { targetId: id } : {}
2021
+ ...direction === "in" ? { targetId: id } : {},
2022
+ ...direction === "both" ? { endpointId: id } : {}
1112
2023
  };
1113
2024
  }
1114
2025
  function filterNeighborResult(id, result, options = {}) {
@@ -1417,13 +2328,14 @@ function createAiwgIndexController(initialIndex) {
1417
2328
  }
1418
2329
  function aiwgFortemiIndexToCommunityGraph(index, options = {}) {
1419
2330
  const ids = new Set(index.items.map((item) => item.id));
1420
- const relationshipWeights = options.relationshipWeights ?? {};
2331
+ const relationshipWeights = options.relationshipWeights ?? /* @__PURE__ */ Object.create(null);
1421
2332
  const edgeCounts = /* @__PURE__ */ new Map();
1422
2333
  for (const item of index.items) {
1423
2334
  for (const relationship of item.relationships) {
1424
2335
  if (!ids.has(relationship.target_id) && !options.includeDanglingRelationships) continue;
1425
2336
  const kind = relationship.type;
1426
- const baseWeight = relationshipWeights[kind] ?? 1;
2337
+ const configuredWeight = Object.prototype.hasOwnProperty.call(relationshipWeights, kind) ? relationshipWeights[kind] : void 0;
2338
+ const baseWeight = typeof configuredWeight === "number" && Number.isFinite(configuredWeight) ? configuredWeight : 1;
1427
2339
  const key = `${item.id}\0${relationship.target_id}\0${kind}`;
1428
2340
  const existing = edgeCounts.get(key);
1429
2341
  if (existing) existing.weight += baseWeight;