@fortemi/core 2026.6.0 → 2026.6.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.
@@ -0,0 +1,550 @@
1
+ // src/aiwg-index.ts
2
+ var REQUIRED_RECORD_FIELDS = [
3
+ "schema_version",
4
+ "id",
5
+ "type",
6
+ "source",
7
+ "title",
8
+ "text",
9
+ "facets",
10
+ "tags",
11
+ "concepts",
12
+ "relationships",
13
+ "provenance",
14
+ "privacy",
15
+ "updated_at"
16
+ ];
17
+ var VALID_TYPES = /* @__PURE__ */ new Set([
18
+ "crm.contact",
19
+ "crm.organization",
20
+ "crm.event",
21
+ "crm.interaction",
22
+ "aiwg.artifact",
23
+ "docs.page"
24
+ ]);
25
+ var DEFAULT_QUERY_WEIGHTS = {
26
+ title: 4,
27
+ tag: 3,
28
+ concept: 2,
29
+ text: 1
30
+ };
31
+ function hasString(value) {
32
+ return typeof value === "string" && value.length > 0;
33
+ }
34
+ function pushFacet(counts, name, value) {
35
+ counts[name] ??= {};
36
+ counts[name][value] = (counts[name][value] ?? 0) + 1;
37
+ }
38
+ function hasNonNegativeInteger(value) {
39
+ return Number.isInteger(value) && typeof value === "number" && value >= 0;
40
+ }
41
+ function hasPositiveInteger(value) {
42
+ return Number.isInteger(value) && typeof value === "number" && value > 0;
43
+ }
44
+ function isFacetCounts(value) {
45
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
46
+ return Object.values(value).every((counts) => !!counts && typeof counts === "object" && !Array.isArray(counts) && Object.values(counts).every((count) => hasNonNegativeInteger(count)));
47
+ }
48
+ function validateAiwgFortemiIndexExport(value) {
49
+ const errors = [];
50
+ const counts = {};
51
+ const data = value;
52
+ if (data?.schema_version !== "aiwg.fortemi.index.export.v1") {
53
+ errors.push("schema_version must be aiwg.fortemi.index.export.v1");
54
+ }
55
+ if (!hasString(data?.generated_at)) errors.push("generated_at is required");
56
+ if (!hasString(data?.source?.repo)) errors.push("source.repo is required");
57
+ if (!hasString(data?.source?.privacy)) errors.push("source.privacy is required");
58
+ if (!Array.isArray(data?.items)) errors.push("items must be an array");
59
+ const ids = /* @__PURE__ */ new Set();
60
+ let previousId = "";
61
+ for (const [index, item] of (data.items ?? []).entries()) {
62
+ for (const field of REQUIRED_RECORD_FIELDS) {
63
+ if (!(field in item)) errors.push("items[" + index + "]." + field + " is required");
64
+ }
65
+ if (item.schema_version !== "aiwg.fortemi.index.record.v1") {
66
+ errors.push("items[" + index + "].schema_version must be aiwg.fortemi.index.record.v1");
67
+ }
68
+ if (!hasString(item.id)) errors.push("items[" + index + "].id is required");
69
+ if (hasString(item.id) && ids.has(item.id)) errors.push("duplicate id: " + item.id);
70
+ if (hasString(item.id)) ids.add(item.id);
71
+ if (previousId && hasString(item.id) && previousId.localeCompare(item.id) > 0) {
72
+ errors.push("items must be sorted by id: " + previousId + " before " + item.id);
73
+ }
74
+ if (hasString(item.id)) previousId = item.id;
75
+ if (!VALID_TYPES.has(item.type)) errors.push("items[" + index + "].type is invalid");
76
+ else counts[item.type] = (counts[item.type] ?? 0) + 1;
77
+ if (!hasString(item.source?.path)) errors.push("items[" + index + "].source.path is required");
78
+ if (!hasString(item.source?.repo_relative_path)) errors.push("items[" + index + "].source.repo_relative_path is required");
79
+ if (!hasString(item.source?.locator)) errors.push("items[" + index + "].source.locator is required");
80
+ if (!Array.isArray(item.tags)) errors.push("items[" + index + "].tags must be an array");
81
+ if (!Array.isArray(item.concepts)) errors.push("items[" + index + "].concepts must be an array");
82
+ if (!Array.isArray(item.relationships)) errors.push("items[" + index + "].relationships must be an array");
83
+ if (!Array.isArray(item.provenance) || item.provenance.length === 0) {
84
+ errors.push("items[" + index + "].provenance must be a non-empty array");
85
+ }
86
+ if (!item.privacy || typeof item.privacy.pii !== "boolean" || !hasString(item.privacy.classification)) {
87
+ errors.push("items[" + index + "].privacy requires classification and pii");
88
+ }
89
+ }
90
+ return { valid: errors.length === 0, errors, counts };
91
+ }
92
+ function assertAiwgFortemiIndexExport(value) {
93
+ const result = validateAiwgFortemiIndexExport(value);
94
+ if (!result.valid) {
95
+ throw new Error("Invalid AIWG Fortemi index export:\n" + result.errors.join("\n"));
96
+ }
97
+ return value;
98
+ }
99
+ function validateAiwgFortemiChunkManifest(value) {
100
+ const errors = [];
101
+ const data = value;
102
+ if (data?.schema_version !== "aiwg.fortemi.index.chunk-manifest.v1") {
103
+ errors.push("schema_version must be aiwg.fortemi.index.chunk-manifest.v1");
104
+ }
105
+ if (!hasString(data?.generated_at)) errors.push("generated_at is required");
106
+ if (!hasString(data?.source?.repo)) errors.push("source.repo is required");
107
+ if (!hasString(data?.source?.privacy)) errors.push("source.privacy is required");
108
+ if (!hasNonNegativeInteger(data?.total)) errors.push("total must be a non-negative integer");
109
+ if (!hasPositiveInteger(data?.part_size)) errors.push("part_size must be a positive integer");
110
+ if (data.facets !== void 0 && !isFacetCounts(data.facets)) {
111
+ errors.push("facets must be a nested string-to-number count object");
112
+ }
113
+ if (!Array.isArray(data?.parts)) errors.push("parts must be an array");
114
+ let expectedOffset = 0;
115
+ const parts = Array.isArray(data?.parts) ? data.parts : [];
116
+ for (const [index, part] of parts.entries()) {
117
+ if (!hasString(part.href)) errors.push("parts[" + index + "].href is required");
118
+ if (!hasNonNegativeInteger(part.offset)) errors.push("parts[" + index + "].offset must be a non-negative integer");
119
+ if (!hasNonNegativeInteger(part.count)) errors.push("parts[" + index + "].count must be a non-negative integer");
120
+ if (hasNonNegativeInteger(part.offset) && part.offset !== expectedOffset) {
121
+ errors.push("parts[" + index + "].offset must be " + expectedOffset);
122
+ }
123
+ if (hasNonNegativeInteger(part.count)) expectedOffset += part.count;
124
+ }
125
+ if (hasNonNegativeInteger(data?.total) && expectedOffset !== data.total) {
126
+ errors.push("parts counts must add up to total");
127
+ }
128
+ return { valid: errors.length === 0, errors };
129
+ }
130
+ function assertAiwgFortemiChunkManifest(value) {
131
+ const result = validateAiwgFortemiChunkManifest(value);
132
+ if (!result.valid) {
133
+ throw new Error("Invalid AIWG Fortemi chunk manifest:\n" + result.errors.join("\n"));
134
+ }
135
+ return value;
136
+ }
137
+ function validateAiwgFortemiChunkPart(value, partRef, manifest) {
138
+ const errors = [];
139
+ const data = value;
140
+ if (data?.schema_version !== "aiwg.fortemi.index.chunk.v1") {
141
+ errors.push("schema_version must be aiwg.fortemi.index.chunk.v1");
142
+ }
143
+ if (data?.manifest_schema_version !== "aiwg.fortemi.index.chunk-manifest.v1") {
144
+ errors.push("manifest_schema_version must be aiwg.fortemi.index.chunk-manifest.v1");
145
+ }
146
+ if (!hasNonNegativeInteger(data?.offset)) errors.push("offset must be a non-negative integer");
147
+ if (!Array.isArray(data?.items)) errors.push("items must be an array");
148
+ if (partRef && hasNonNegativeInteger(data?.offset) && data.offset !== partRef.offset) {
149
+ errors.push("offset must match manifest part offset " + partRef.offset);
150
+ }
151
+ if (partRef && Array.isArray(data?.items) && data.items.length !== partRef.count) {
152
+ errors.push("items length must match manifest part count " + partRef.count);
153
+ }
154
+ if (Array.isArray(data?.items)) {
155
+ const validation = validateAiwgFortemiIndexExport({
156
+ schema_version: "aiwg.fortemi.index.export.v1",
157
+ generated_at: manifest?.generated_at ?? "1970-01-01T00:00:00.000Z",
158
+ source: manifest?.source ?? { repo: "chunk", privacy: "public" },
159
+ items: data.items
160
+ });
161
+ errors.push(...validation.errors.map((error) => "items." + error));
162
+ }
163
+ return { valid: errors.length === 0, errors };
164
+ }
165
+ function assertAiwgFortemiChunkPart(value, partRef, manifest) {
166
+ const result = validateAiwgFortemiChunkPart(value, partRef, manifest);
167
+ if (!result.valid) {
168
+ throw new Error("Invalid AIWG Fortemi chunk part:\n" + result.errors.join("\n"));
169
+ }
170
+ return value;
171
+ }
172
+ function createAiwgFetchChunkLoader(baseUrl) {
173
+ return async (part) => {
174
+ const href = baseUrl ? new URL(part.href, baseUrl).toString() : part.href;
175
+ const response = await fetch(href);
176
+ if (!response.ok) throw new Error("Failed to fetch AIWG index chunk " + href + ": " + response.status);
177
+ return response.json();
178
+ };
179
+ }
180
+ function getAiwgFortemiFacets(items) {
181
+ const result = {};
182
+ for (const item of items) {
183
+ pushFacet(result, "type", item.type);
184
+ pushFacet(result, "privacy", item.privacy.classification);
185
+ for (const tag of item.tags) pushFacet(result, "tag", tag);
186
+ for (const concept of item.concepts) pushFacet(result, "concept", concept);
187
+ for (const [name, values] of Object.entries(item.facets)) {
188
+ for (const value of values) pushFacet(result, name, value);
189
+ }
190
+ }
191
+ return result;
192
+ }
193
+ function includesAll(actual, expected) {
194
+ if (!expected || expected.length === 0) return true;
195
+ const actualSet = new Set(actual);
196
+ return expected.every((value) => actualSet.has(value));
197
+ }
198
+ function matchesFacetFilters(item, filters) {
199
+ if (!filters) return true;
200
+ return Object.entries(filters).every(([name, expected]) => includesAll(item.facets[name] ?? [], expected));
201
+ }
202
+ function queryMatches(item, q) {
203
+ if (!q) return [];
204
+ const matches = [];
205
+ if (item.title.toLowerCase().includes(q)) matches.push({ field: "title", value: item.title });
206
+ if (item.text.toLowerCase().includes(q)) matches.push({ field: "text", value: item.text });
207
+ for (const tag of item.tags) {
208
+ if (tag.toLowerCase().includes(q)) matches.push({ field: "tag", value: tag });
209
+ }
210
+ for (const concept of item.concepts) {
211
+ if (concept.toLowerCase().includes(q)) matches.push({ field: "concept", value: concept });
212
+ }
213
+ return matches;
214
+ }
215
+ function rankMatches(matches, weights) {
216
+ return matches.reduce((total, match) => total + weights[match.field], 0);
217
+ }
218
+ function clipSnippet(value, q, maxLength) {
219
+ const normalizedLength = Math.max(20, maxLength);
220
+ if (!value) return "";
221
+ if (!q) return value.length > normalizedLength ? `${value.slice(0, normalizedLength).trimEnd()}...` : value;
222
+ const lower = value.toLowerCase();
223
+ const index = lower.indexOf(q);
224
+ if (index < 0) return value.length > normalizedLength ? `${value.slice(0, normalizedLength).trimEnd()}...` : value;
225
+ const context = Math.max(0, Math.floor((normalizedLength - q.length) / 2));
226
+ const start = Math.max(0, index - context);
227
+ const end = Math.min(value.length, start + normalizedLength);
228
+ const prefix = start > 0 ? "..." : "";
229
+ const suffix = end < value.length ? "..." : "";
230
+ return `${prefix}${value.slice(start, end).trim()}${suffix}`;
231
+ }
232
+ function createSnippet(item, matches, q, maxLength) {
233
+ const textMatch = matches.find((match) => match.field === "text");
234
+ const titleMatch = matches.find((match) => match.field === "title");
235
+ const firstMatch = textMatch ?? titleMatch ?? matches[0];
236
+ return clipSnippet(firstMatch?.value ?? item.text, q, maxLength);
237
+ }
238
+ function createRankedEntries(items, q, options, ordinalBase = 0) {
239
+ const weights = { ...DEFAULT_QUERY_WEIGHTS, ...options.weights };
240
+ return items.map((item, ordinal) => ({ item, ordinal: ordinalBase + ordinal, matches: queryMatches(item, q) })).filter(({ item, matches }) => {
241
+ if (q && matches.length === 0) return false;
242
+ if (options.types && !options.types.includes(item.type)) return false;
243
+ if (options.privacy && !options.privacy.includes(item.privacy.classification)) return false;
244
+ if (!includesAll(item.tags, options.tags)) return false;
245
+ if (!includesAll(item.concepts, options.concepts)) return false;
246
+ if (!matchesFacetFilters(item, options.facets)) return false;
247
+ if (options.relationshipTargetId && !item.relationships.some((rel) => rel.target_id === options.relationshipTargetId)) {
248
+ return false;
249
+ }
250
+ return true;
251
+ }).map(({ item, ordinal, matches }) => ({
252
+ item,
253
+ ordinal,
254
+ rank: rankMatches(matches, weights),
255
+ matches
256
+ }));
257
+ }
258
+ function sortRankedEntries(entries, rank) {
259
+ return [...entries].sort((left, right) => {
260
+ if (rank) return right.rank - left.rank || left.ordinal - right.ordinal;
261
+ return left.ordinal - right.ordinal;
262
+ });
263
+ }
264
+ function createQueryResultFromRankedEntries(entries, query, options) {
265
+ const ranked = sortRankedEntries(entries, options.rank);
266
+ const offset = options.offset ?? 0;
267
+ const limit = options.limit ?? ranked.length;
268
+ const page = ranked.slice(offset, offset + limit);
269
+ const result = {
270
+ items: page.map((entry) => entry.item),
271
+ total: ranked.length,
272
+ facets: getAiwgFortemiFacets(ranked.map((entry) => entry.item))
273
+ };
274
+ if (options.rank || options.snippets || options.includeMatches) {
275
+ const snippetLength = options.snippetLength ?? 160;
276
+ result.rankedItems = page.map((entry) => ({
277
+ item: entry.item,
278
+ rank: entry.rank,
279
+ ...options.snippets ? { snippet: createSnippet(entry.item, entry.matches, query, snippetLength) } : {},
280
+ ...options.includeMatches ? { matches: entry.matches } : {}
281
+ }));
282
+ }
283
+ return result;
284
+ }
285
+ function queryAiwgFortemiIndex(index, query = "", options = {}) {
286
+ const q = query.trim().toLowerCase();
287
+ return createQueryResultFromRankedEntries(createRankedEntries(index.items, q, options), q, options);
288
+ }
289
+ function chunkPartCacheKey(part) {
290
+ return `${part.offset}:${part.href}`;
291
+ }
292
+ function clampMaxCachedParts(value) {
293
+ if (!hasPositiveInteger(value)) return 3;
294
+ return value;
295
+ }
296
+ function isDirectChunkBrowse(query, options) {
297
+ return query.trim() === "" && !options.rank && !options.snippets && !options.includeMatches && !options.types && !options.facets && !options.tags && !options.concepts && !options.privacy && !options.relationshipTargetId;
298
+ }
299
+ function getPartsForRange(manifest, offset, limit) {
300
+ const end = offset + limit;
301
+ return manifest.parts.filter((part) => part.count > 0 && part.offset < end && part.offset + part.count > offset);
302
+ }
303
+ async function loadChunkPart(runtime, part) {
304
+ const key = chunkPartCacheKey(part);
305
+ const cached = runtime.partCache.get(key);
306
+ if (cached) {
307
+ runtime.partCache.delete(key);
308
+ runtime.partCache.set(key, cached);
309
+ return { part: cached, fetched: false };
310
+ }
311
+ const parsed = assertAiwgFortemiChunkPart(await runtime.loader(part, runtime.manifest), part, runtime.manifest);
312
+ runtime.partCache.set(key, parsed);
313
+ while (runtime.partCache.size > runtime.maxCachedParts) {
314
+ const oldest = runtime.partCache.keys().next().value;
315
+ if (oldest === void 0) break;
316
+ runtime.partCache.delete(oldest);
317
+ }
318
+ return { part: parsed, fetched: true };
319
+ }
320
+ async function queryChunkedAiwgFortemiIndex(runtime, query = "", options = {}) {
321
+ const q = query.trim().toLowerCase();
322
+ let scannedParts = 0;
323
+ let fetchedParts = 0;
324
+ if (isDirectChunkBrowse(query, options)) {
325
+ const offset = options.offset ?? 0;
326
+ const limit = options.limit ?? runtime.manifest.total;
327
+ const parts = getPartsForRange(runtime.manifest, offset, limit);
328
+ const items = [];
329
+ for (const partRef of parts) {
330
+ const loaded = await loadChunkPart(runtime, partRef);
331
+ if (loaded.fetched) fetchedParts += 1;
332
+ scannedParts += 1;
333
+ options.onProgress?.({ phase: "part", done: scannedParts, total: parts.length, href: partRef.href });
334
+ const start = Math.max(0, offset - partRef.offset);
335
+ const end = Math.min(loaded.part.items.length, offset + limit - partRef.offset);
336
+ items.push(...loaded.part.items.slice(start, end));
337
+ }
338
+ return {
339
+ items,
340
+ total: runtime.manifest.total,
341
+ facets: runtime.manifest.facets ?? {},
342
+ manifestTotal: runtime.manifest.total,
343
+ scannedParts,
344
+ fetchedParts,
345
+ complete: true
346
+ };
347
+ }
348
+ const entries = [];
349
+ for (const partRef of runtime.manifest.parts) {
350
+ const loaded = await loadChunkPart(runtime, partRef);
351
+ if (loaded.fetched) fetchedParts += 1;
352
+ scannedParts += 1;
353
+ options.onProgress?.({ phase: "part", done: scannedParts, total: runtime.manifest.parts.length, href: partRef.href });
354
+ entries.push(...createRankedEntries(loaded.part.items, q, options, partRef.offset));
355
+ options.onProgress?.({ phase: "query", done: scannedParts, total: runtime.manifest.parts.length, href: partRef.href });
356
+ }
357
+ return {
358
+ ...createQueryResultFromRankedEntries(entries, q, options),
359
+ manifestTotal: runtime.manifest.total,
360
+ scannedParts,
361
+ fetchedParts,
362
+ complete: true
363
+ };
364
+ }
365
+ function createAiwgReviewDecisionExport(source, decisions, generatedAt = (/* @__PURE__ */ new Date()).toISOString()) {
366
+ return {
367
+ schema_version: "aiwg.fortemi.review-decisions.v1",
368
+ generated_at: generatedAt,
369
+ source_export_schema_version: source.schema_version,
370
+ decisions: [...decisions].sort((left, right) => left.item_id.localeCompare(right.item_id))
371
+ };
372
+ }
373
+ function createAiwgIndexController(initialIndex) {
374
+ let index = initialIndex ?? null;
375
+ let chunked = null;
376
+ let data = null;
377
+ let error = null;
378
+ let reviewDecisions = [];
379
+ const listeners = /* @__PURE__ */ new Set();
380
+ const snapshot = () => ({
381
+ index,
382
+ chunked: chunked ? {
383
+ manifest: chunked.manifest,
384
+ cachedParts: chunked.partCache.size,
385
+ maxCachedParts: chunked.maxCachedParts
386
+ } : null,
387
+ data,
388
+ error,
389
+ reviewDecisions: [...reviewDecisions]
390
+ });
391
+ const notify = () => {
392
+ const current = snapshot();
393
+ for (const listener of listeners) listener(current);
394
+ };
395
+ const requireIndex = () => {
396
+ if (!index) throw new Error("No AIWG index export loaded");
397
+ return index;
398
+ };
399
+ return {
400
+ loadIndex(value) {
401
+ try {
402
+ const parsed = assertAiwgFortemiIndexExport(value);
403
+ index = parsed;
404
+ chunked = null;
405
+ data = null;
406
+ reviewDecisions = [];
407
+ error = null;
408
+ notify();
409
+ return parsed;
410
+ } catch (err) {
411
+ error = err instanceof Error ? err : new Error(String(err));
412
+ notify();
413
+ throw error;
414
+ }
415
+ },
416
+ loadChunkedIndex(manifest, loader, options = {}) {
417
+ try {
418
+ const parsed = assertAiwgFortemiChunkManifest(manifest);
419
+ index = null;
420
+ chunked = {
421
+ manifest: parsed,
422
+ loader,
423
+ maxCachedParts: clampMaxCachedParts(options.maxCachedParts),
424
+ partCache: /* @__PURE__ */ new Map()
425
+ };
426
+ data = null;
427
+ reviewDecisions = [];
428
+ error = null;
429
+ notify();
430
+ return parsed;
431
+ } catch (err) {
432
+ error = err instanceof Error ? err : new Error(String(err));
433
+ notify();
434
+ throw error;
435
+ }
436
+ },
437
+ getIndex() {
438
+ return index;
439
+ },
440
+ getChunkedManifest() {
441
+ return chunked?.manifest ?? null;
442
+ },
443
+ getSnapshot() {
444
+ return snapshot();
445
+ },
446
+ query(query = "", options) {
447
+ const result = queryAiwgFortemiIndex(requireIndex(), query, options);
448
+ data = result;
449
+ error = null;
450
+ notify();
451
+ return result;
452
+ },
453
+ async queryChunked(query = "", options) {
454
+ if (!chunked) throw new Error("No AIWG chunked index manifest loaded");
455
+ try {
456
+ const result = await queryChunkedAiwgFortemiIndex(chunked, query, options);
457
+ data = result;
458
+ error = null;
459
+ notify();
460
+ return result;
461
+ } catch (err) {
462
+ error = err instanceof Error ? err : new Error(String(err));
463
+ notify();
464
+ throw error;
465
+ }
466
+ },
467
+ clearChunkCache() {
468
+ chunked?.partCache.clear();
469
+ error = null;
470
+ notify();
471
+ },
472
+ toCommunityGraph(options) {
473
+ return aiwgFortemiIndexToCommunityGraph(requireIndex(), options);
474
+ },
475
+ setReviewDecision(input) {
476
+ const decision = {
477
+ ...input,
478
+ updated_at: (/* @__PURE__ */ new Date()).toISOString()
479
+ };
480
+ reviewDecisions = [
481
+ ...reviewDecisions.filter((item) => item.item_id !== decision.item_id),
482
+ decision
483
+ ].sort((left, right) => left.item_id.localeCompare(right.item_id));
484
+ error = null;
485
+ notify();
486
+ return decision;
487
+ },
488
+ clearReviewDecision(itemId) {
489
+ reviewDecisions = reviewDecisions.filter((item) => item.item_id !== itemId);
490
+ error = null;
491
+ notify();
492
+ },
493
+ createReviewDecisionExport(generatedAt) {
494
+ return createAiwgReviewDecisionExport(requireIndex(), reviewDecisions, generatedAt);
495
+ },
496
+ subscribe(listener) {
497
+ listeners.add(listener);
498
+ return () => {
499
+ listeners.delete(listener);
500
+ };
501
+ }
502
+ };
503
+ }
504
+ function aiwgFortemiIndexToCommunityGraph(index, options = {}) {
505
+ const ids = new Set(index.items.map((item) => item.id));
506
+ const relationshipWeights = options.relationshipWeights ?? {};
507
+ const edgeCounts = /* @__PURE__ */ new Map();
508
+ for (const item of index.items) {
509
+ for (const relationship of item.relationships) {
510
+ if (!ids.has(relationship.target_id) && !options.includeDanglingRelationships) continue;
511
+ const kind = relationship.type;
512
+ const baseWeight = relationshipWeights[kind] ?? 1;
513
+ const key = `${item.id}\0${relationship.target_id}\0${kind}`;
514
+ const existing = edgeCounts.get(key);
515
+ if (existing) existing.weight += baseWeight;
516
+ else edgeCounts.set(key, { source: item.id, target: relationship.target_id, kind, weight: baseWeight });
517
+ }
518
+ }
519
+ const communities = /* @__PURE__ */ new Map();
520
+ for (const item of index.items) {
521
+ const communityIds = communityIdsFor(item, options);
522
+ for (const communityId of communityIds) {
523
+ const nodes = communities.get(communityId) ?? [];
524
+ nodes.push(item.id);
525
+ communities.set(communityId, nodes);
526
+ }
527
+ }
528
+ return {
529
+ nodes: index.items.map((item) => ({ id: item.id })),
530
+ edges: Array.from(edgeCounts.values()).sort((left, right) => left.source.localeCompare(right.source) || left.target.localeCompare(right.target) || left.kind.localeCompare(right.kind)),
531
+ communities: Array.from(communities.entries()).map(([id, nodes]) => ({ id, nodes: [...new Set(nodes)].sort() })).sort((left, right) => left.id.localeCompare(right.id))
532
+ };
533
+ }
534
+ function communityIdsFor(item, options) {
535
+ if (options.communityFacet) {
536
+ const values = item.facets[options.communityFacet] ?? [];
537
+ if (values.length > 0) return values.map((value) => `${options.communityFacet}:${value}`);
538
+ }
539
+ if (options.communityTagPrefix) {
540
+ const prefix = options.communityTagPrefix;
541
+ const tags = item.tags.filter((tag) => tag.startsWith(prefix));
542
+ if (tags.length > 0) return tags;
543
+ }
544
+ if (item.concepts.length > 0) return item.concepts.map((concept) => `concept:${concept}`);
545
+ return [`type:${item.type}`];
546
+ }
547
+
548
+ export { aiwgFortemiIndexToCommunityGraph, assertAiwgFortemiChunkManifest, assertAiwgFortemiChunkPart, assertAiwgFortemiIndexExport, createAiwgFetchChunkLoader, createAiwgIndexController, createAiwgReviewDecisionExport, getAiwgFortemiFacets, queryAiwgFortemiIndex, validateAiwgFortemiChunkManifest, validateAiwgFortemiChunkPart, validateAiwgFortemiIndexExport };
549
+ //# sourceMappingURL=aiwg-index.js.map
550
+ //# sourceMappingURL=aiwg-index.js.map