@fortemi/core 2026.7.2 → 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",
@@ -19,6 +735,16 @@ var AIWG_SCAN_REQUIRED_FIELDS = [
19
735
  "concepts",
20
736
  "privacy"
21
737
  ];
738
+ function isPrivacyExcluded(record, options) {
739
+ const privacy = record.privacy;
740
+ if (!privacy || !isPrivacyClassification(privacy.classification) || typeof privacy.pii !== "boolean") return true;
741
+ if (privacy.classification === "private" && !options?.includePrivate) return true;
742
+ if (privacy.pii && !options?.includePii) return true;
743
+ return false;
744
+ }
745
+ function filterAiwgRecordsByPrivacy(records, options) {
746
+ return records.filter((record) => !isPrivacyExcluded(record, options));
747
+ }
22
748
  var REQUIRED_RECORD_FIELDS = [
23
749
  "schema_version",
24
750
  "id",
@@ -45,8 +771,12 @@ function hasString(value) {
45
771
  return typeof value === "string" && value.length > 0;
46
772
  }
47
773
  function pushFacet(counts, name, value) {
48
- counts[name] ??= {};
49
- counts[name][value] = (counts[name][value] ?? 0) + 1;
774
+ let bucket = counts[name];
775
+ if (bucket === void 0) {
776
+ bucket = /* @__PURE__ */ Object.create(null);
777
+ counts[name] = bucket;
778
+ }
779
+ bucket[value] = (bucket[value] ?? 0) + 1;
50
780
  }
51
781
  function hasNonNegativeInteger(value) {
52
782
  return Number.isInteger(value) && typeof value === "number" && value >= 0;
@@ -76,6 +806,10 @@ function validateOptionalRichMetadata(item, index, errors) {
76
806
  errors.push("items[" + index + "].skos_concepts must be an array when present");
77
807
  } else {
78
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
+ }
79
813
  if (!hasString(concept.id)) errors.push("items[" + index + "].skos_concepts[" + conceptIndex + "].id is required");
80
814
  if (!hasString(concept.prefLabel)) errors.push("items[" + index + "].skos_concepts[" + conceptIndex + "].prefLabel is required");
81
815
  if (!isOptionalStringArray(concept.altLabels)) errors.push("items[" + index + "].skos_concepts[" + conceptIndex + "].altLabels must be a string array");
@@ -90,6 +824,10 @@ function validateOptionalRichMetadata(item, index, errors) {
90
824
  errors.push("items[" + index + "].skos_relations must be an array when present");
91
825
  } else {
92
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
+ }
93
831
  if (!hasString(relation.type)) errors.push("items[" + index + "].skos_relations[" + relationIndex + "].type is required");
94
832
  if (!hasString(relation.source_id)) errors.push("items[" + index + "].skos_relations[" + relationIndex + "].source_id is required");
95
833
  if (!hasString(relation.target_id)) errors.push("items[" + index + "].skos_relations[" + relationIndex + "].target_id is required");
@@ -104,6 +842,10 @@ function validateOptionalRichMetadata(item, index, errors) {
104
842
  errors.push("items[" + index + "].provenance_events must be an array when present");
105
843
  } else {
106
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
+ }
107
849
  if (!hasString(event.activity)) errors.push("items[" + index + "].provenance_events[" + eventIndex + "].activity is required");
108
850
  if (event.attributes !== void 0 && !isPlainRecord(event.attributes)) {
109
851
  errors.push("items[" + index + "].provenance_events[" + eventIndex + "].attributes must be an object");
@@ -113,6 +855,10 @@ function validateOptionalRichMetadata(item, index, errors) {
113
855
  }
114
856
  if (Array.isArray(item.relationships)) {
115
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
+ }
116
862
  if (relationship.metadata !== void 0 && !isPlainRecord(relationship.metadata)) {
117
863
  errors.push("items[" + index + "].relationships[" + relationshipIndex + "].metadata must be an object");
118
864
  }
@@ -141,7 +887,10 @@ function validateOptionalRichMetadata(item, index, errors) {
141
887
  errors.push("items[" + index + "].chunks must be an array when present");
142
888
  } else {
143
889
  for (const [chunkIndex, chunk] of item.chunks.entries()) {
144
- 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
+ }
145
894
  if (chunk.metadata !== void 0 && !isPlainRecord(chunk.metadata)) {
146
895
  errors.push("items[" + index + "].chunks[" + chunkIndex + "].metadata must be an object");
147
896
  }
@@ -153,7 +902,10 @@ function validateOptionalRichMetadata(item, index, errors) {
153
902
  errors.push("items[" + index + "].embeddings must be an array when present");
154
903
  } else {
155
904
  for (const [embeddingIndex, embedding] of item.embeddings.entries()) {
156
- 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
+ }
157
909
  const vector = embedding.embedding ?? embedding.vector;
158
910
  if (vector !== void 0 && (!Array.isArray(vector) || !vector.every((entry) => typeof entry === "number"))) {
159
911
  errors.push("items[" + index + "].embeddings[" + embeddingIndex + "].embedding/vector must be a number array");
@@ -168,23 +920,91 @@ function validateOptionalRichMetadata(item, index, errors) {
168
920
  errors.push("items[" + index + "].compatibility must be an object");
169
921
  }
170
922
  }
923
+ function isPrivacyClassification(value) {
924
+ return value === "private" || value === "sanitized" || value === "public";
925
+ }
926
+ function isProvenanceConfidence(value) {
927
+ return value === "source" || value === "candidate" || value === "reviewed" || value === "rejected";
928
+ }
929
+ function validateProvenanceItems(item, index, errors) {
930
+ if (!Array.isArray(item.provenance)) return;
931
+ for (const [provIndex, prov] of item.provenance.entries()) {
932
+ const at = "items[" + index + "].provenance[" + provIndex + "]";
933
+ if (!isPlainRecord(prov)) {
934
+ errors.push(at + " must be an object");
935
+ continue;
936
+ }
937
+ if (!hasString(prov.field)) errors.push(at + ".field is required");
938
+ if (!hasString(prov.source)) errors.push(at + ".source is required");
939
+ if (!hasString(prov.path)) errors.push(at + ".path is required");
940
+ if (!isProvenanceConfidence(prov.confidence)) errors.push(at + ".confidence must be one of source, candidate, reviewed, rejected");
941
+ if (!isPrivacyClassification(prov.privacy)) errors.push(at + ".privacy must be one of private, sanitized, public");
942
+ }
943
+ }
944
+ var V2_ONLY_RECORD_FIELDS = ["search", "chunks", "embeddings", "skos_concepts", "skos_relations", "compatibility"];
945
+ var V2_ONLY_SOURCE_FIELDS = ["origin", "generated", "checksum", "updated_at"];
946
+ var V2_ONLY_RELATIONSHIP_FIELDS = ["target_path", "direction", "metadata"];
947
+ function forbidV2FieldsOnV1Record(item, index, errors) {
948
+ if (item.schema_version !== "aiwg.fortemi.index.record.v1") return;
949
+ const at = "items[" + index + "]";
950
+ const bag = item;
951
+ const v2msg = " is a v2-only field and must be absent on a record.v1 record";
952
+ for (const field of V2_ONLY_RECORD_FIELDS) {
953
+ if (bag[field] !== void 0) errors.push(at + "." + field + v2msg);
954
+ }
955
+ if (isPlainRecord(item.source)) {
956
+ const src = item.source;
957
+ for (const field of V2_ONLY_SOURCE_FIELDS) {
958
+ if (src[field] !== void 0) errors.push(at + ".source." + field + v2msg);
959
+ }
960
+ }
961
+ if (isPlainRecord(item.privacy) && item.privacy.locality !== void 0) {
962
+ errors.push(at + ".privacy.locality" + v2msg);
963
+ }
964
+ if (Array.isArray(item.relationships)) {
965
+ for (const [relIndex, rel] of item.relationships.entries()) {
966
+ if (!isPlainRecord(rel)) continue;
967
+ const relBag = rel;
968
+ for (const field of V2_ONLY_RELATIONSHIP_FIELDS) {
969
+ if (relBag[field] !== void 0) errors.push(at + ".relationships[" + relIndex + "]." + field + v2msg);
970
+ }
971
+ }
972
+ }
973
+ }
171
974
  function validateAiwgFortemiIndexExport(value) {
172
- const errors = [];
173
- const counts = {};
174
- 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");
175
979
  if (!isSupportedIndexSchemaVersion(data?.schema_version)) {
176
980
  errors.push("schema_version must be aiwg.fortemi.index.export.v1 or aiwg.fortemi.index.export.v2");
177
981
  }
178
982
  if (!hasString(data?.generated_at)) errors.push("generated_at is required");
179
983
  if (!hasString(data?.source?.repo)) errors.push("source.repo is required");
180
984
  if (!hasString(data?.source?.privacy)) errors.push("source.privacy is required");
985
+ else if (!isPrivacyClassification(data?.source?.privacy)) {
986
+ errors.push("source.privacy must be one of private, sanitized, public");
987
+ }
181
988
  if (!Array.isArray(data?.items)) errors.push("items must be an array");
182
989
  if (data.compatibility !== void 0 && !isPlainRecord(data.compatibility)) {
183
990
  errors.push("compatibility must be an object");
184
991
  }
992
+ if (data?.schema_version === "aiwg.fortemi.index.export.v1") {
993
+ if (isPlainRecord(data.source) && data.source.graph !== void 0) {
994
+ errors.push("source.graph is a v2-only field and must be absent on an export.v1 export");
995
+ }
996
+ if (data.compatibility !== void 0) {
997
+ errors.push("compatibility is a v2-only field and must be absent on an export.v1 export");
998
+ }
999
+ }
185
1000
  const ids = /* @__PURE__ */ new Set();
186
1001
  let previousId = "";
187
- 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
+ }
188
1008
  for (const field of REQUIRED_RECORD_FIELDS) {
189
1009
  if (!(field in item)) errors.push("items[" + index + "]." + field + " is required");
190
1010
  }
@@ -194,7 +1014,7 @@ function validateAiwgFortemiIndexExport(value) {
194
1014
  if (!hasString(item.id)) errors.push("items[" + index + "].id is required");
195
1015
  if (hasString(item.id) && ids.has(item.id)) errors.push("duplicate id: " + item.id);
196
1016
  if (hasString(item.id)) ids.add(item.id);
197
- if (previousId && hasString(item.id) && previousId.localeCompare(item.id) > 0) {
1017
+ if (previousId && hasString(item.id) && previousId > item.id) {
198
1018
  errors.push("items must be sorted by id: " + previousId + " before " + item.id);
199
1019
  }
200
1020
  if (hasString(item.id)) previousId = item.id;
@@ -216,8 +1036,12 @@ function validateAiwgFortemiIndexExport(value) {
216
1036
  errors.push("items[" + index + "].provenance must be a non-empty array");
217
1037
  }
218
1038
  validateOptionalRichMetadata(item, index, errors);
1039
+ validateProvenanceItems(item, index, errors);
1040
+ forbidV2FieldsOnV1Record(item, index, errors);
219
1041
  if (!item.privacy || typeof item.privacy.pii !== "boolean" || !hasString(item.privacy.classification)) {
220
1042
  errors.push("items[" + index + "].privacy requires classification and pii");
1043
+ } else if (!isPrivacyClassification(item.privacy.classification)) {
1044
+ errors.push("items[" + index + "].privacy.classification must be one of private, sanitized, public");
221
1045
  }
222
1046
  }
223
1047
  return { valid: errors.length === 0, errors, counts };
@@ -231,13 +1055,17 @@ function assertAiwgFortemiIndexExport(value) {
231
1055
  }
232
1056
  function validateAiwgFortemiChunkManifest(value) {
233
1057
  const errors = [];
234
- const data = value;
1058
+ const data = isPlainRecord(value) ? value : {};
1059
+ if (!isPlainRecord(value)) errors.push("chunk manifest must be an object");
235
1060
  if (data?.schema_version !== "aiwg.fortemi.index.chunk-manifest.v1") {
236
1061
  errors.push("schema_version must be aiwg.fortemi.index.chunk-manifest.v1");
237
1062
  }
238
1063
  if (!hasString(data?.generated_at)) errors.push("generated_at is required");
239
1064
  if (!hasString(data?.source?.repo)) errors.push("source.repo is required");
240
1065
  if (!hasString(data?.source?.privacy)) errors.push("source.privacy is required");
1066
+ if (data?.source_export_schema_version !== void 0 && !isSupportedIndexSchemaVersion(data.source_export_schema_version)) {
1067
+ errors.push("source_export_schema_version must be aiwg.fortemi.index.export.v1 or aiwg.fortemi.index.export.v2 when present");
1068
+ }
241
1069
  if (!hasNonNegativeInteger(data?.total)) errors.push("total must be a non-negative integer");
242
1070
  if (!hasPositiveInteger(data?.part_size)) errors.push("part_size must be a positive integer");
243
1071
  if (data.facets !== void 0 && !isFacetCounts(data.facets)) {
@@ -253,7 +1081,9 @@ function validateAiwgFortemiChunkManifest(value) {
253
1081
  }
254
1082
  }
255
1083
  }
256
- 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) {
257
1087
  if (!hasString(data.detail.href)) errors.push("detail.href is required");
258
1088
  else if (!data.detail.href.includes("{id}")) errors.push("detail.href must contain the {id} placeholder");
259
1089
  if (data.detail.encoding !== void 0 && data.detail.encoding !== "uri" && data.detail.encoding !== "base64url") {
@@ -264,6 +1094,10 @@ function validateAiwgFortemiChunkManifest(value) {
264
1094
  let expectedOffset = 0;
265
1095
  const parts = Array.isArray(data?.parts) ? data.parts : [];
266
1096
  for (const [index, part] of parts.entries()) {
1097
+ if (!isPlainRecord(part)) {
1098
+ errors.push("parts[" + index + "] must be an object");
1099
+ continue;
1100
+ }
267
1101
  if (!hasString(part.href)) errors.push("parts[" + index + "].href is required");
268
1102
  if (!hasNonNegativeInteger(part.offset)) errors.push("parts[" + index + "].offset must be a non-negative integer");
269
1103
  if (!hasNonNegativeInteger(part.count)) errors.push("parts[" + index + "].count must be a non-negative integer");
@@ -284,18 +1118,28 @@ function assertAiwgFortemiChunkManifest(value) {
284
1118
  }
285
1119
  return value;
286
1120
  }
287
- function validateProjectedRecords(items) {
1121
+ function validateProjectedRecords(items, sourceSchemaVersion) {
288
1122
  const errors = [];
289
1123
  const ids = /* @__PURE__ */ new Set();
290
1124
  let previousId = "";
291
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
+ }
292
1136
  if (!isSupportedRecordSchemaVersion(item.schema_version)) {
293
1137
  errors.push("items[" + index + "].schema_version must be aiwg.fortemi.index.record.v1 or aiwg.fortemi.index.record.v2");
294
1138
  }
295
1139
  if (!hasString(item.id)) errors.push("items[" + index + "].id is required");
296
1140
  if (hasString(item.id) && ids.has(item.id)) errors.push("duplicate id: " + item.id);
297
1141
  if (hasString(item.id)) ids.add(item.id);
298
- if (previousId && hasString(item.id) && previousId.localeCompare(item.id) > 0) {
1142
+ if (previousId && hasString(item.id) && previousId > item.id) {
299
1143
  errors.push("items must be sorted by id: " + previousId + " before " + item.id);
300
1144
  }
301
1145
  if (hasString(item.id)) previousId = item.id;
@@ -311,15 +1155,13 @@ function validateProjectedRecords(items) {
311
1155
  }
312
1156
  if (!Array.isArray(item.tags)) errors.push("items[" + index + "].tags must be an array");
313
1157
  if (!Array.isArray(item.concepts)) errors.push("items[" + index + "].concepts must be an array");
314
- if (!item.privacy || !hasString(item.privacy.classification)) {
315
- errors.push("items[" + index + "].privacy.classification is required");
316
- }
317
1158
  }
318
1159
  return errors;
319
1160
  }
320
1161
  function validateAiwgFortemiChunkPart(value, partRef, manifest) {
321
1162
  const errors = [];
322
- const data = value;
1163
+ const data = isPlainRecord(value) ? value : {};
1164
+ if (!isPlainRecord(value)) errors.push("chunk part must be an object");
323
1165
  if (data?.schema_version !== "aiwg.fortemi.index.chunk.v1") {
324
1166
  errors.push("schema_version must be aiwg.fortemi.index.chunk.v1");
325
1167
  }
@@ -336,12 +1178,16 @@ function validateAiwgFortemiChunkPart(value, partRef, manifest) {
336
1178
  }
337
1179
  if (Array.isArray(data?.items)) {
338
1180
  if (manifest?.projection) {
339
- 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));
340
1185
  } else {
341
1186
  const validation = validateAiwgFortemiIndexExport({
342
- schema_version: "aiwg.fortemi.index.export.v1",
1187
+ schema_version: manifest?.source_export_schema_version ?? "aiwg.fortemi.index.export.v1",
343
1188
  generated_at: manifest?.generated_at ?? "1970-01-01T00:00:00.000Z",
344
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" } } : {},
345
1191
  items: data.items
346
1192
  });
347
1193
  errors.push(...validation.errors.map((error) => "items." + error));
@@ -356,9 +1202,35 @@ function assertAiwgFortemiChunkPart(value, partRef, manifest) {
356
1202
  }
357
1203
  return value;
358
1204
  }
1205
+ var ALLOWED_AIWG_FETCH_SCHEMES = /* @__PURE__ */ new Set(["http:", "https:", "blob:", "data:"]);
1206
+ function tryParseUrl(value) {
1207
+ try {
1208
+ return new URL(value);
1209
+ } catch {
1210
+ return null;
1211
+ }
1212
+ }
1213
+ function resolveAiwgFetchUrl(href, baseUrl) {
1214
+ if (baseUrl === void 0) {
1215
+ const absolute = tryParseUrl(href);
1216
+ if (absolute && !ALLOWED_AIWG_FETCH_SCHEMES.has(absolute.protocol)) {
1217
+ throw new Error("Refusing AIWG index fetch with disallowed scheme: " + absolute.protocol);
1218
+ }
1219
+ return href;
1220
+ }
1221
+ const base = new URL(baseUrl);
1222
+ const resolved = new URL(href, base);
1223
+ if (!ALLOWED_AIWG_FETCH_SCHEMES.has(resolved.protocol)) {
1224
+ throw new Error("Refusing AIWG index fetch with disallowed scheme: " + resolved.protocol);
1225
+ }
1226
+ if (resolved.origin !== base.origin) {
1227
+ throw new Error("Refusing cross-origin AIWG index fetch: " + resolved.origin + " != " + base.origin);
1228
+ }
1229
+ return resolved.toString();
1230
+ }
359
1231
  function createAiwgFetchChunkLoader(baseUrl) {
360
1232
  return async (part) => {
361
- const href = baseUrl ? new URL(part.href, baseUrl).toString() : part.href;
1233
+ const href = resolveAiwgFetchUrl(part.href, baseUrl);
362
1234
  const response = await fetch(href);
363
1235
  if (!response.ok) throw new Error("Failed to fetch AIWG index chunk " + href + ": " + response.status);
364
1236
  return response.json();
@@ -378,14 +1250,14 @@ function createAiwgFetchDetailLoader(baseUrl) {
378
1250
  return async (id, manifest) => {
379
1251
  if (!manifest.detail?.href) throw new Error("Manifest has no detail.href for record resolution");
380
1252
  const relative = aiwgDetailHrefForId(manifest.detail, id);
381
- const href = baseUrl ? new URL(relative, baseUrl).toString() : relative;
1253
+ const href = resolveAiwgFetchUrl(relative, baseUrl);
382
1254
  const response = await fetch(href);
383
1255
  if (!response.ok) throw new Error("Failed to fetch AIWG index detail " + href + ": " + response.status);
384
1256
  return response.json();
385
1257
  };
386
1258
  }
387
1259
  function getAiwgFortemiFacets(items) {
388
- const result = {};
1260
+ const result = /* @__PURE__ */ Object.create(null);
389
1261
  for (const item of items) {
390
1262
  pushFacet(result, "type", item.type);
391
1263
  pushFacet(result, "privacy", item.privacy.classification);
@@ -402,15 +1274,17 @@ function recordTitle(item) {
402
1274
  }
403
1275
  function recordText(item) {
404
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") ?? "";
405
- const extractedText = "binary_sources" in item ? item.binary_sources?.map((source) => source.extracted_text).filter(Boolean).join("\n") ?? "" : "";
1277
+ const extractedText = binarySourceText(item);
406
1278
  return [base, extractedText].filter(Boolean).join("\n");
407
1279
  }
1280
+ function binarySourceText(item) {
1281
+ return "binary_sources" in item ? item.binary_sources?.map((source) => source.extracted_text).filter(Boolean).join("\n") ?? "" : "";
1282
+ }
408
1283
  function defaultEmbeddingInput(record, granularity) {
409
1284
  const title = recordTitle(record);
410
1285
  const text = recordText(record);
411
1286
  if (granularity === "title-summary") {
412
- const extractedText = record.binary_sources?.map((source) => source.extracted_text).filter(Boolean).join("\n") ?? "";
413
- return [title, record.search?.summary ?? "", extractedText].filter(Boolean).join("\n");
1287
+ return [title, record.search?.summary ?? "", binarySourceText(record)].filter(Boolean).join("\n");
414
1288
  }
415
1289
  return [title, text].filter(Boolean).join("\n");
416
1290
  }
@@ -421,7 +1295,7 @@ function generatedAtString(value) {
421
1295
  async function buildAiwgStaticEmbeddingSet(index, options) {
422
1296
  assertAiwgFortemiIndexExport(index);
423
1297
  const granularity = options.granularity ?? "body";
424
- const records = options.records ?? index.items;
1298
+ const records = filterAiwgRecordsByPrivacy(options.records ?? index.items, options.privacy);
425
1299
  const embeddings = [];
426
1300
  for (const record of records) {
427
1301
  const input = options.textForRecord?.(record) ?? defaultEmbeddingInput(record, granularity);
@@ -478,11 +1352,12 @@ function recordSearchValues(item) {
478
1352
  return values.filter((value) => typeof value === "string" && value.length > 0);
479
1353
  }
480
1354
  function buildAiwgChunkedIndex(index, options = {}) {
1355
+ assertAiwgFortemiIndexExport(index);
481
1356
  const partSize = hasPositiveInteger(options.partSize) ? options.partSize : 500;
482
1357
  const projection = options.projection;
483
1358
  const idEncoding = options.idEncoding ?? "base64url";
484
1359
  const detailHref = options.detailHref ?? "detail/{id}.json";
485
- const items = index.items;
1360
+ const items = filterAiwgRecordsByPrivacy(index.items, options.privacy);
486
1361
  const pad = (value) => String(value).padStart(4, "0");
487
1362
  const project = (record) => {
488
1363
  if (!projection) return record;
@@ -510,6 +1385,7 @@ function buildAiwgChunkedIndex(index, options = {}) {
510
1385
  schema_version: "aiwg.fortemi.index.chunk-manifest.v1",
511
1386
  generated_at: options.generatedAt ?? index.generated_at,
512
1387
  source: index.source,
1388
+ source_export_schema_version: index.schema_version,
513
1389
  total: items.length,
514
1390
  part_size: partSize,
515
1391
  facets: getAiwgFortemiFacets(items),
@@ -556,35 +1432,88 @@ function queryMatches(item, q) {
556
1432
  return matches;
557
1433
  }
558
1434
  var DISCOVERY_STOPWORDS = /* @__PURE__ */ new Set([
1435
+ "the",
559
1436
  "a",
560
1437
  "an",
561
1438
  "and",
562
- "are",
563
- "as",
1439
+ "or",
1440
+ "of",
564
1441
  "for",
565
- "from",
566
- "how",
567
- "i",
1442
+ "to",
568
1443
  "in",
1444
+ "on",
1445
+ "with",
1446
+ "into",
1447
+ "from",
569
1448
  "is",
1449
+ "are",
1450
+ "be",
1451
+ "i",
1452
+ "we",
1453
+ "my",
1454
+ "it",
1455
+ "you",
570
1456
  "me",
571
- "of",
572
- "on",
573
- "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",
574
1470
  "please",
575
- "the",
576
- "to",
577
- "use",
578
- "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"
579
1511
  ]);
580
- function normalizeDiscoveryText(value) {
581
- return value.toLowerCase().replace(/[_/]+/g, " ").replace(/[^a-z0-9.-]+/g, " ").trim();
582
- }
583
- function canonicalDiscoveryName(value) {
584
- return normalizeDiscoveryText(value).replace(/[\s.-]+/g, "");
1512
+ function normalizeDiscoveryName(value) {
1513
+ return value.toLowerCase().replace(/[-_\s]+/g, " ").trim();
585
1514
  }
586
1515
  function discoveryTokens(value) {
587
- 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));
588
1517
  }
589
1518
  function facetValues(item, names) {
590
1519
  return names.flatMap((name) => item.facets[name] ?? []);
@@ -594,17 +1523,58 @@ function addDiscoveryMatch(matches, match) {
594
1523
  matches.push(match);
595
1524
  }
596
1525
  }
597
- function tokenOverlapScore(tokens, value) {
598
- if (tokens.length === 0 || !value) return 0;
599
- const normalized = normalizeDiscoveryText(value);
600
- const hits = tokens.filter((token) => normalized.includes(token)).length;
601
- 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;
1557
+ }
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());
602
1571
  }
603
- function discoveryMatches(item, query) {
1572
+ function discoveryMatches(item, query, options = {}) {
604
1573
  if (!query) return [];
605
1574
  const matches = [];
606
1575
  const tokens = discoveryTokens(query);
607
- const canonicalQuery = canonicalDiscoveryName(query);
1576
+ const lower = tokens.length > 0 ? tokens.join(" ") : query.toLowerCase().trim();
1577
+ const rawLower = query.toLowerCase().trim();
608
1578
  const idParts = item.id.split(/[:/]/);
609
1579
  const names = [
610
1580
  item.id,
@@ -630,33 +1600,66 @@ function discoveryMatches(item, query) {
630
1600
  ...item.tags
631
1601
  ].filter((value) => hasString(value));
632
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
+ };
633
1614
  for (const name of names) {
634
- const canonicalName = canonicalDiscoveryName(name);
635
- if (!canonicalName) continue;
636
- if (canonicalName === canonicalQuery) {
637
- addDiscoveryMatch(matches, { field: "id", value: name, score: 80, reason: "exact canonical name" });
638
- } else if (canonicalName.includes(canonicalQuery) || canonicalQuery.includes(canonicalName)) {
639
- 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;
640
1624
  }
641
1625
  }
642
- const title = recordTitle(item);
643
- const titleOverlap = tokenOverlapScore(tokens, title);
644
- if (titleOverlap > 0) addDiscoveryMatch(matches, { field: "title", value: title, score: 18 * titleOverlap, reason: "title token overlap" });
645
- for (const trigger of triggers) {
646
- const overlap = tokenOverlapScore(tokens, trigger);
647
- 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
+ }
648
1647
  }
649
- for (const capability of capabilities) {
650
- const overlap = tokenOverlapScore(tokens, capability);
651
- 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);
652
1650
  }
653
- const text = recordText(item);
654
- const textOverlap = tokenOverlapScore(tokens, text);
655
- 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);
656
1655
  for (const source of sourceValues) {
657
- const overlap = tokenOverlapScore(tokens, source);
658
- 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);
1657
+ }
1658
+ if (type.toLowerCase().includes(lower)) {
1659
+ score += 0.1;
1660
+ addInfo({ field: "facet", value: type, reason: "type overlap" });
659
1661
  }
1662
+ addScore(Math.min(score, 1));
660
1663
  return matches;
661
1664
  }
662
1665
  function rankMatches(matches, weights) {
@@ -682,13 +1685,13 @@ function createSnippet(item, matches, q, maxLength) {
682
1685
  const firstMatch = textMatch ?? titleMatch ?? matches[0];
683
1686
  return clipSnippet(firstMatch?.value ?? recordText(item), q, maxLength);
684
1687
  }
685
- function createRankedEntries(items, q, options, ordinalBase = 0) {
1688
+ function createRankedEntries(items, q, options, ordinalBase = 0, discoveryOptions = {}) {
686
1689
  const weights = { ...DEFAULT_QUERY_WEIGHTS, ...options.weights };
687
1690
  const profile = options.searchProfile ?? "default";
688
1691
  return items.map((item, ordinal) => ({
689
1692
  item,
690
1693
  ordinal: ordinalBase + ordinal,
691
- matches: profile === "aiwg-discovery" ? discoveryMatches(item, q) : queryMatches(item, q)
1694
+ matches: profile === "aiwg-discovery" ? discoveryMatches(item, q, discoveryOptions) : queryMatches(item, q)
692
1695
  })).filter(({ item, matches }) => {
693
1696
  if (q && matches.length === 0) return false;
694
1697
  if (options.types && !options.types.includes(item.type)) return false;
@@ -738,8 +1741,7 @@ function queryAiwgFortemiIndex(index, query = "", options = {}) {
738
1741
  const q = query.trim().toLowerCase();
739
1742
  const entries = createRankedEntries(index.items, q, options);
740
1743
  if (entries.length === 0 && q && options.searchProfile === "aiwg-discovery") {
741
- const relaxed = discoveryTokens(q).join(" ");
742
- return createQueryResultFromRankedEntries(createRankedEntries(index.items, relaxed, options), relaxed, options);
1744
+ return createQueryResultFromRankedEntries(createRankedEntries(index.items, q, options, 0, { relaxOverlap: true }), q, options);
743
1745
  }
744
1746
  return createQueryResultFromRankedEntries(entries, q, options);
745
1747
  }
@@ -760,7 +1762,8 @@ function cosineSimilarity(left, right) {
760
1762
  }
761
1763
  function validateAiwgStaticEmbeddingSet(value) {
762
1764
  const errors = [];
763
- const data = value;
1765
+ const data = isPlainRecord(value) ? value : {};
1766
+ if (!isPlainRecord(value)) errors.push("embedding set must be an object");
764
1767
  if (data?.schema_version !== "aiwg.fortemi.embedding.set.v1") errors.push("schema_version must be aiwg.fortemi.embedding.set.v1");
765
1768
  if (!hasString(data?.id)) errors.push("id is required");
766
1769
  if (!hasString(data?.model)) errors.push("model is required");
@@ -768,7 +1771,12 @@ function validateAiwgStaticEmbeddingSet(value) {
768
1771
  if (!hasString(data?.generated_at)) errors.push("generated_at is required");
769
1772
  if (!hasString(data?.granularity)) errors.push("granularity is required");
770
1773
  if (!Array.isArray(data?.embeddings)) errors.push("embeddings must be an array");
771
- 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
+ }
772
1780
  if (!hasString(embedding.record_id)) errors.push("embeddings[" + index + "].record_id is required");
773
1781
  if (!hasString(embedding.input_hash)) errors.push("embeddings[" + index + "].input_hash is required");
774
1782
  if (!Array.isArray(embedding.embedding)) errors.push("embeddings[" + index + "].embedding must be an array");
@@ -798,7 +1806,10 @@ function queryAiwgSemanticIndex(index, embeddingSet, queryEmbedding, options = {
798
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);
799
1807
  }
800
1808
  function queryAiwgHybridIndex(index, embeddingSet, query, queryEmbedding, options = {}) {
801
- 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 });
802
1813
  const semantic = queryAiwgSemanticIndex(index, embeddingSet, queryEmbedding, { limit: index.items.length });
803
1814
  const lexicalWeight = options.lexicalWeight ?? 0.5;
804
1815
  const semanticWeight = options.semanticWeight ?? 0.5;
@@ -820,8 +1831,15 @@ function queryAiwgHybridIndex(index, embeddingSet, query, queryEmbedding, option
820
1831
  };
821
1832
  }).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);
822
1833
  }
823
- function findAiwgStaticDuplicatePairs(index, embeddingSet, threshold = 0.9) {
1834
+ var DEFAULT_AIWG_DUPLICATE_SCAN_MAX_EMBEDDINGS = 5e3;
1835
+ function findAiwgStaticDuplicatePairs(index, embeddingSet, threshold = 0.9, options) {
824
1836
  assertAiwgStaticEmbeddingSet(embeddingSet);
1837
+ const maxEmbeddings = options?.maxEmbeddings ?? DEFAULT_AIWG_DUPLICATE_SCAN_MAX_EMBEDDINGS;
1838
+ if (embeddingSet.embeddings.length > maxEmbeddings) {
1839
+ throw new Error(
1840
+ "Embedding set too large for duplicate scan: " + embeddingSet.embeddings.length + " > " + maxEmbeddings + " (raise options.maxEmbeddings to override for trusted input)"
1841
+ );
1842
+ }
825
1843
  const byId = new Map(index.items.map((item) => [item.id, item]));
826
1844
  const pairs = [];
827
1845
  for (let leftIndex = 0; leftIndex < embeddingSet.embeddings.length; leftIndex += 1) {
@@ -861,6 +1879,7 @@ function matchSetCacheKey(q, options) {
861
1879
  concepts: options.concepts ?? null,
862
1880
  privacy: options.privacy ?? null,
863
1881
  rel: options.relationshipTargetId ?? null,
1882
+ searchProfile: options.searchProfile ?? "default",
864
1883
  weights: { ...DEFAULT_QUERY_WEIGHTS, ...options.weights }
865
1884
  });
866
1885
  }
@@ -949,15 +1968,21 @@ function edgeFromRelationship(sourceId, relationship) {
949
1968
  }
950
1969
  function relationshipMatches(edge, options = {}) {
951
1970
  const type = relationshipTypeFilter(options);
952
- const direction = options.direction ?? "both";
953
1971
  if (type && edge.type !== type) return false;
954
1972
  if (options.relationshipDirection && edge.direction !== options.relationshipDirection) return false;
955
1973
  if (options.sourceId && edge.source_id !== options.sourceId) return false;
956
1974
  if (options.targetId && edge.target_id !== options.targetId) return false;
957
- if (direction === "out" && options.targetId && edge.target_id !== options.targetId) return false;
958
- 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;
959
1976
  return true;
960
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
+ }
961
1986
  function nodeSummary(item) {
962
1987
  return { id: item.id, type: item.type, title: recordTitle(item) };
963
1988
  }
@@ -965,6 +1990,7 @@ function addNode(nodes, item) {
965
1990
  if (item) nodes.set(item.id, nodeSummary(item));
966
1991
  }
967
1992
  function relationshipResultFromRecords(records, options = {}) {
1993
+ assertRelationshipDirectionAnchor(options);
968
1994
  const byId = new Map(records.map((record) => [record.id, record]));
969
1995
  const edges = [];
970
1996
  for (const record of records) {
@@ -974,7 +2000,8 @@ function relationshipResultFromRecords(records, options = {}) {
974
2000
  edges.push(edge);
975
2001
  }
976
2002
  }
977
- 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;
978
2005
  const nodes = /* @__PURE__ */ new Map();
979
2006
  for (const edge of limitedEdges) {
980
2007
  addNode(nodes, byId.get(edge.source_id));
@@ -991,7 +2018,8 @@ function neighborQueryOptions(id, options = {}) {
991
2018
  return {
992
2019
  ...options,
993
2020
  ...direction === "out" ? { sourceId: id } : {},
994
- ...direction === "in" ? { targetId: id } : {}
2021
+ ...direction === "in" ? { targetId: id } : {},
2022
+ ...direction === "both" ? { endpointId: id } : {}
995
2023
  };
996
2024
  }
997
2025
  function filterNeighborResult(id, result, options = {}) {
@@ -1286,7 +2314,7 @@ function createAiwgIndexController(initialIndex) {
1286
2314
  notify();
1287
2315
  },
1288
2316
  createReviewDecisionExport(generatedAt) {
1289
- const source = index ?? (chunked ? { schema_version: "aiwg.fortemi.index.export.v1" } : null);
2317
+ const source = index ?? (chunked ? { schema_version: chunked.manifest.source_export_schema_version ?? "aiwg.fortemi.index.export.v1" } : null);
1290
2318
  if (!source) throw new Error("No AIWG index export or chunked manifest loaded");
1291
2319
  return createAiwgReviewDecisionExport(source, reviewDecisions, generatedAt);
1292
2320
  },
@@ -1300,13 +2328,14 @@ function createAiwgIndexController(initialIndex) {
1300
2328
  }
1301
2329
  function aiwgFortemiIndexToCommunityGraph(index, options = {}) {
1302
2330
  const ids = new Set(index.items.map((item) => item.id));
1303
- const relationshipWeights = options.relationshipWeights ?? {};
2331
+ const relationshipWeights = options.relationshipWeights ?? /* @__PURE__ */ Object.create(null);
1304
2332
  const edgeCounts = /* @__PURE__ */ new Map();
1305
2333
  for (const item of index.items) {
1306
2334
  for (const relationship of item.relationships) {
1307
2335
  if (!ids.has(relationship.target_id) && !options.includeDanglingRelationships) continue;
1308
2336
  const kind = relationship.type;
1309
- 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;
1310
2339
  const key = `${item.id}\0${relationship.target_id}\0${kind}`;
1311
2340
  const existing = edgeCounts.get(key);
1312
2341
  if (existing) existing.weight += baseWeight;
@@ -1342,6 +2371,6 @@ function communityIdsFor(item, options) {
1342
2371
  return [`type:${item.type}`];
1343
2372
  }
1344
2373
 
1345
- export { AIWG_SCAN_REQUIRED_FIELDS, aiwgDetailHrefForId, aiwgFortemiIndexToCommunityGraph, assertAiwgFortemiChunkManifest, assertAiwgFortemiChunkPart, assertAiwgFortemiIndexExport, assertAiwgStaticEmbeddingSet, buildAiwgChunkedIndex, buildAiwgStaticEmbeddingSet, createAiwgFetchChunkLoader, createAiwgFetchDetailLoader, createAiwgIndexController, createAiwgReviewDecisionExport, encodeAiwgDetailId, findAiwgStaticDuplicatePairs, getAiwgFortemiFacets, queryAiwgFortemiIndex, queryAiwgHybridIndex, queryAiwgSemanticIndex, validateAiwgFortemiChunkManifest, validateAiwgFortemiChunkPart, validateAiwgFortemiIndexExport, validateAiwgStaticEmbeddingSet };
2374
+ 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 };
1346
2375
  //# sourceMappingURL=aiwg-index.js.map
1347
2376
  //# sourceMappingURL=aiwg-index.js.map