@fortemi/core 2026.6.1 → 2026.6.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +57 -0
- package/dist/aiwg-index.d.ts +242 -0
- package/dist/aiwg-index.js +717 -0
- package/dist/aiwg-index.js.map +1 -0
- package/dist/index.d.ts +57 -133
- package/dist/index.js +832 -66
- package/dist/index.js.map +1 -1
- package/package.json +5 -1
|
@@ -0,0 +1,717 @@
|
|
|
1
|
+
// src/aiwg-index.ts
|
|
2
|
+
var AIWG_SCAN_REQUIRED_FIELDS = [
|
|
3
|
+
"schema_version",
|
|
4
|
+
"id",
|
|
5
|
+
"type",
|
|
6
|
+
"title",
|
|
7
|
+
"text",
|
|
8
|
+
"facets",
|
|
9
|
+
"tags",
|
|
10
|
+
"concepts",
|
|
11
|
+
"privacy"
|
|
12
|
+
];
|
|
13
|
+
var REQUIRED_RECORD_FIELDS = [
|
|
14
|
+
"schema_version",
|
|
15
|
+
"id",
|
|
16
|
+
"type",
|
|
17
|
+
"source",
|
|
18
|
+
"title",
|
|
19
|
+
"text",
|
|
20
|
+
"facets",
|
|
21
|
+
"tags",
|
|
22
|
+
"concepts",
|
|
23
|
+
"relationships",
|
|
24
|
+
"provenance",
|
|
25
|
+
"privacy",
|
|
26
|
+
"updated_at"
|
|
27
|
+
];
|
|
28
|
+
var VALID_TYPES = /* @__PURE__ */ new Set([
|
|
29
|
+
"crm.contact",
|
|
30
|
+
"crm.organization",
|
|
31
|
+
"crm.event",
|
|
32
|
+
"crm.interaction",
|
|
33
|
+
"aiwg.artifact",
|
|
34
|
+
"docs.page"
|
|
35
|
+
]);
|
|
36
|
+
var DEFAULT_QUERY_WEIGHTS = {
|
|
37
|
+
title: 4,
|
|
38
|
+
tag: 3,
|
|
39
|
+
concept: 2,
|
|
40
|
+
text: 1
|
|
41
|
+
};
|
|
42
|
+
function hasString(value) {
|
|
43
|
+
return typeof value === "string" && value.length > 0;
|
|
44
|
+
}
|
|
45
|
+
function pushFacet(counts, name, value) {
|
|
46
|
+
counts[name] ??= {};
|
|
47
|
+
counts[name][value] = (counts[name][value] ?? 0) + 1;
|
|
48
|
+
}
|
|
49
|
+
function hasNonNegativeInteger(value) {
|
|
50
|
+
return Number.isInteger(value) && typeof value === "number" && value >= 0;
|
|
51
|
+
}
|
|
52
|
+
function hasPositiveInteger(value) {
|
|
53
|
+
return Number.isInteger(value) && typeof value === "number" && value > 0;
|
|
54
|
+
}
|
|
55
|
+
function isFacetCounts(value) {
|
|
56
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
57
|
+
return Object.values(value).every((counts) => !!counts && typeof counts === "object" && !Array.isArray(counts) && Object.values(counts).every((count) => hasNonNegativeInteger(count)));
|
|
58
|
+
}
|
|
59
|
+
function validateAiwgFortemiIndexExport(value) {
|
|
60
|
+
const errors = [];
|
|
61
|
+
const counts = {};
|
|
62
|
+
const data = value;
|
|
63
|
+
if (data?.schema_version !== "aiwg.fortemi.index.export.v1") {
|
|
64
|
+
errors.push("schema_version must be aiwg.fortemi.index.export.v1");
|
|
65
|
+
}
|
|
66
|
+
if (!hasString(data?.generated_at)) errors.push("generated_at is required");
|
|
67
|
+
if (!hasString(data?.source?.repo)) errors.push("source.repo is required");
|
|
68
|
+
if (!hasString(data?.source?.privacy)) errors.push("source.privacy is required");
|
|
69
|
+
if (!Array.isArray(data?.items)) errors.push("items must be an array");
|
|
70
|
+
const ids = /* @__PURE__ */ new Set();
|
|
71
|
+
let previousId = "";
|
|
72
|
+
for (const [index, item] of (data.items ?? []).entries()) {
|
|
73
|
+
for (const field of REQUIRED_RECORD_FIELDS) {
|
|
74
|
+
if (!(field in item)) errors.push("items[" + index + "]." + field + " is required");
|
|
75
|
+
}
|
|
76
|
+
if (item.schema_version !== "aiwg.fortemi.index.record.v1") {
|
|
77
|
+
errors.push("items[" + index + "].schema_version must be aiwg.fortemi.index.record.v1");
|
|
78
|
+
}
|
|
79
|
+
if (!hasString(item.id)) errors.push("items[" + index + "].id is required");
|
|
80
|
+
if (hasString(item.id) && ids.has(item.id)) errors.push("duplicate id: " + item.id);
|
|
81
|
+
if (hasString(item.id)) ids.add(item.id);
|
|
82
|
+
if (previousId && hasString(item.id) && previousId.localeCompare(item.id) > 0) {
|
|
83
|
+
errors.push("items must be sorted by id: " + previousId + " before " + item.id);
|
|
84
|
+
}
|
|
85
|
+
if (hasString(item.id)) previousId = item.id;
|
|
86
|
+
if (!VALID_TYPES.has(item.type)) errors.push("items[" + index + "].type is invalid");
|
|
87
|
+
else counts[item.type] = (counts[item.type] ?? 0) + 1;
|
|
88
|
+
if (!hasString(item.source?.path)) errors.push("items[" + index + "].source.path is required");
|
|
89
|
+
if (!hasString(item.source?.repo_relative_path)) errors.push("items[" + index + "].source.repo_relative_path is required");
|
|
90
|
+
if (!hasString(item.source?.locator)) errors.push("items[" + index + "].source.locator is required");
|
|
91
|
+
if (!Array.isArray(item.tags)) errors.push("items[" + index + "].tags must be an array");
|
|
92
|
+
if (!Array.isArray(item.concepts)) errors.push("items[" + index + "].concepts must be an array");
|
|
93
|
+
if (!Array.isArray(item.relationships)) errors.push("items[" + index + "].relationships must be an array");
|
|
94
|
+
if (!Array.isArray(item.provenance) || item.provenance.length === 0) {
|
|
95
|
+
errors.push("items[" + index + "].provenance must be a non-empty array");
|
|
96
|
+
}
|
|
97
|
+
if (!item.privacy || typeof item.privacy.pii !== "boolean" || !hasString(item.privacy.classification)) {
|
|
98
|
+
errors.push("items[" + index + "].privacy requires classification and pii");
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return { valid: errors.length === 0, errors, counts };
|
|
102
|
+
}
|
|
103
|
+
function assertAiwgFortemiIndexExport(value) {
|
|
104
|
+
const result = validateAiwgFortemiIndexExport(value);
|
|
105
|
+
if (!result.valid) {
|
|
106
|
+
throw new Error("Invalid AIWG Fortemi index export:\n" + result.errors.join("\n"));
|
|
107
|
+
}
|
|
108
|
+
return value;
|
|
109
|
+
}
|
|
110
|
+
function validateAiwgFortemiChunkManifest(value) {
|
|
111
|
+
const errors = [];
|
|
112
|
+
const data = value;
|
|
113
|
+
if (data?.schema_version !== "aiwg.fortemi.index.chunk-manifest.v1") {
|
|
114
|
+
errors.push("schema_version must be aiwg.fortemi.index.chunk-manifest.v1");
|
|
115
|
+
}
|
|
116
|
+
if (!hasString(data?.generated_at)) errors.push("generated_at is required");
|
|
117
|
+
if (!hasString(data?.source?.repo)) errors.push("source.repo is required");
|
|
118
|
+
if (!hasString(data?.source?.privacy)) errors.push("source.privacy is required");
|
|
119
|
+
if (!hasNonNegativeInteger(data?.total)) errors.push("total must be a non-negative integer");
|
|
120
|
+
if (!hasPositiveInteger(data?.part_size)) errors.push("part_size must be a positive integer");
|
|
121
|
+
if (data.facets !== void 0 && !isFacetCounts(data.facets)) {
|
|
122
|
+
errors.push("facets must be a nested string-to-number count object");
|
|
123
|
+
}
|
|
124
|
+
if (data.projection !== void 0) {
|
|
125
|
+
if (!Array.isArray(data.projection) || !data.projection.every((field) => typeof field === "string")) {
|
|
126
|
+
errors.push("projection must be an array of field names");
|
|
127
|
+
} else {
|
|
128
|
+
const present = new Set(data.projection);
|
|
129
|
+
for (const field of AIWG_SCAN_REQUIRED_FIELDS) {
|
|
130
|
+
if (!present.has(field)) errors.push("projection must include scan-required field " + field);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
if (data.detail !== void 0) {
|
|
135
|
+
if (!hasString(data.detail.href)) errors.push("detail.href is required");
|
|
136
|
+
else if (!data.detail.href.includes("{id}")) errors.push("detail.href must contain the {id} placeholder");
|
|
137
|
+
}
|
|
138
|
+
if (!Array.isArray(data?.parts)) errors.push("parts must be an array");
|
|
139
|
+
let expectedOffset = 0;
|
|
140
|
+
const parts = Array.isArray(data?.parts) ? data.parts : [];
|
|
141
|
+
for (const [index, part] of parts.entries()) {
|
|
142
|
+
if (!hasString(part.href)) errors.push("parts[" + index + "].href is required");
|
|
143
|
+
if (!hasNonNegativeInteger(part.offset)) errors.push("parts[" + index + "].offset must be a non-negative integer");
|
|
144
|
+
if (!hasNonNegativeInteger(part.count)) errors.push("parts[" + index + "].count must be a non-negative integer");
|
|
145
|
+
if (hasNonNegativeInteger(part.offset) && part.offset !== expectedOffset) {
|
|
146
|
+
errors.push("parts[" + index + "].offset must be " + expectedOffset);
|
|
147
|
+
}
|
|
148
|
+
if (hasNonNegativeInteger(part.count)) expectedOffset += part.count;
|
|
149
|
+
}
|
|
150
|
+
if (hasNonNegativeInteger(data?.total) && expectedOffset !== data.total) {
|
|
151
|
+
errors.push("parts counts must add up to total");
|
|
152
|
+
}
|
|
153
|
+
return { valid: errors.length === 0, errors };
|
|
154
|
+
}
|
|
155
|
+
function assertAiwgFortemiChunkManifest(value) {
|
|
156
|
+
const result = validateAiwgFortemiChunkManifest(value);
|
|
157
|
+
if (!result.valid) {
|
|
158
|
+
throw new Error("Invalid AIWG Fortemi chunk manifest:\n" + result.errors.join("\n"));
|
|
159
|
+
}
|
|
160
|
+
return value;
|
|
161
|
+
}
|
|
162
|
+
function validateProjectedRecords(items) {
|
|
163
|
+
const errors = [];
|
|
164
|
+
const ids = /* @__PURE__ */ new Set();
|
|
165
|
+
let previousId = "";
|
|
166
|
+
for (const [index, item] of items.entries()) {
|
|
167
|
+
if (item.schema_version !== "aiwg.fortemi.index.record.v1") {
|
|
168
|
+
errors.push("items[" + index + "].schema_version must be aiwg.fortemi.index.record.v1");
|
|
169
|
+
}
|
|
170
|
+
if (!hasString(item.id)) errors.push("items[" + index + "].id is required");
|
|
171
|
+
if (hasString(item.id) && ids.has(item.id)) errors.push("duplicate id: " + item.id);
|
|
172
|
+
if (hasString(item.id)) ids.add(item.id);
|
|
173
|
+
if (previousId && hasString(item.id) && previousId.localeCompare(item.id) > 0) {
|
|
174
|
+
errors.push("items must be sorted by id: " + previousId + " before " + item.id);
|
|
175
|
+
}
|
|
176
|
+
if (hasString(item.id)) previousId = item.id;
|
|
177
|
+
if (!item.type || !VALID_TYPES.has(item.type)) errors.push("items[" + index + "].type is invalid");
|
|
178
|
+
if (!hasString(item.title)) errors.push("items[" + index + "].title is required");
|
|
179
|
+
if (typeof item.text !== "string") errors.push("items[" + index + "].text is required");
|
|
180
|
+
if (!item.facets || typeof item.facets !== "object" || Array.isArray(item.facets)) {
|
|
181
|
+
errors.push("items[" + index + "].facets must be an object");
|
|
182
|
+
}
|
|
183
|
+
if (!Array.isArray(item.tags)) errors.push("items[" + index + "].tags must be an array");
|
|
184
|
+
if (!Array.isArray(item.concepts)) errors.push("items[" + index + "].concepts must be an array");
|
|
185
|
+
if (!item.privacy || !hasString(item.privacy.classification)) {
|
|
186
|
+
errors.push("items[" + index + "].privacy.classification is required");
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return errors;
|
|
190
|
+
}
|
|
191
|
+
function validateAiwgFortemiChunkPart(value, partRef, manifest) {
|
|
192
|
+
const errors = [];
|
|
193
|
+
const data = value;
|
|
194
|
+
if (data?.schema_version !== "aiwg.fortemi.index.chunk.v1") {
|
|
195
|
+
errors.push("schema_version must be aiwg.fortemi.index.chunk.v1");
|
|
196
|
+
}
|
|
197
|
+
if (data?.manifest_schema_version !== "aiwg.fortemi.index.chunk-manifest.v1") {
|
|
198
|
+
errors.push("manifest_schema_version must be aiwg.fortemi.index.chunk-manifest.v1");
|
|
199
|
+
}
|
|
200
|
+
if (!hasNonNegativeInteger(data?.offset)) errors.push("offset must be a non-negative integer");
|
|
201
|
+
if (!Array.isArray(data?.items)) errors.push("items must be an array");
|
|
202
|
+
if (partRef && hasNonNegativeInteger(data?.offset) && data.offset !== partRef.offset) {
|
|
203
|
+
errors.push("offset must match manifest part offset " + partRef.offset);
|
|
204
|
+
}
|
|
205
|
+
if (partRef && Array.isArray(data?.items) && data.items.length !== partRef.count) {
|
|
206
|
+
errors.push("items length must match manifest part count " + partRef.count);
|
|
207
|
+
}
|
|
208
|
+
if (Array.isArray(data?.items)) {
|
|
209
|
+
if (manifest?.projection) {
|
|
210
|
+
errors.push(...validateProjectedRecords(data.items).map((error) => "items." + error));
|
|
211
|
+
} else {
|
|
212
|
+
const validation = validateAiwgFortemiIndexExport({
|
|
213
|
+
schema_version: "aiwg.fortemi.index.export.v1",
|
|
214
|
+
generated_at: manifest?.generated_at ?? "1970-01-01T00:00:00.000Z",
|
|
215
|
+
source: manifest?.source ?? { repo: "chunk", privacy: "public" },
|
|
216
|
+
items: data.items
|
|
217
|
+
});
|
|
218
|
+
errors.push(...validation.errors.map((error) => "items." + error));
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
return { valid: errors.length === 0, errors };
|
|
222
|
+
}
|
|
223
|
+
function assertAiwgFortemiChunkPart(value, partRef, manifest) {
|
|
224
|
+
const result = validateAiwgFortemiChunkPart(value, partRef, manifest);
|
|
225
|
+
if (!result.valid) {
|
|
226
|
+
throw new Error("Invalid AIWG Fortemi chunk part:\n" + result.errors.join("\n"));
|
|
227
|
+
}
|
|
228
|
+
return value;
|
|
229
|
+
}
|
|
230
|
+
function createAiwgFetchChunkLoader(baseUrl) {
|
|
231
|
+
return async (part) => {
|
|
232
|
+
const href = baseUrl ? new URL(part.href, baseUrl).toString() : part.href;
|
|
233
|
+
const response = await fetch(href);
|
|
234
|
+
if (!response.ok) throw new Error("Failed to fetch AIWG index chunk " + href + ": " + response.status);
|
|
235
|
+
return response.json();
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
function createAiwgFetchDetailLoader(baseUrl) {
|
|
239
|
+
return async (id, manifest) => {
|
|
240
|
+
if (!manifest.detail?.href) throw new Error("Manifest has no detail.href for record resolution");
|
|
241
|
+
const relative = manifest.detail.href.replace("{id}", encodeURIComponent(id));
|
|
242
|
+
const href = baseUrl ? new URL(relative, baseUrl).toString() : relative;
|
|
243
|
+
const response = await fetch(href);
|
|
244
|
+
if (!response.ok) throw new Error("Failed to fetch AIWG index detail " + href + ": " + response.status);
|
|
245
|
+
return response.json();
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
function getAiwgFortemiFacets(items) {
|
|
249
|
+
const result = {};
|
|
250
|
+
for (const item of items) {
|
|
251
|
+
pushFacet(result, "type", item.type);
|
|
252
|
+
pushFacet(result, "privacy", item.privacy.classification);
|
|
253
|
+
for (const tag of item.tags) pushFacet(result, "tag", tag);
|
|
254
|
+
for (const concept of item.concepts) pushFacet(result, "concept", concept);
|
|
255
|
+
for (const [name, values] of Object.entries(item.facets)) {
|
|
256
|
+
for (const value of values) pushFacet(result, name, value);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
return result;
|
|
260
|
+
}
|
|
261
|
+
function buildAiwgChunkedIndex(index, options = {}) {
|
|
262
|
+
const partSize = hasPositiveInteger(options.partSize) ? options.partSize : 500;
|
|
263
|
+
const projection = options.projection;
|
|
264
|
+
const items = index.items;
|
|
265
|
+
const pad = (value) => String(value).padStart(4, "0");
|
|
266
|
+
const project = (record) => {
|
|
267
|
+
if (!projection) return record;
|
|
268
|
+
const slim = {};
|
|
269
|
+
for (const field of projection) slim[field] = record[field];
|
|
270
|
+
return slim;
|
|
271
|
+
};
|
|
272
|
+
const parts = [];
|
|
273
|
+
const partRefs = [];
|
|
274
|
+
for (let offset = 0, partIndex = 0; offset < items.length; offset += partSize, partIndex += 1) {
|
|
275
|
+
const slice = items.slice(offset, offset + partSize);
|
|
276
|
+
const href = "part-" + pad(partIndex) + ".json";
|
|
277
|
+
parts.push({
|
|
278
|
+
href,
|
|
279
|
+
part: {
|
|
280
|
+
schema_version: "aiwg.fortemi.index.chunk.v1",
|
|
281
|
+
manifest_schema_version: "aiwg.fortemi.index.chunk-manifest.v1",
|
|
282
|
+
offset,
|
|
283
|
+
items: slice.map(project)
|
|
284
|
+
}
|
|
285
|
+
});
|
|
286
|
+
partRefs.push({ href, offset, count: slice.length });
|
|
287
|
+
}
|
|
288
|
+
const manifest = {
|
|
289
|
+
schema_version: "aiwg.fortemi.index.chunk-manifest.v1",
|
|
290
|
+
generated_at: options.generatedAt ?? index.generated_at,
|
|
291
|
+
source: index.source,
|
|
292
|
+
total: items.length,
|
|
293
|
+
part_size: partSize,
|
|
294
|
+
facets: getAiwgFortemiFacets(items),
|
|
295
|
+
parts: partRefs,
|
|
296
|
+
...projection ? { projection, detail: { href: options.detailHref ?? "detail/{id}.json" } } : {}
|
|
297
|
+
};
|
|
298
|
+
return {
|
|
299
|
+
manifest,
|
|
300
|
+
parts,
|
|
301
|
+
details: projection ? items.map((record) => ({ id: record.id, record })) : []
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
function includesAll(actual, expected) {
|
|
305
|
+
if (!expected || expected.length === 0) return true;
|
|
306
|
+
const actualSet = new Set(actual);
|
|
307
|
+
return expected.every((value) => actualSet.has(value));
|
|
308
|
+
}
|
|
309
|
+
function matchesFacetFilters(item, filters) {
|
|
310
|
+
if (!filters) return true;
|
|
311
|
+
return Object.entries(filters).every(([name, expected]) => includesAll(item.facets[name] ?? [], expected));
|
|
312
|
+
}
|
|
313
|
+
function queryMatches(item, q) {
|
|
314
|
+
if (!q) return [];
|
|
315
|
+
const matches = [];
|
|
316
|
+
if (item.title.toLowerCase().includes(q)) matches.push({ field: "title", value: item.title });
|
|
317
|
+
if (item.text.toLowerCase().includes(q)) matches.push({ field: "text", value: item.text });
|
|
318
|
+
for (const tag of item.tags) {
|
|
319
|
+
if (tag.toLowerCase().includes(q)) matches.push({ field: "tag", value: tag });
|
|
320
|
+
}
|
|
321
|
+
for (const concept of item.concepts) {
|
|
322
|
+
if (concept.toLowerCase().includes(q)) matches.push({ field: "concept", value: concept });
|
|
323
|
+
}
|
|
324
|
+
return matches;
|
|
325
|
+
}
|
|
326
|
+
function rankMatches(matches, weights) {
|
|
327
|
+
return matches.reduce((total, match) => total + weights[match.field], 0);
|
|
328
|
+
}
|
|
329
|
+
function clipSnippet(value, q, maxLength) {
|
|
330
|
+
const normalizedLength = Math.max(20, maxLength);
|
|
331
|
+
if (!value) return "";
|
|
332
|
+
if (!q) return value.length > normalizedLength ? `${value.slice(0, normalizedLength).trimEnd()}...` : value;
|
|
333
|
+
const lower = value.toLowerCase();
|
|
334
|
+
const index = lower.indexOf(q);
|
|
335
|
+
if (index < 0) return value.length > normalizedLength ? `${value.slice(0, normalizedLength).trimEnd()}...` : value;
|
|
336
|
+
const context = Math.max(0, Math.floor((normalizedLength - q.length) / 2));
|
|
337
|
+
const start = Math.max(0, index - context);
|
|
338
|
+
const end = Math.min(value.length, start + normalizedLength);
|
|
339
|
+
const prefix = start > 0 ? "..." : "";
|
|
340
|
+
const suffix = end < value.length ? "..." : "";
|
|
341
|
+
return `${prefix}${value.slice(start, end).trim()}${suffix}`;
|
|
342
|
+
}
|
|
343
|
+
function createSnippet(item, matches, q, maxLength) {
|
|
344
|
+
const textMatch = matches.find((match) => match.field === "text");
|
|
345
|
+
const titleMatch = matches.find((match) => match.field === "title");
|
|
346
|
+
const firstMatch = textMatch ?? titleMatch ?? matches[0];
|
|
347
|
+
return clipSnippet(firstMatch?.value ?? item.text, q, maxLength);
|
|
348
|
+
}
|
|
349
|
+
function createRankedEntries(items, q, options, ordinalBase = 0) {
|
|
350
|
+
const weights = { ...DEFAULT_QUERY_WEIGHTS, ...options.weights };
|
|
351
|
+
return items.map((item, ordinal) => ({ item, ordinal: ordinalBase + ordinal, matches: queryMatches(item, q) })).filter(({ item, matches }) => {
|
|
352
|
+
if (q && matches.length === 0) return false;
|
|
353
|
+
if (options.types && !options.types.includes(item.type)) return false;
|
|
354
|
+
if (options.privacy && !options.privacy.includes(item.privacy.classification)) return false;
|
|
355
|
+
if (!includesAll(item.tags, options.tags)) return false;
|
|
356
|
+
if (!includesAll(item.concepts, options.concepts)) return false;
|
|
357
|
+
if (!matchesFacetFilters(item, options.facets)) return false;
|
|
358
|
+
if (options.relationshipTargetId && !(item.relationships ?? []).some((rel) => rel.target_id === options.relationshipTargetId)) {
|
|
359
|
+
return false;
|
|
360
|
+
}
|
|
361
|
+
return true;
|
|
362
|
+
}).map(({ item, ordinal, matches }) => ({
|
|
363
|
+
item,
|
|
364
|
+
ordinal,
|
|
365
|
+
rank: rankMatches(matches, weights),
|
|
366
|
+
matches
|
|
367
|
+
}));
|
|
368
|
+
}
|
|
369
|
+
function sortRankedEntries(entries, rank) {
|
|
370
|
+
return [...entries].sort((left, right) => {
|
|
371
|
+
if (rank) return right.rank - left.rank || left.ordinal - right.ordinal;
|
|
372
|
+
return left.ordinal - right.ordinal;
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
function createQueryResultFromRankedEntries(entries, query, options) {
|
|
376
|
+
const ranked = sortRankedEntries(entries, options.rank);
|
|
377
|
+
const offset = options.offset ?? 0;
|
|
378
|
+
const limit = options.limit ?? ranked.length;
|
|
379
|
+
const page = ranked.slice(offset, offset + limit);
|
|
380
|
+
const result = {
|
|
381
|
+
items: page.map((entry) => entry.item),
|
|
382
|
+
total: ranked.length,
|
|
383
|
+
facets: getAiwgFortemiFacets(ranked.map((entry) => entry.item))
|
|
384
|
+
};
|
|
385
|
+
if (options.rank || options.snippets || options.includeMatches) {
|
|
386
|
+
const snippetLength = options.snippetLength ?? 160;
|
|
387
|
+
result.rankedItems = page.map((entry) => ({
|
|
388
|
+
item: entry.item,
|
|
389
|
+
rank: entry.rank,
|
|
390
|
+
...options.snippets ? { snippet: createSnippet(entry.item, entry.matches, query, snippetLength) } : {},
|
|
391
|
+
...options.includeMatches ? { matches: entry.matches } : {}
|
|
392
|
+
}));
|
|
393
|
+
}
|
|
394
|
+
return result;
|
|
395
|
+
}
|
|
396
|
+
function queryAiwgFortemiIndex(index, query = "", options = {}) {
|
|
397
|
+
const q = query.trim().toLowerCase();
|
|
398
|
+
return createQueryResultFromRankedEntries(createRankedEntries(index.items, q, options), q, options);
|
|
399
|
+
}
|
|
400
|
+
function chunkPartCacheKey(part) {
|
|
401
|
+
return `${part.offset}:${part.href}`;
|
|
402
|
+
}
|
|
403
|
+
function clampMaxCachedParts(value) {
|
|
404
|
+
if (!hasPositiveInteger(value)) return 3;
|
|
405
|
+
return value;
|
|
406
|
+
}
|
|
407
|
+
function clampMaxCachedDetails(value) {
|
|
408
|
+
if (!hasPositiveInteger(value)) return 32;
|
|
409
|
+
return value;
|
|
410
|
+
}
|
|
411
|
+
function isDirectChunkBrowse(query, options) {
|
|
412
|
+
return query.trim() === "" && !options.rank && !options.snippets && !options.includeMatches && !options.types && !options.facets && !options.tags && !options.concepts && !options.privacy && !options.relationshipTargetId;
|
|
413
|
+
}
|
|
414
|
+
function getPartsForRange(manifest, offset, limit) {
|
|
415
|
+
const end = offset + limit;
|
|
416
|
+
return manifest.parts.filter((part) => part.count > 0 && part.offset < end && part.offset + part.count > offset);
|
|
417
|
+
}
|
|
418
|
+
async function loadChunkPart(runtime, part) {
|
|
419
|
+
const key = chunkPartCacheKey(part);
|
|
420
|
+
const cached = runtime.partCache.get(key);
|
|
421
|
+
if (cached) {
|
|
422
|
+
runtime.partCache.delete(key);
|
|
423
|
+
runtime.partCache.set(key, cached);
|
|
424
|
+
return { part: cached, fetched: false };
|
|
425
|
+
}
|
|
426
|
+
const parsed = assertAiwgFortemiChunkPart(await runtime.loader(part, runtime.manifest), part, runtime.manifest);
|
|
427
|
+
runtime.partCache.set(key, parsed);
|
|
428
|
+
while (runtime.partCache.size > runtime.maxCachedParts) {
|
|
429
|
+
const oldest = runtime.partCache.keys().next().value;
|
|
430
|
+
if (oldest === void 0) break;
|
|
431
|
+
runtime.partCache.delete(oldest);
|
|
432
|
+
}
|
|
433
|
+
return { part: parsed, fetched: true };
|
|
434
|
+
}
|
|
435
|
+
async function getChunkRecord(runtime, id) {
|
|
436
|
+
const cached = runtime.detailCache.get(id);
|
|
437
|
+
if (cached) {
|
|
438
|
+
runtime.detailCache.delete(id);
|
|
439
|
+
runtime.detailCache.set(id, cached);
|
|
440
|
+
return cached;
|
|
441
|
+
}
|
|
442
|
+
if (!runtime.manifest.projection) {
|
|
443
|
+
for (const part of runtime.partCache.values()) {
|
|
444
|
+
const found = part.items.find((item) => item.id === id);
|
|
445
|
+
if (found) return found;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
if (!runtime.detailLoader) {
|
|
449
|
+
throw new Error("No detailLoader configured to resolve record " + id);
|
|
450
|
+
}
|
|
451
|
+
const raw = await runtime.detailLoader(id, runtime.manifest);
|
|
452
|
+
const record = assertAiwgFortemiIndexExport({
|
|
453
|
+
schema_version: "aiwg.fortemi.index.export.v1",
|
|
454
|
+
generated_at: runtime.manifest.generated_at,
|
|
455
|
+
source: runtime.manifest.source,
|
|
456
|
+
items: [raw]
|
|
457
|
+
}).items[0];
|
|
458
|
+
if (record.id !== id) {
|
|
459
|
+
throw new Error("Detail record id mismatch: expected " + id + ", got " + record.id);
|
|
460
|
+
}
|
|
461
|
+
runtime.detailCache.set(id, record);
|
|
462
|
+
while (runtime.detailCache.size > runtime.maxCachedDetails) {
|
|
463
|
+
const oldest = runtime.detailCache.keys().next().value;
|
|
464
|
+
if (oldest === void 0) break;
|
|
465
|
+
runtime.detailCache.delete(oldest);
|
|
466
|
+
}
|
|
467
|
+
return record;
|
|
468
|
+
}
|
|
469
|
+
async function queryChunkedAiwgFortemiIndex(runtime, query = "", options = {}) {
|
|
470
|
+
const q = query.trim().toLowerCase();
|
|
471
|
+
let scannedParts = 0;
|
|
472
|
+
let fetchedParts = 0;
|
|
473
|
+
if (isDirectChunkBrowse(query, options)) {
|
|
474
|
+
const offset = options.offset ?? 0;
|
|
475
|
+
const limit = options.limit ?? runtime.manifest.total;
|
|
476
|
+
const parts = getPartsForRange(runtime.manifest, offset, limit);
|
|
477
|
+
const items = [];
|
|
478
|
+
for (const partRef of parts) {
|
|
479
|
+
const loaded = await loadChunkPart(runtime, partRef);
|
|
480
|
+
if (loaded.fetched) fetchedParts += 1;
|
|
481
|
+
scannedParts += 1;
|
|
482
|
+
options.onProgress?.({ phase: "part", done: scannedParts, total: parts.length, href: partRef.href });
|
|
483
|
+
const start = Math.max(0, offset - partRef.offset);
|
|
484
|
+
const end = Math.min(loaded.part.items.length, offset + limit - partRef.offset);
|
|
485
|
+
items.push(...loaded.part.items.slice(start, end));
|
|
486
|
+
}
|
|
487
|
+
return {
|
|
488
|
+
items,
|
|
489
|
+
total: runtime.manifest.total,
|
|
490
|
+
facets: runtime.manifest.facets ?? {},
|
|
491
|
+
manifestTotal: runtime.manifest.total,
|
|
492
|
+
scannedParts,
|
|
493
|
+
fetchedParts,
|
|
494
|
+
complete: true
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
const entries = [];
|
|
498
|
+
for (const partRef of runtime.manifest.parts) {
|
|
499
|
+
const loaded = await loadChunkPart(runtime, partRef);
|
|
500
|
+
if (loaded.fetched) fetchedParts += 1;
|
|
501
|
+
scannedParts += 1;
|
|
502
|
+
options.onProgress?.({ phase: "part", done: scannedParts, total: runtime.manifest.parts.length, href: partRef.href });
|
|
503
|
+
entries.push(...createRankedEntries(loaded.part.items, q, options, partRef.offset));
|
|
504
|
+
options.onProgress?.({ phase: "query", done: scannedParts, total: runtime.manifest.parts.length, href: partRef.href });
|
|
505
|
+
}
|
|
506
|
+
return {
|
|
507
|
+
...createQueryResultFromRankedEntries(entries, q, options),
|
|
508
|
+
manifestTotal: runtime.manifest.total,
|
|
509
|
+
scannedParts,
|
|
510
|
+
fetchedParts,
|
|
511
|
+
complete: true
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
function createAiwgReviewDecisionExport(source, decisions, generatedAt = (/* @__PURE__ */ new Date()).toISOString()) {
|
|
515
|
+
return {
|
|
516
|
+
schema_version: "aiwg.fortemi.review-decisions.v1",
|
|
517
|
+
generated_at: generatedAt,
|
|
518
|
+
source_export_schema_version: source.schema_version,
|
|
519
|
+
decisions: [...decisions].sort((left, right) => left.item_id.localeCompare(right.item_id))
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
function createAiwgIndexController(initialIndex) {
|
|
523
|
+
let index = initialIndex ?? null;
|
|
524
|
+
let chunked = null;
|
|
525
|
+
let data = null;
|
|
526
|
+
let error = null;
|
|
527
|
+
let reviewDecisions = [];
|
|
528
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
529
|
+
const snapshot = () => ({
|
|
530
|
+
index,
|
|
531
|
+
chunked: chunked ? {
|
|
532
|
+
manifest: chunked.manifest,
|
|
533
|
+
cachedParts: chunked.partCache.size,
|
|
534
|
+
maxCachedParts: chunked.maxCachedParts
|
|
535
|
+
} : null,
|
|
536
|
+
data,
|
|
537
|
+
error,
|
|
538
|
+
reviewDecisions: [...reviewDecisions]
|
|
539
|
+
});
|
|
540
|
+
const notify = () => {
|
|
541
|
+
const current = snapshot();
|
|
542
|
+
for (const listener of listeners) listener(current);
|
|
543
|
+
};
|
|
544
|
+
const requireIndex = () => {
|
|
545
|
+
if (!index) throw new Error("No AIWG index export loaded");
|
|
546
|
+
return index;
|
|
547
|
+
};
|
|
548
|
+
return {
|
|
549
|
+
loadIndex(value) {
|
|
550
|
+
try {
|
|
551
|
+
const parsed = assertAiwgFortemiIndexExport(value);
|
|
552
|
+
index = parsed;
|
|
553
|
+
chunked = null;
|
|
554
|
+
data = null;
|
|
555
|
+
reviewDecisions = [];
|
|
556
|
+
error = null;
|
|
557
|
+
notify();
|
|
558
|
+
return parsed;
|
|
559
|
+
} catch (err) {
|
|
560
|
+
error = err instanceof Error ? err : new Error(String(err));
|
|
561
|
+
notify();
|
|
562
|
+
throw error;
|
|
563
|
+
}
|
|
564
|
+
},
|
|
565
|
+
loadChunkedIndex(manifest, loader, options = {}) {
|
|
566
|
+
try {
|
|
567
|
+
const parsed = assertAiwgFortemiChunkManifest(manifest);
|
|
568
|
+
index = null;
|
|
569
|
+
chunked = {
|
|
570
|
+
manifest: parsed,
|
|
571
|
+
loader,
|
|
572
|
+
maxCachedParts: clampMaxCachedParts(options.maxCachedParts),
|
|
573
|
+
partCache: /* @__PURE__ */ new Map(),
|
|
574
|
+
detailLoader: options.detailLoader,
|
|
575
|
+
maxCachedDetails: clampMaxCachedDetails(options.maxCachedDetails),
|
|
576
|
+
detailCache: /* @__PURE__ */ new Map()
|
|
577
|
+
};
|
|
578
|
+
data = null;
|
|
579
|
+
reviewDecisions = [];
|
|
580
|
+
error = null;
|
|
581
|
+
notify();
|
|
582
|
+
return parsed;
|
|
583
|
+
} catch (err) {
|
|
584
|
+
error = err instanceof Error ? err : new Error(String(err));
|
|
585
|
+
notify();
|
|
586
|
+
throw error;
|
|
587
|
+
}
|
|
588
|
+
},
|
|
589
|
+
getIndex() {
|
|
590
|
+
return index;
|
|
591
|
+
},
|
|
592
|
+
getChunkedManifest() {
|
|
593
|
+
return chunked?.manifest ?? null;
|
|
594
|
+
},
|
|
595
|
+
getSnapshot() {
|
|
596
|
+
return snapshot();
|
|
597
|
+
},
|
|
598
|
+
query(query = "", options) {
|
|
599
|
+
const result = queryAiwgFortemiIndex(requireIndex(), query, options);
|
|
600
|
+
data = result;
|
|
601
|
+
error = null;
|
|
602
|
+
notify();
|
|
603
|
+
return result;
|
|
604
|
+
},
|
|
605
|
+
async queryChunked(query = "", options) {
|
|
606
|
+
if (!chunked) throw new Error("No AIWG chunked index manifest loaded");
|
|
607
|
+
try {
|
|
608
|
+
const result = await queryChunkedAiwgFortemiIndex(chunked, query, options);
|
|
609
|
+
data = result;
|
|
610
|
+
error = null;
|
|
611
|
+
notify();
|
|
612
|
+
return result;
|
|
613
|
+
} catch (err) {
|
|
614
|
+
error = err instanceof Error ? err : new Error(String(err));
|
|
615
|
+
notify();
|
|
616
|
+
throw error;
|
|
617
|
+
}
|
|
618
|
+
},
|
|
619
|
+
async getRecord(id) {
|
|
620
|
+
if (chunked) {
|
|
621
|
+
try {
|
|
622
|
+
return await getChunkRecord(chunked, id);
|
|
623
|
+
} catch (err) {
|
|
624
|
+
error = err instanceof Error ? err : new Error(String(err));
|
|
625
|
+
notify();
|
|
626
|
+
throw error;
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
const found = requireIndex().items.find((item) => item.id === id);
|
|
630
|
+
if (!found) throw new Error("Record not found: " + id);
|
|
631
|
+
return found;
|
|
632
|
+
},
|
|
633
|
+
clearChunkCache() {
|
|
634
|
+
chunked?.partCache.clear();
|
|
635
|
+
chunked?.detailCache.clear();
|
|
636
|
+
error = null;
|
|
637
|
+
notify();
|
|
638
|
+
},
|
|
639
|
+
toCommunityGraph(options) {
|
|
640
|
+
return aiwgFortemiIndexToCommunityGraph(requireIndex(), options);
|
|
641
|
+
},
|
|
642
|
+
setReviewDecision(input) {
|
|
643
|
+
const decision = {
|
|
644
|
+
...input,
|
|
645
|
+
updated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
646
|
+
};
|
|
647
|
+
reviewDecisions = [
|
|
648
|
+
...reviewDecisions.filter((item) => item.item_id !== decision.item_id),
|
|
649
|
+
decision
|
|
650
|
+
].sort((left, right) => left.item_id.localeCompare(right.item_id));
|
|
651
|
+
error = null;
|
|
652
|
+
notify();
|
|
653
|
+
return decision;
|
|
654
|
+
},
|
|
655
|
+
clearReviewDecision(itemId) {
|
|
656
|
+
reviewDecisions = reviewDecisions.filter((item) => item.item_id !== itemId);
|
|
657
|
+
error = null;
|
|
658
|
+
notify();
|
|
659
|
+
},
|
|
660
|
+
createReviewDecisionExport(generatedAt) {
|
|
661
|
+
return createAiwgReviewDecisionExport(requireIndex(), reviewDecisions, generatedAt);
|
|
662
|
+
},
|
|
663
|
+
subscribe(listener) {
|
|
664
|
+
listeners.add(listener);
|
|
665
|
+
return () => {
|
|
666
|
+
listeners.delete(listener);
|
|
667
|
+
};
|
|
668
|
+
}
|
|
669
|
+
};
|
|
670
|
+
}
|
|
671
|
+
function aiwgFortemiIndexToCommunityGraph(index, options = {}) {
|
|
672
|
+
const ids = new Set(index.items.map((item) => item.id));
|
|
673
|
+
const relationshipWeights = options.relationshipWeights ?? {};
|
|
674
|
+
const edgeCounts = /* @__PURE__ */ new Map();
|
|
675
|
+
for (const item of index.items) {
|
|
676
|
+
for (const relationship of item.relationships) {
|
|
677
|
+
if (!ids.has(relationship.target_id) && !options.includeDanglingRelationships) continue;
|
|
678
|
+
const kind = relationship.type;
|
|
679
|
+
const baseWeight = relationshipWeights[kind] ?? 1;
|
|
680
|
+
const key = `${item.id}\0${relationship.target_id}\0${kind}`;
|
|
681
|
+
const existing = edgeCounts.get(key);
|
|
682
|
+
if (existing) existing.weight += baseWeight;
|
|
683
|
+
else edgeCounts.set(key, { source: item.id, target: relationship.target_id, kind, weight: baseWeight });
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
const communities = /* @__PURE__ */ new Map();
|
|
687
|
+
for (const item of index.items) {
|
|
688
|
+
const communityIds = communityIdsFor(item, options);
|
|
689
|
+
for (const communityId of communityIds) {
|
|
690
|
+
const nodes = communities.get(communityId) ?? [];
|
|
691
|
+
nodes.push(item.id);
|
|
692
|
+
communities.set(communityId, nodes);
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
return {
|
|
696
|
+
nodes: index.items.map((item) => ({ id: item.id })),
|
|
697
|
+
edges: Array.from(edgeCounts.values()).sort((left, right) => left.source.localeCompare(right.source) || left.target.localeCompare(right.target) || left.kind.localeCompare(right.kind)),
|
|
698
|
+
communities: Array.from(communities.entries()).map(([id, nodes]) => ({ id, nodes: [...new Set(nodes)].sort() })).sort((left, right) => left.id.localeCompare(right.id))
|
|
699
|
+
};
|
|
700
|
+
}
|
|
701
|
+
function communityIdsFor(item, options) {
|
|
702
|
+
if (options.communityFacet) {
|
|
703
|
+
const values = item.facets[options.communityFacet] ?? [];
|
|
704
|
+
if (values.length > 0) return values.map((value) => `${options.communityFacet}:${value}`);
|
|
705
|
+
}
|
|
706
|
+
if (options.communityTagPrefix) {
|
|
707
|
+
const prefix = options.communityTagPrefix;
|
|
708
|
+
const tags = item.tags.filter((tag) => tag.startsWith(prefix));
|
|
709
|
+
if (tags.length > 0) return tags;
|
|
710
|
+
}
|
|
711
|
+
if (item.concepts.length > 0) return item.concepts.map((concept) => `concept:${concept}`);
|
|
712
|
+
return [`type:${item.type}`];
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
export { AIWG_SCAN_REQUIRED_FIELDS, aiwgFortemiIndexToCommunityGraph, assertAiwgFortemiChunkManifest, assertAiwgFortemiChunkPart, assertAiwgFortemiIndexExport, buildAiwgChunkedIndex, createAiwgFetchChunkLoader, createAiwgFetchDetailLoader, createAiwgIndexController, createAiwgReviewDecisionExport, getAiwgFortemiFacets, queryAiwgFortemiIndex, validateAiwgFortemiChunkManifest, validateAiwgFortemiChunkPart, validateAiwgFortemiIndexExport };
|
|
716
|
+
//# sourceMappingURL=aiwg-index.js.map
|
|
717
|
+
//# sourceMappingURL=aiwg-index.js.map
|