@fortemi/core 2026.6.2 → 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 CHANGED
@@ -15,6 +15,7 @@ pnpm add @fortemi/core
15
15
  [![License: AGPL-3.0-only](https://img.shields.io/badge/License-AGPL--3.0--only-blue.svg?style=flat-square)](https://github.com/Fortemi/fortemi-react/blob/main/LICENSE)
16
16
  [![Node Version](https://img.shields.io/badge/node-%3E%3D22.0.0-brightgreen?style=flat-square&logo=node.js)](https://nodejs.org)
17
17
  [![TypeScript](https://img.shields.io/badge/TypeScript-5.x-blue?style=flat-square&logo=typescript)](https://www.typescriptlang.org)
18
+ [![Built with aiwg](https://img.shields.io/npm/v/aiwg?label=built%20with%20aiwg&color=CB3837&logo=npm&style=flat-square)](https://www.npmjs.com/package/aiwg)
18
19
 
19
20
  [**Install**](#installation) · [**Why Fortemi**](#why-fortemi-core) · [**Quick Start**](#quick-start) · [**Surface**](#what-you-get) · [**Tools**](#tool-surface) · [**Docs**](#documentation) · [**License**](#license)
20
21
 
@@ -51,6 +51,11 @@ interface AiwgFortemiChunkPartRef {
51
51
  offset: number;
52
52
  count: number;
53
53
  }
54
+ declare const AIWG_SCAN_REQUIRED_FIELDS: Array<keyof AiwgFortemiRecord>;
55
+ type AiwgFortemiProjectedRecord = Pick<AiwgFortemiRecord, 'schema_version' | 'id' | 'type' | 'title' | 'text' | 'facets' | 'tags' | 'concepts' | 'privacy'> & Partial<AiwgFortemiRecord>;
56
+ interface AiwgFortemiChunkDetailRef {
57
+ href: string;
58
+ }
54
59
  interface AiwgFortemiChunkManifest {
55
60
  schema_version: 'aiwg.fortemi.index.chunk-manifest.v1';
56
61
  generated_at: string;
@@ -58,6 +63,8 @@ interface AiwgFortemiChunkManifest {
58
63
  total: number;
59
64
  part_size: number;
60
65
  facets?: Record<string, Record<string, number>>;
66
+ projection?: Array<keyof AiwgFortemiRecord>;
67
+ detail?: AiwgFortemiChunkDetailRef;
61
68
  parts: AiwgFortemiChunkPartRef[];
62
69
  }
63
70
  interface AiwgFortemiChunkPart {
@@ -113,8 +120,11 @@ interface AiwgIndexQueryResult {
113
120
  rankedItems?: AiwgIndexQueryRankedItem[];
114
121
  }
115
122
  type AiwgChunkedIndexLoader = (part: AiwgFortemiChunkPartRef, manifest: AiwgFortemiChunkManifest) => Promise<unknown>;
123
+ type AiwgChunkedIndexDetailLoader = (id: string, manifest: AiwgFortemiChunkManifest) => Promise<unknown>;
116
124
  interface AiwgChunkedIndexLoadOptions {
117
125
  maxCachedParts?: number;
126
+ detailLoader?: AiwgChunkedIndexDetailLoader;
127
+ maxCachedDetails?: number;
118
128
  }
119
129
  type AiwgChunkedIndexProgressPhase = 'part' | 'query';
120
130
  interface AiwgChunkedIndexProgress {
@@ -175,6 +185,7 @@ interface AiwgIndexController {
175
185
  getSnapshot(): AiwgIndexControllerSnapshot;
176
186
  query(query?: string, options?: AiwgIndexQueryOptions): AiwgIndexQueryResult;
177
187
  queryChunked(query?: string, options?: AiwgChunkedIndexQueryOptions): Promise<AiwgChunkedIndexQueryResult>;
188
+ getRecord(id: string): Promise<AiwgFortemiRecord>;
178
189
  clearChunkCache(): void;
179
190
  toCommunityGraph(options?: AiwgIndexGraphOptions): ReturnType<typeof aiwgFortemiIndexToCommunityGraph>;
180
191
  setReviewDecision(input: AiwgReviewInput): AiwgReviewDecision;
@@ -189,7 +200,26 @@ declare function assertAiwgFortemiChunkManifest(value: unknown): AiwgFortemiChun
189
200
  declare function validateAiwgFortemiChunkPart(value: unknown, partRef?: AiwgFortemiChunkPartRef, manifest?: AiwgFortemiChunkManifest): AiwgChunkedIndexValidationResult;
190
201
  declare function assertAiwgFortemiChunkPart(value: unknown, partRef?: AiwgFortemiChunkPartRef, manifest?: AiwgFortemiChunkManifest): AiwgFortemiChunkPart;
191
202
  declare function createAiwgFetchChunkLoader(baseUrl?: string | URL): AiwgChunkedIndexLoader;
203
+ declare function createAiwgFetchDetailLoader(baseUrl?: string | URL): AiwgChunkedIndexDetailLoader;
192
204
  declare function getAiwgFortemiFacets(items: AiwgFortemiRecord[]): Record<string, Record<string, number>>;
205
+ interface AiwgChunkedIndexBuildOptions {
206
+ partSize?: number;
207
+ projection?: Array<keyof AiwgFortemiRecord>;
208
+ detailHref?: string;
209
+ generatedAt?: string;
210
+ }
211
+ interface AiwgChunkedIndexBuildResult {
212
+ manifest: AiwgFortemiChunkManifest;
213
+ parts: Array<{
214
+ href: string;
215
+ part: AiwgFortemiChunkPart;
216
+ }>;
217
+ details: Array<{
218
+ id: string;
219
+ record: AiwgFortemiRecord;
220
+ }>;
221
+ }
222
+ declare function buildAiwgChunkedIndex(index: AiwgFortemiIndexExport, options?: AiwgChunkedIndexBuildOptions): AiwgChunkedIndexBuildResult;
193
223
  declare function queryAiwgFortemiIndex(index: AiwgFortemiIndexExport, query?: string, options?: AiwgIndexQueryOptions): AiwgIndexQueryResult;
194
224
  declare function createAiwgReviewDecisionExport(source: AiwgFortemiIndexExport, decisions: AiwgReviewDecision[], generatedAt?: string): AiwgReviewDecisionExport;
195
225
  declare function createAiwgIndexController(initialIndex?: AiwgFortemiIndexExport): AiwgIndexController;
@@ -209,4 +239,4 @@ declare function aiwgFortemiIndexToCommunityGraph(index: AiwgFortemiIndexExport,
209
239
  }[];
210
240
  };
211
241
 
212
- export { type AiwgChunkedIndexLoadOptions, type AiwgChunkedIndexLoader, type AiwgChunkedIndexProgress, type AiwgChunkedIndexProgressPhase, type AiwgChunkedIndexQueryOptions, type AiwgChunkedIndexQueryResult, type AiwgChunkedIndexValidationResult, type AiwgFortemiChunkManifest, type AiwgFortemiChunkPart, type AiwgFortemiChunkPartRef, type AiwgFortemiIndexExport, type AiwgFortemiProvenance, type AiwgFortemiRecord, type AiwgFortemiRecordSource, type AiwgFortemiRecordType, type AiwgFortemiRelationship, type AiwgIndexController, type AiwgIndexControllerListener, type AiwgIndexControllerSnapshot, type AiwgIndexGraphOptions, type AiwgIndexQueryMatch, type AiwgIndexQueryOptions, type AiwgIndexQueryRankedItem, type AiwgIndexQueryResult, type AiwgIndexQueryWeights, type AiwgIndexValidationResult, type AiwgPrivacyClassification, type AiwgProvenanceConfidence, type AiwgReviewAction, type AiwgReviewDecision, type AiwgReviewDecisionExport, type AiwgReviewInput, aiwgFortemiIndexToCommunityGraph, assertAiwgFortemiChunkManifest, assertAiwgFortemiChunkPart, assertAiwgFortemiIndexExport, createAiwgFetchChunkLoader, createAiwgIndexController, createAiwgReviewDecisionExport, getAiwgFortemiFacets, queryAiwgFortemiIndex, validateAiwgFortemiChunkManifest, validateAiwgFortemiChunkPart, validateAiwgFortemiIndexExport };
242
+ export { AIWG_SCAN_REQUIRED_FIELDS, type AiwgChunkedIndexBuildOptions, type AiwgChunkedIndexBuildResult, type AiwgChunkedIndexDetailLoader, type AiwgChunkedIndexLoadOptions, type AiwgChunkedIndexLoader, type AiwgChunkedIndexProgress, type AiwgChunkedIndexProgressPhase, type AiwgChunkedIndexQueryOptions, type AiwgChunkedIndexQueryResult, type AiwgChunkedIndexValidationResult, type AiwgFortemiChunkDetailRef, type AiwgFortemiChunkManifest, type AiwgFortemiChunkPart, type AiwgFortemiChunkPartRef, type AiwgFortemiIndexExport, type AiwgFortemiProjectedRecord, type AiwgFortemiProvenance, type AiwgFortemiRecord, type AiwgFortemiRecordSource, type AiwgFortemiRecordType, type AiwgFortemiRelationship, type AiwgIndexController, type AiwgIndexControllerListener, type AiwgIndexControllerSnapshot, type AiwgIndexGraphOptions, type AiwgIndexQueryMatch, type AiwgIndexQueryOptions, type AiwgIndexQueryRankedItem, type AiwgIndexQueryResult, type AiwgIndexQueryWeights, type AiwgIndexValidationResult, type AiwgPrivacyClassification, type AiwgProvenanceConfidence, type AiwgReviewAction, type AiwgReviewDecision, type AiwgReviewDecisionExport, type AiwgReviewInput, aiwgFortemiIndexToCommunityGraph, assertAiwgFortemiChunkManifest, assertAiwgFortemiChunkPart, assertAiwgFortemiIndexExport, buildAiwgChunkedIndex, createAiwgFetchChunkLoader, createAiwgFetchDetailLoader, createAiwgIndexController, createAiwgReviewDecisionExport, getAiwgFortemiFacets, queryAiwgFortemiIndex, validateAiwgFortemiChunkManifest, validateAiwgFortemiChunkPart, validateAiwgFortemiIndexExport };
@@ -1,4 +1,15 @@
1
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
+ ];
2
13
  var REQUIRED_RECORD_FIELDS = [
3
14
  "schema_version",
4
15
  "id",
@@ -110,6 +121,20 @@ function validateAiwgFortemiChunkManifest(value) {
110
121
  if (data.facets !== void 0 && !isFacetCounts(data.facets)) {
111
122
  errors.push("facets must be a nested string-to-number count object");
112
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
+ }
113
138
  if (!Array.isArray(data?.parts)) errors.push("parts must be an array");
114
139
  let expectedOffset = 0;
115
140
  const parts = Array.isArray(data?.parts) ? data.parts : [];
@@ -134,6 +159,35 @@ function assertAiwgFortemiChunkManifest(value) {
134
159
  }
135
160
  return value;
136
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
+ }
137
191
  function validateAiwgFortemiChunkPart(value, partRef, manifest) {
138
192
  const errors = [];
139
193
  const data = value;
@@ -152,13 +206,17 @@ function validateAiwgFortemiChunkPart(value, partRef, manifest) {
152
206
  errors.push("items length must match manifest part count " + partRef.count);
153
207
  }
154
208
  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));
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
+ }
162
220
  }
163
221
  return { valid: errors.length === 0, errors };
164
222
  }
@@ -177,6 +235,16 @@ function createAiwgFetchChunkLoader(baseUrl) {
177
235
  return response.json();
178
236
  };
179
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
+ }
180
248
  function getAiwgFortemiFacets(items) {
181
249
  const result = {};
182
250
  for (const item of items) {
@@ -190,6 +258,49 @@ function getAiwgFortemiFacets(items) {
190
258
  }
191
259
  return result;
192
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
+ }
193
304
  function includesAll(actual, expected) {
194
305
  if (!expected || expected.length === 0) return true;
195
306
  const actualSet = new Set(actual);
@@ -244,7 +355,7 @@ function createRankedEntries(items, q, options, ordinalBase = 0) {
244
355
  if (!includesAll(item.tags, options.tags)) return false;
245
356
  if (!includesAll(item.concepts, options.concepts)) return false;
246
357
  if (!matchesFacetFilters(item, options.facets)) return false;
247
- if (options.relationshipTargetId && !item.relationships.some((rel) => rel.target_id === options.relationshipTargetId)) {
358
+ if (options.relationshipTargetId && !(item.relationships ?? []).some((rel) => rel.target_id === options.relationshipTargetId)) {
248
359
  return false;
249
360
  }
250
361
  return true;
@@ -293,6 +404,10 @@ function clampMaxCachedParts(value) {
293
404
  if (!hasPositiveInteger(value)) return 3;
294
405
  return value;
295
406
  }
407
+ function clampMaxCachedDetails(value) {
408
+ if (!hasPositiveInteger(value)) return 32;
409
+ return value;
410
+ }
296
411
  function isDirectChunkBrowse(query, options) {
297
412
  return query.trim() === "" && !options.rank && !options.snippets && !options.includeMatches && !options.types && !options.facets && !options.tags && !options.concepts && !options.privacy && !options.relationshipTargetId;
298
413
  }
@@ -317,6 +432,40 @@ async function loadChunkPart(runtime, part) {
317
432
  }
318
433
  return { part: parsed, fetched: true };
319
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
+ }
320
469
  async function queryChunkedAiwgFortemiIndex(runtime, query = "", options = {}) {
321
470
  const q = query.trim().toLowerCase();
322
471
  let scannedParts = 0;
@@ -421,7 +570,10 @@ function createAiwgIndexController(initialIndex) {
421
570
  manifest: parsed,
422
571
  loader,
423
572
  maxCachedParts: clampMaxCachedParts(options.maxCachedParts),
424
- partCache: /* @__PURE__ */ new Map()
573
+ partCache: /* @__PURE__ */ new Map(),
574
+ detailLoader: options.detailLoader,
575
+ maxCachedDetails: clampMaxCachedDetails(options.maxCachedDetails),
576
+ detailCache: /* @__PURE__ */ new Map()
425
577
  };
426
578
  data = null;
427
579
  reviewDecisions = [];
@@ -464,8 +616,23 @@ function createAiwgIndexController(initialIndex) {
464
616
  throw error;
465
617
  }
466
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
+ },
467
633
  clearChunkCache() {
468
634
  chunked?.partCache.clear();
635
+ chunked?.detailCache.clear();
469
636
  error = null;
470
637
  notify();
471
638
  },
@@ -545,6 +712,6 @@ function communityIdsFor(item, options) {
545
712
  return [`type:${item.type}`];
546
713
  }
547
714
 
548
- export { aiwgFortemiIndexToCommunityGraph, assertAiwgFortemiChunkManifest, assertAiwgFortemiChunkPart, assertAiwgFortemiIndexExport, createAiwgFetchChunkLoader, createAiwgIndexController, createAiwgReviewDecisionExport, getAiwgFortemiFacets, queryAiwgFortemiIndex, validateAiwgFortemiChunkManifest, validateAiwgFortemiChunkPart, validateAiwgFortemiIndexExport };
715
+ export { AIWG_SCAN_REQUIRED_FIELDS, aiwgFortemiIndexToCommunityGraph, assertAiwgFortemiChunkManifest, assertAiwgFortemiChunkPart, assertAiwgFortemiIndexExport, buildAiwgChunkedIndex, createAiwgFetchChunkLoader, createAiwgFetchDetailLoader, createAiwgIndexController, createAiwgReviewDecisionExport, getAiwgFortemiFacets, queryAiwgFortemiIndex, validateAiwgFortemiChunkManifest, validateAiwgFortemiChunkPart, validateAiwgFortemiIndexExport };
549
716
  //# sourceMappingURL=aiwg-index.js.map
550
717
  //# sourceMappingURL=aiwg-index.js.map