@fortemi/core 2026.7.0 → 2026.7.2

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,3 +1,12 @@
1
+ import { sha256 } from '@noble/hashes/sha256';
2
+ import { bytesToHex } from '@noble/hashes/utils';
3
+
4
+ // src/hash.ts
5
+ function computeHash(data) {
6
+ const digest = sha256(data);
7
+ return `sha256:${bytesToHex(digest)}`;
8
+ }
9
+
1
10
  // src/aiwg-index.ts
2
11
  var AIWG_SCAN_REQUIRED_FIELDS = [
3
12
  "schema_version",
@@ -15,8 +24,6 @@ var REQUIRED_RECORD_FIELDS = [
15
24
  "id",
16
25
  "type",
17
26
  "source",
18
- "title",
19
- "text",
20
27
  "facets",
21
28
  "tags",
22
29
  "concepts",
@@ -57,6 +64,12 @@ function isPlainRecord(value) {
57
64
  function isOptionalStringArray(value) {
58
65
  return value === void 0 || Array.isArray(value) && value.every((item) => typeof item === "string");
59
66
  }
67
+ function isSupportedIndexSchemaVersion(value) {
68
+ return value === "aiwg.fortemi.index.export.v1" || value === "aiwg.fortemi.index.export.v2";
69
+ }
70
+ function isSupportedRecordSchemaVersion(value) {
71
+ return value === "aiwg.fortemi.index.record.v1" || value === "aiwg.fortemi.index.record.v2";
72
+ }
60
73
  function validateOptionalRichMetadata(item, index, errors) {
61
74
  if (item.skos_concepts !== void 0) {
62
75
  if (!Array.isArray(item.skos_concepts)) {
@@ -103,28 +116,80 @@ function validateOptionalRichMetadata(item, index, errors) {
103
116
  if (relationship.metadata !== void 0 && !isPlainRecord(relationship.metadata)) {
104
117
  errors.push("items[" + index + "].relationships[" + relationshipIndex + "].metadata must be an object");
105
118
  }
119
+ if (relationship.direction !== void 0 && relationship.direction !== "upstream" && relationship.direction !== "downstream" && relationship.direction !== "related") {
120
+ errors.push("items[" + index + "].relationships[" + relationshipIndex + "].direction must be upstream, downstream, or related");
121
+ }
122
+ if (relationship.target_path !== void 0 && typeof relationship.target_path !== "string") {
123
+ errors.push("items[" + index + "].relationships[" + relationshipIndex + "].target_path must be a string");
124
+ }
125
+ }
126
+ }
127
+ if (item.search !== void 0) {
128
+ if (!isPlainRecord(item.search)) {
129
+ errors.push("items[" + index + "].search must be an object");
130
+ } else {
131
+ if (!isOptionalStringArray(item.search.triggers)) errors.push("items[" + index + "].search.triggers must be a string array");
132
+ if (!isOptionalStringArray(item.search.aliases)) errors.push("items[" + index + "].search.aliases must be a string array");
133
+ if (!isOptionalStringArray(item.search.tags)) errors.push("items[" + index + "].search.tags must be a string array");
134
+ if (item.search.frontmatter !== void 0 && !isPlainRecord(item.search.frontmatter)) {
135
+ errors.push("items[" + index + "].search.frontmatter must be an object");
136
+ }
106
137
  }
107
138
  }
139
+ if (item.chunks !== void 0) {
140
+ if (!Array.isArray(item.chunks)) {
141
+ errors.push("items[" + index + "].chunks must be an array when present");
142
+ } else {
143
+ for (const [chunkIndex, chunk] of item.chunks.entries()) {
144
+ if (!isPlainRecord(chunk)) errors.push("items[" + index + "].chunks[" + chunkIndex + "] must be an object");
145
+ if (chunk.metadata !== void 0 && !isPlainRecord(chunk.metadata)) {
146
+ errors.push("items[" + index + "].chunks[" + chunkIndex + "].metadata must be an object");
147
+ }
148
+ }
149
+ }
150
+ }
151
+ if (item.embeddings !== void 0) {
152
+ if (!Array.isArray(item.embeddings)) {
153
+ errors.push("items[" + index + "].embeddings must be an array when present");
154
+ } else {
155
+ for (const [embeddingIndex, embedding] of item.embeddings.entries()) {
156
+ if (!isPlainRecord(embedding)) errors.push("items[" + index + "].embeddings[" + embeddingIndex + "] must be an object");
157
+ const vector = embedding.embedding ?? embedding.vector;
158
+ if (vector !== void 0 && (!Array.isArray(vector) || !vector.every((entry) => typeof entry === "number"))) {
159
+ errors.push("items[" + index + "].embeddings[" + embeddingIndex + "].embedding/vector must be a number array");
160
+ }
161
+ if (embedding.metadata !== void 0 && !isPlainRecord(embedding.metadata)) {
162
+ errors.push("items[" + index + "].embeddings[" + embeddingIndex + "].metadata must be an object");
163
+ }
164
+ }
165
+ }
166
+ }
167
+ if (item.compatibility !== void 0 && !isPlainRecord(item.compatibility)) {
168
+ errors.push("items[" + index + "].compatibility must be an object");
169
+ }
108
170
  }
109
171
  function validateAiwgFortemiIndexExport(value) {
110
172
  const errors = [];
111
173
  const counts = {};
112
174
  const data = value;
113
- if (data?.schema_version !== "aiwg.fortemi.index.export.v1") {
114
- errors.push("schema_version must be aiwg.fortemi.index.export.v1");
175
+ if (!isSupportedIndexSchemaVersion(data?.schema_version)) {
176
+ errors.push("schema_version must be aiwg.fortemi.index.export.v1 or aiwg.fortemi.index.export.v2");
115
177
  }
116
178
  if (!hasString(data?.generated_at)) errors.push("generated_at is required");
117
179
  if (!hasString(data?.source?.repo)) errors.push("source.repo is required");
118
180
  if (!hasString(data?.source?.privacy)) errors.push("source.privacy is required");
119
181
  if (!Array.isArray(data?.items)) errors.push("items must be an array");
182
+ if (data.compatibility !== void 0 && !isPlainRecord(data.compatibility)) {
183
+ errors.push("compatibility must be an object");
184
+ }
120
185
  const ids = /* @__PURE__ */ new Set();
121
186
  let previousId = "";
122
187
  for (const [index, item] of (data.items ?? []).entries()) {
123
188
  for (const field of REQUIRED_RECORD_FIELDS) {
124
189
  if (!(field in item)) errors.push("items[" + index + "]." + field + " is required");
125
190
  }
126
- if (item.schema_version !== "aiwg.fortemi.index.record.v1") {
127
- errors.push("items[" + index + "].schema_version must be aiwg.fortemi.index.record.v1");
191
+ if (!isSupportedRecordSchemaVersion(item.schema_version)) {
192
+ errors.push("items[" + index + "].schema_version must be aiwg.fortemi.index.record.v1 or aiwg.fortemi.index.record.v2");
128
193
  }
129
194
  if (!hasString(item.id)) errors.push("items[" + index + "].id is required");
130
195
  if (hasString(item.id) && ids.has(item.id)) errors.push("duplicate id: " + item.id);
@@ -138,6 +203,12 @@ function validateAiwgFortemiIndexExport(value) {
138
203
  if (!hasString(item.source?.path)) errors.push("items[" + index + "].source.path is required");
139
204
  if (!hasString(item.source?.repo_relative_path)) errors.push("items[" + index + "].source.repo_relative_path is required");
140
205
  if (!hasString(item.source?.locator)) errors.push("items[" + index + "].source.locator is required");
206
+ if (typeof item.title !== "string" && typeof item.search?.title !== "string" && typeof item.search?.name !== "string") {
207
+ errors.push("items[" + index + "].title or search.title/search.name is required");
208
+ }
209
+ if (typeof item.text !== "string" && typeof item.search?.body !== "string" && typeof item.search?.summary !== "string") {
210
+ errors.push("items[" + index + "].text or search.body/search.summary is required");
211
+ }
141
212
  if (!Array.isArray(item.tags)) errors.push("items[" + index + "].tags must be an array");
142
213
  if (!Array.isArray(item.concepts)) errors.push("items[" + index + "].concepts must be an array");
143
214
  if (!Array.isArray(item.relationships)) errors.push("items[" + index + "].relationships must be an array");
@@ -218,8 +289,8 @@ function validateProjectedRecords(items) {
218
289
  const ids = /* @__PURE__ */ new Set();
219
290
  let previousId = "";
220
291
  for (const [index, item] of items.entries()) {
221
- if (item.schema_version !== "aiwg.fortemi.index.record.v1") {
222
- errors.push("items[" + index + "].schema_version must be aiwg.fortemi.index.record.v1");
292
+ if (!isSupportedRecordSchemaVersion(item.schema_version)) {
293
+ errors.push("items[" + index + "].schema_version must be aiwg.fortemi.index.record.v1 or aiwg.fortemi.index.record.v2");
223
294
  }
224
295
  if (!hasString(item.id)) errors.push("items[" + index + "].id is required");
225
296
  if (hasString(item.id) && ids.has(item.id)) errors.push("duplicate id: " + item.id);
@@ -229,8 +300,12 @@ function validateProjectedRecords(items) {
229
300
  }
230
301
  if (hasString(item.id)) previousId = item.id;
231
302
  if (!hasString(item.type)) errors.push("items[" + index + "].type must be a non-empty string");
232
- if (!hasString(item.title)) errors.push("items[" + index + "].title is required");
233
- if (typeof item.text !== "string") errors.push("items[" + index + "].text is required");
303
+ if (typeof item.title !== "string" && typeof item.search?.title !== "string" && typeof item.search?.name !== "string") {
304
+ errors.push("items[" + index + "].title or search.title/search.name is required");
305
+ }
306
+ if (typeof item.text !== "string" && typeof item.search?.body !== "string" && typeof item.search?.summary !== "string") {
307
+ errors.push("items[" + index + "].text or search.body/search.summary is required");
308
+ }
234
309
  if (!item.facets || typeof item.facets !== "object" || Array.isArray(item.facets)) {
235
310
  errors.push("items[" + index + "].facets must be an object");
236
311
  }
@@ -322,6 +397,86 @@ function getAiwgFortemiFacets(items) {
322
397
  }
323
398
  return result;
324
399
  }
400
+ function recordTitle(item) {
401
+ return item.title ?? item.search?.title ?? item.search?.name ?? item.id;
402
+ }
403
+ function recordText(item) {
404
+ 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") ?? "" : "";
406
+ return [base, extractedText].filter(Boolean).join("\n");
407
+ }
408
+ function defaultEmbeddingInput(record, granularity) {
409
+ const title = recordTitle(record);
410
+ const text = recordText(record);
411
+ 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");
414
+ }
415
+ return [title, text].filter(Boolean).join("\n");
416
+ }
417
+ function generatedAtString(value) {
418
+ if (value instanceof Date) return value.toISOString();
419
+ return value ?? (/* @__PURE__ */ new Date()).toISOString();
420
+ }
421
+ async function buildAiwgStaticEmbeddingSet(index, options) {
422
+ assertAiwgFortemiIndexExport(index);
423
+ const granularity = options.granularity ?? "body";
424
+ const records = options.records ?? index.items;
425
+ const embeddings = [];
426
+ for (const record of records) {
427
+ const input = options.textForRecord?.(record) ?? defaultEmbeddingInput(record, granularity);
428
+ const embedding = await options.backend.embed(input, record);
429
+ if (embedding.length !== options.backend.dimensions) {
430
+ throw new Error(`Embedding for ${record.id} has ${embedding.length} dimensions; expected ${options.backend.dimensions}`);
431
+ }
432
+ embeddings.push({
433
+ record_id: record.id,
434
+ embedding,
435
+ granularity,
436
+ input_hash: computeHash(new TextEncoder().encode(input)),
437
+ source_path: record.source.path
438
+ });
439
+ }
440
+ const embeddingSet = {
441
+ schema_version: "aiwg.fortemi.embedding.set.v1",
442
+ id: options.id,
443
+ model: options.backend.model,
444
+ dimensions: options.backend.dimensions,
445
+ generated_at: generatedAtString(options.generatedAt),
446
+ granularity,
447
+ ...options.metric ? { metric: options.metric } : {},
448
+ input_hash_algorithm: "sha256",
449
+ embeddings
450
+ };
451
+ assertAiwgStaticEmbeddingSet(embeddingSet);
452
+ return embeddingSet;
453
+ }
454
+ function recordSearchValues(item) {
455
+ const search = item.search;
456
+ const values = [
457
+ item.id,
458
+ recordTitle(item),
459
+ recordText(item),
460
+ search?.title,
461
+ search?.name,
462
+ search?.summary,
463
+ search?.body,
464
+ search?.capability,
465
+ search?.phase,
466
+ search?.type,
467
+ ...search?.triggers ?? [],
468
+ ...search?.aliases ?? [],
469
+ ...search?.tags ?? [],
470
+ ...(item.chunks ?? []).flatMap((chunk) => [chunk.text, chunk.body, chunk.summary, chunk.source_path])
471
+ ];
472
+ if (search?.frontmatter) {
473
+ for (const value of Object.values(search.frontmatter)) {
474
+ if (typeof value === "string") values.push(value);
475
+ else if (Array.isArray(value)) values.push(...value.filter((entry) => typeof entry === "string"));
476
+ }
477
+ }
478
+ return values.filter((value) => typeof value === "string" && value.length > 0);
479
+ }
325
480
  function buildAiwgChunkedIndex(index, options = {}) {
326
481
  const partSize = hasPositiveInteger(options.partSize) ? options.partSize : 500;
327
482
  const projection = options.projection;
@@ -383,14 +538,21 @@ function matchesFacetFilters(item, filters) {
383
538
  function queryMatches(item, q) {
384
539
  if (!q) return [];
385
540
  const matches = [];
386
- if (item.title.toLowerCase().includes(q)) matches.push({ field: "title", value: item.title });
387
- if (item.text.toLowerCase().includes(q)) matches.push({ field: "text", value: item.text });
541
+ const title = recordTitle(item);
542
+ const text = recordText(item);
543
+ if (title.toLowerCase().includes(q)) matches.push({ field: "title", value: title });
544
+ if (text.toLowerCase().includes(q)) matches.push({ field: "text", value: text });
388
545
  for (const tag of item.tags) {
389
546
  if (tag.toLowerCase().includes(q)) matches.push({ field: "tag", value: tag });
390
547
  }
391
548
  for (const concept of item.concepts) {
392
549
  if (concept.toLowerCase().includes(q)) matches.push({ field: "concept", value: concept });
393
550
  }
551
+ for (const value of recordSearchValues(item)) {
552
+ if (value !== title && value !== text && value.toLowerCase().includes(q)) {
553
+ matches.push({ field: "text", value, score: DEFAULT_QUERY_WEIGHTS.text });
554
+ }
555
+ }
394
556
  return matches;
395
557
  }
396
558
  var DISCOVERY_STOPWORDS = /* @__PURE__ */ new Set([
@@ -446,17 +608,28 @@ function discoveryMatches(item, query) {
446
608
  const idParts = item.id.split(/[:/]/);
447
609
  const names = [
448
610
  item.id,
449
- item.title,
611
+ recordTitle(item),
612
+ item.search?.name,
613
+ item.search?.title,
614
+ ...item.search?.aliases ?? [],
450
615
  ...idParts,
451
616
  ...facetValues(item, ["name", "canonical_name", "command", "skill", "agent", "rule"])
452
- ].filter(Boolean);
453
- const triggers = facetValues(item, ["trigger", "triggers", "trigger_phrase", "trigger_phrases"]);
617
+ ].filter((value) => hasString(value));
618
+ const triggers = [
619
+ ...facetValues(item, ["trigger", "triggers", "trigger_phrase", "trigger_phrases"]),
620
+ ...item.search?.triggers ?? []
621
+ ];
454
622
  const capabilities = [
455
623
  ...facetValues(item, ["capability", "capabilities", "summary", "description"]),
624
+ item.search?.capability,
625
+ item.search?.summary,
626
+ item.search?.phase,
627
+ item.search?.type,
628
+ ...item.search?.tags ?? [],
456
629
  ...item.concepts,
457
630
  ...item.tags
458
- ];
459
- const sourceValues = [item.source?.path, item.source?.repo_relative_path, item.source?.locator].filter(Boolean);
631
+ ].filter((value) => hasString(value));
632
+ const sourceValues = [item.source?.path, item.source?.repo_relative_path, item.source?.locator].filter((value) => hasString(value));
460
633
  for (const name of names) {
461
634
  const canonicalName = canonicalDiscoveryName(name);
462
635
  if (!canonicalName) continue;
@@ -466,8 +639,9 @@ function discoveryMatches(item, query) {
466
639
  addDiscoveryMatch(matches, { field: "id", value: name, score: 48, reason: "near canonical name" });
467
640
  }
468
641
  }
469
- const titleOverlap = tokenOverlapScore(tokens, item.title);
470
- if (titleOverlap > 0) addDiscoveryMatch(matches, { field: "title", value: item.title, score: 18 * titleOverlap, reason: "title token overlap" });
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" });
471
645
  for (const trigger of triggers) {
472
646
  const overlap = tokenOverlapScore(tokens, trigger);
473
647
  if (overlap > 0) addDiscoveryMatch(matches, { field: "facet", value: trigger, score: 34 * overlap, reason: "trigger phrase" });
@@ -476,8 +650,9 @@ function discoveryMatches(item, query) {
476
650
  const overlap = tokenOverlapScore(tokens, capability);
477
651
  if (overlap > 0) addDiscoveryMatch(matches, { field: "concept", value: capability, score: 22 * overlap, reason: "capability overlap" });
478
652
  }
479
- const textOverlap = tokenOverlapScore(tokens, item.text);
480
- if (textOverlap > 0) addDiscoveryMatch(matches, { field: "text", value: item.text, score: 8 * textOverlap, reason: "body token overlap" });
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" });
481
656
  for (const source of sourceValues) {
482
657
  const overlap = tokenOverlapScore(tokens, source);
483
658
  if (overlap > 0) addDiscoveryMatch(matches, { field: "source", value: source, score: 2 * overlap, reason: "path overlap" });
@@ -505,7 +680,7 @@ function createSnippet(item, matches, q, maxLength) {
505
680
  const textMatch = matches.find((match) => match.field === "text");
506
681
  const titleMatch = matches.find((match) => match.field === "title");
507
682
  const firstMatch = textMatch ?? titleMatch ?? matches[0];
508
- return clipSnippet(firstMatch?.value ?? item.text, q, maxLength);
683
+ return clipSnippet(firstMatch?.value ?? recordText(item), q, maxLength);
509
684
  }
510
685
  function createRankedEntries(items, q, options, ordinalBase = 0) {
511
686
  const weights = { ...DEFAULT_QUERY_WEIGHTS, ...options.weights };
@@ -767,13 +942,16 @@ function edgeFromRelationship(sourceId, relationship) {
767
942
  source_id: sourceId,
768
943
  target_id: relationship.target_id,
769
944
  type: relationship.type,
770
- ...relationship.source_path ? { source_path: relationship.source_path } : {}
945
+ ...relationship.source_path ? { source_path: relationship.source_path } : {},
946
+ ...relationship.target_path ? { target_path: relationship.target_path } : {},
947
+ ...relationship.direction ? { direction: relationship.direction } : {}
771
948
  };
772
949
  }
773
950
  function relationshipMatches(edge, options = {}) {
774
951
  const type = relationshipTypeFilter(options);
775
952
  const direction = options.direction ?? "both";
776
953
  if (type && edge.type !== type) return false;
954
+ if (options.relationshipDirection && edge.direction !== options.relationshipDirection) return false;
777
955
  if (options.sourceId && edge.source_id !== options.sourceId) return false;
778
956
  if (options.targetId && edge.target_id !== options.targetId) return false;
779
957
  if (direction === "out" && options.targetId && edge.target_id !== options.targetId) return false;
@@ -781,7 +959,7 @@ function relationshipMatches(edge, options = {}) {
781
959
  return true;
782
960
  }
783
961
  function nodeSummary(item) {
784
- return { id: item.id, type: item.type, title: item.title };
962
+ return { id: item.id, type: item.type, title: recordTitle(item) };
785
963
  }
786
964
  function addNode(nodes, item) {
787
965
  if (item) nodes.set(item.id, nodeSummary(item));
@@ -1164,6 +1342,6 @@ function communityIdsFor(item, options) {
1164
1342
  return [`type:${item.type}`];
1165
1343
  }
1166
1344
 
1167
- export { AIWG_SCAN_REQUIRED_FIELDS, aiwgDetailHrefForId, aiwgFortemiIndexToCommunityGraph, assertAiwgFortemiChunkManifest, assertAiwgFortemiChunkPart, assertAiwgFortemiIndexExport, assertAiwgStaticEmbeddingSet, buildAiwgChunkedIndex, createAiwgFetchChunkLoader, createAiwgFetchDetailLoader, createAiwgIndexController, createAiwgReviewDecisionExport, encodeAiwgDetailId, findAiwgStaticDuplicatePairs, getAiwgFortemiFacets, queryAiwgFortemiIndex, queryAiwgHybridIndex, queryAiwgSemanticIndex, validateAiwgFortemiChunkManifest, validateAiwgFortemiChunkPart, validateAiwgFortemiIndexExport, validateAiwgStaticEmbeddingSet };
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 };
1168
1346
  //# sourceMappingURL=aiwg-index.js.map
1169
1347
  //# sourceMappingURL=aiwg-index.js.map