@pagenary/publisher 2026.6.3 → 2026.6.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pagenary/publisher",
3
- "version": "2026.6.3",
3
+ "version": "2026.6.4",
4
4
  "type": "module",
5
5
  "description": "Multi-tenant static publishing component for Pagenary platform.",
6
6
  "license": "AGPL-3.0-or-later",
@@ -10,7 +10,7 @@ import { generateSeoArtifacts, resolveBaseUrl, resolveOgImage } from './lib/seo-
10
10
  import { generateCollections } from './lib/collections-generator.js';
11
11
  import { parseFrontmatter } from './lib/frontmatter.js';
12
12
  import { generateSearchIndex } from './lib/search-index-generator.js';
13
- import { fileURLToPath } from 'node:url';
13
+ import { fileURLToPath, pathToFileURL } from 'node:url';
14
14
 
15
15
  const root = process.cwd();
16
16
  // The package's own directory (this file lives at <pkg>/scripts/build-tenants.js).
@@ -976,6 +976,42 @@ async function injectTenantBase(distDir, tenantId) {
976
976
  console.log(` ↳ wired tenant base for ${tenantId}`);
977
977
  }
978
978
 
979
+ /**
980
+ * Set the shell <title> to the default page's metadata title for SEO (#28).
981
+ * Reads the generated manifest.js for DEFAULT_SECTION and its (metadata-derived)
982
+ * title, producing "<page title> · <brand>" to mirror the runtime seo.js format.
983
+ * Generic brand is used only as a fallback when no default title is available,
984
+ * so the crawler-visible root URL gets a specific, descriptive title.
985
+ * @param {string} distDir - Tenant output directory
986
+ * @param {object} config - Tenant config (for the brand title)
987
+ */
988
+ async function applyDefaultPageTitle(distDir, config) {
989
+ const indexPath = path.join(distDir, 'index.html');
990
+ const manifestPath = path.join(distDir, 'manifest.js');
991
+ if (!(await pathExists(indexPath)) || !(await pathExists(manifestPath))) return;
992
+
993
+ let defaultTitle = null;
994
+ try {
995
+ // Cache-bust so incremental rebuilds re-read the freshly written manifest.
996
+ const mod = await import(`${pathToFileURL(manifestPath).href}?t=${Date.now()}`);
997
+ const id = mod.DEFAULT_SECTION;
998
+ const entry = id && typeof mod.findSection === 'function' ? mod.findSection(id) : null;
999
+ if (entry && entry.title) defaultTitle = entry.title;
1000
+ } catch {
1001
+ // Manifest not importable — keep the existing (branded/generic) title.
1002
+ }
1003
+ if (!defaultTitle) return;
1004
+
1005
+ const brand = config.title || null;
1006
+ const shellTitle = brand ? `${defaultTitle} · ${brand}` : defaultTitle;
1007
+ let html = await fsp.readFile(indexPath, 'utf8');
1008
+ const replaced = html.replace(/<title>[^<]*<\/title>/, `<title>${escapeHtml(shellTitle)}</title>`);
1009
+ if (replaced !== html) {
1010
+ await fsp.writeFile(indexPath, replaced, 'utf8');
1011
+ console.log(` ↳ default page title: ${shellTitle}`);
1012
+ }
1013
+ }
1014
+
979
1015
  function hexToRgb(hex) {
980
1016
  const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
981
1017
  if (!result) return null;
@@ -3335,6 +3371,10 @@ async function buildTenant(tenant, targetOverride, cacheDir, buildOptions) {
3335
3371
  // against the tenant root. Domain-root deploys fall back to "/".
3336
3372
  await injectTenantBase(distDir, tenantId);
3337
3373
 
3374
+ // Set the shell <title> from the default page's metadata title (SEO, #28),
3375
+ // falling back to the generic brand only when no default title exists.
3376
+ await applyDefaultPageTitle(distDir, config);
3377
+
3338
3378
  // Copy static assets from .public/ directory
3339
3379
  await copyPublicAssets(sourceDir, distDir, tenantId);
3340
3380
 
package/site/index.html CHANGED
@@ -3,7 +3,7 @@
3
3
  <head>
4
4
  <meta charset="utf-8" />
5
5
  <meta name="viewport" content="width=device-width,initial-scale=1" />
6
- <title>Pagenary Docs</title>
6
+ <title>Welcome · Pagenary Docs</title>
7
7
  <meta name="description" content="Pagenary developer documentation — building, configuring, deploying, and extending the multi-tenant documentation publisher, published with Pagenary itself." />
8
8
  <script>
9
9
  // Resolve all asset/module URLs against the tenant root in both deploy
@@ -21,7 +21,7 @@
21
21
  </script>
22
22
  <link rel="icon" type="image/png" href="./favicon.png" />
23
23
  <link rel="stylesheet" href="./styles.css" />
24
- <meta name="x-build" content="2026-06-15T19:17:20.632Z" />
24
+ <meta name="x-build" content="2026-06-15T21:21:38.022Z" />
25
25
  </head>
26
26
  <body>
27
27
  <a class="skip-link" href="#app">Skip to content</a>
package/site/robots.txt CHANGED
@@ -1,5 +1,5 @@
1
1
  # Pagenary Docs
2
- # Generated: 2026-06-15T19:17:21.186Z
2
+ # Generated: 2026-06-15T21:21:38.574Z
3
3
 
4
4
  User-agent: *
5
5
  Allow: /
@@ -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 +1 @@
1
- var e=["schema_version","id","type","source","title","text","facets","tags","concepts","relationships","provenance","privacy","updated_at"],t=new Set(["crm.contact","crm.organization","crm.event","crm.interaction","aiwg.artifact","docs.page"]),r={title:4,tag:3,concept:2,text:1};function n(e){return"string"==typeof e&&e.length>0}function s(e,t,r){e[t]??={},e[t][r]=(e[t][r]??0)+1}function i(e){return Number.isInteger(e)&&"number"==typeof e&&e>=0}function a(e){return Number.isInteger(e)&&"number"==typeof e&&e>0}function o(r){const s=[],i={},a=r;"aiwg.fortemi.index.export.v1"!==a?.schema_version&&s.push("schema_version must be aiwg.fortemi.index.export.v1"),n(a?.generated_at)||s.push("generated_at is required"),n(a?.source?.repo)||s.push("source.repo is required"),n(a?.source?.privacy)||s.push("source.privacy is required"),Array.isArray(a?.items)||s.push("items must be an array");const o=new Set;let c="";for(const[r,u]of(a.items??[]).entries()){for(const t of e)t in u||s.push("items["+r+"]."+t+" is required");"aiwg.fortemi.index.record.v1"!==u.schema_version&&s.push("items["+r+"].schema_version must be aiwg.fortemi.index.record.v1"),n(u.id)||s.push("items["+r+"].id is required"),n(u.id)&&o.has(u.id)&&s.push("duplicate id: "+u.id),n(u.id)&&o.add(u.id),c&&n(u.id)&&c.localeCompare(u.id)>0&&s.push("items must be sorted by id: "+c+" before "+u.id),n(u.id)&&(c=u.id),t.has(u.type)?i[u.type]=(i[u.type]??0)+1:s.push("items["+r+"].type is invalid"),n(u.source?.path)||s.push("items["+r+"].source.path is required"),n(u.source?.repo_relative_path)||s.push("items["+r+"].source.repo_relative_path is required"),n(u.source?.locator)||s.push("items["+r+"].source.locator is required"),Array.isArray(u.tags)||s.push("items["+r+"].tags must be an array"),Array.isArray(u.concepts)||s.push("items["+r+"].concepts must be an array"),Array.isArray(u.relationships)||s.push("items["+r+"].relationships must be an array"),Array.isArray(u.provenance)&&0!==u.provenance.length||s.push("items["+r+"].provenance must be a non-empty array"),u.privacy&&"boolean"==typeof u.privacy.pii&&n(u.privacy.classification)||s.push("items["+r+"].privacy requires classification and pii")}return{valid:0===s.length,errors:s,counts:i}}function c(e){const t=o(e);if(!t.valid)throw new Error("Invalid AIWG Fortemi index export:\n"+t.errors.join("\n"));return e}function u(e){const t=[],r=e;"aiwg.fortemi.index.chunk-manifest.v1"!==r?.schema_version&&t.push("schema_version must be aiwg.fortemi.index.chunk-manifest.v1"),n(r?.generated_at)||t.push("generated_at is required"),n(r?.source?.repo)||t.push("source.repo is required"),n(r?.source?.privacy)||t.push("source.privacy is required"),i(r?.total)||t.push("total must be a non-negative integer"),a(r?.part_size)||t.push("part_size must be a positive integer"),void 0===r.facets||function(e){return!(!e||"object"!=typeof e||Array.isArray(e))&&Object.values(e).every(e=>!!e&&"object"==typeof e&&!Array.isArray(e)&&Object.values(e).every(e=>i(e)))}(r.facets)||t.push("facets must be a nested string-to-number count object"),Array.isArray(r?.parts)||t.push("parts must be an array");let s=0;const o=Array.isArray(r?.parts)?r.parts:[];for(const[e,r]of o.entries())n(r.href)||t.push("parts["+e+"].href is required"),i(r.offset)||t.push("parts["+e+"].offset must be a non-negative integer"),i(r.count)||t.push("parts["+e+"].count must be a non-negative integer"),i(r.offset)&&r.offset!==s&&t.push("parts["+e+"].offset must be "+s),i(r.count)&&(s+=r.count);return i(r?.total)&&s!==r.total&&t.push("parts counts must add up to total"),{valid:0===t.length,errors:t}}function f(e){const t=u(e);if(!t.valid)throw new Error("Invalid AIWG Fortemi chunk manifest:\n"+t.errors.join("\n"));return e}function p(e,t,r){const n=[],s=e;if("aiwg.fortemi.index.chunk.v1"!==s?.schema_version&&n.push("schema_version must be aiwg.fortemi.index.chunk.v1"),"aiwg.fortemi.index.chunk-manifest.v1"!==s?.manifest_schema_version&&n.push("manifest_schema_version must be aiwg.fortemi.index.chunk-manifest.v1"),i(s?.offset)||n.push("offset must be a non-negative integer"),Array.isArray(s?.items)||n.push("items must be an array"),t&&i(s?.offset)&&s.offset!==t.offset&&n.push("offset must match manifest part offset "+t.offset),t&&Array.isArray(s?.items)&&s.items.length!==t.count&&n.push("items length must match manifest part count "+t.count),Array.isArray(s?.items)){const e=o({schema_version:"aiwg.fortemi.index.export.v1",generated_at:r?.generated_at??"1970-01-01T00:00:00.000Z",source:r?.source??{repo:"chunk",privacy:"public"},items:s.items});n.push(...e.errors.map(e=>"items."+e))}return{valid:0===n.length,errors:n}}function m(e,t,r){const n=p(e,t,r);if(!n.valid)throw new Error("Invalid AIWG Fortemi chunk part:\n"+n.errors.join("\n"));return e}function h(e){return async t=>{const r=e?new URL(t.href,e).toString():t.href,n=await fetch(r);if(!n.ok)throw new Error("Failed to fetch AIWG index chunk "+r+": "+n.status);return n.json()}}function l(e){const t={};for(const r of e){s(t,"type",r.type),s(t,"privacy",r.privacy.classification);for(const e of r.tags)s(t,"tag",e);for(const e of r.concepts)s(t,"concept",e);for(const[e,n]of Object.entries(r.facets))for(const r of n)s(t,e,r)}return t}function d(e,t){if(!t||0===t.length)return!0;const r=new Set(e);return t.every(e=>r.has(e))}function g(e,t){if(!t)return[];const r=[];e.title.toLowerCase().includes(t)&&r.push({field:"title",value:e.title}),e.text.toLowerCase().includes(t)&&r.push({field:"text",value:e.text});for(const n of e.tags)n.toLowerCase().includes(t)&&r.push({field:"tag",value:n});for(const n of e.concepts)n.toLowerCase().includes(t)&&r.push({field:"concept",value:n});return r}function y(e,t){return e.reduce((e,r)=>e+t[r.field],0)}function v(e,t,r,n){const s=t.find(e=>"text"===e.field),i=t.find(e=>"title"===e.field),a=s??i??t[0];return function(e,t,r){const n=Math.max(20,r);if(!e)return"";if(!t)return e.length>n?`${e.slice(0,n).trimEnd()}...`:e;const s=e.toLowerCase().indexOf(t);if(s<0)return e.length>n?`${e.slice(0,n).trimEnd()}...`:e;const i=Math.max(0,Math.floor((n-t.length)/2)),a=Math.max(0,s-i),o=Math.min(e.length,a+n),c=a>0?"...":"",u=o<e.length?"...":"";return`${c}${e.slice(a,o).trim()}${u}`}(a?.value??e.text,r,n)}function w(e,t,n,s=0){const i={...r,...n.weights};return e.map((e,r)=>({item:e,ordinal:s+r,matches:g(e,t)})).filter(({item:e,matches:r})=>!(t&&0===r.length||n.types&&!n.types.includes(e.type)||n.privacy&&!n.privacy.includes(e.privacy.classification)||!d(e.tags,n.tags)||!d(e.concepts,n.concepts)||!function(e,t){return!t||Object.entries(t).every(([t,r])=>d(e.facets[t]??[],r))}(e,n.facets)||n.relationshipTargetId&&!e.relationships.some(e=>e.target_id===n.relationshipTargetId))).map(({item:e,ordinal:t,matches:r})=>({item:e,ordinal:t,rank:y(r,i),matches:r}))}function x(e,t,r){const n=function(e,t){return[...e].sort((e,r)=>t&&r.rank-e.rank||e.ordinal-r.ordinal)}(e,r.rank),s=r.offset??0,i=r.limit??n.length,a=n.slice(s,s+i),o={items:a.map(e=>e.item),total:n.length,facets:l(n.map(e=>e.item))};if(r.rank||r.snippets||r.includeMatches){const e=r.snippetLength??160;o.rankedItems=a.map(n=>({item:n.item,rank:n.rank,...r.snippets?{snippet:v(n.item,n.matches,t,e)}:{},...r.includeMatches?{matches:n.matches}:{}}))}return o}function _(e,t="",r={}){const n=t.trim().toLowerCase();return x(w(e.items,n,r),n,r)}async function b(e,t){const r=function(e){return`${e.offset}:${e.href}`}(t),n=e.partCache.get(r);if(n)return e.partCache.delete(r),e.partCache.set(r,n),{part:n,fetched:!1};const s=m(await e.loader(t,e.manifest),t,e.manifest);for(e.partCache.set(r,s);e.partCache.size>e.maxCachedParts;){const t=e.partCache.keys().next().value;if(void 0===t)break;e.partCache.delete(t)}return{part:s,fetched:!0}}function C(e,t,r=(new Date).toISOString()){return{schema_version:"aiwg.fortemi.review-decisions.v1",generated_at:r,source_export_schema_version:e.schema_version,decisions:[...t].sort((e,t)=>e.item_id.localeCompare(t.item_id))}}function A(e){let t=e??null,r=null,n=null,s=null,i=[];const o=new Set,u=()=>({index:t,chunked:r?{manifest:r.manifest,cachedParts:r.partCache.size,maxCachedParts:r.maxCachedParts}:null,data:n,error:s,reviewDecisions:[...i]}),p=()=>{const e=u();for(const t of o)t(e)},m=()=>{if(!t)throw new Error("No AIWG index export loaded");return t};return{loadIndex(e){try{const a=c(e);return t=a,r=null,n=null,i=[],s=null,p(),a}catch(e){throw s=e instanceof Error?e:new Error(String(e)),p(),s}},loadChunkedIndex(e,o,c={}){try{const m=f(e);return t=null,r={manifest:m,loader:o,maxCachedParts:(u=c.maxCachedParts,a(u)?u:3),partCache:new Map},n=null,i=[],s=null,p(),m}catch(e){throw s=e instanceof Error?e:new Error(String(e)),p(),s}var u},getIndex:()=>t,getChunkedManifest:()=>r?.manifest??null,getSnapshot:()=>u(),query(e="",t){const r=_(m(),e,t);return n=r,s=null,p(),r},async queryChunked(e="",t){if(!r)throw new Error("No AIWG chunked index manifest loaded");try{const i=await async function(e,t="",r={}){const n=t.trim().toLowerCase();let s=0,i=0;if(function(e,t){return!(""!==e.trim()||t.rank||t.snippets||t.includeMatches||t.types||t.facets||t.tags||t.concepts||t.privacy||t.relationshipTargetId)}(t,r)){const t=r.offset??0,n=r.limit??e.manifest.total,a=function(e,t,r){const n=t+r;return e.parts.filter(e=>e.count>0&&e.offset<n&&e.offset+e.count>t)}(e.manifest,t,n),o=[];for(const c of a){const u=await b(e,c);u.fetched&&(i+=1),s+=1,r.onProgress?.({phase:"part",done:s,total:a.length,href:c.href});const f=Math.max(0,t-c.offset),p=Math.min(u.part.items.length,t+n-c.offset);o.push(...u.part.items.slice(f,p))}return{items:o,total:e.manifest.total,facets:e.manifest.facets??{},manifestTotal:e.manifest.total,scannedParts:s,fetchedParts:i,complete:!0}}const a=[];for(const t of e.manifest.parts){const o=await b(e,t);o.fetched&&(i+=1),s+=1,r.onProgress?.({phase:"part",done:s,total:e.manifest.parts.length,href:t.href}),a.push(...w(o.part.items,n,r,t.offset)),r.onProgress?.({phase:"query",done:s,total:e.manifest.parts.length,href:t.href})}return{...x(a,n,r),manifestTotal:e.manifest.total,scannedParts:s,fetchedParts:i,complete:!0}}(r,e,t);return n=i,s=null,p(),i}catch(e){throw s=e instanceof Error?e:new Error(String(e)),p(),s}},clearChunkCache(){r?.partCache.clear(),s=null,p()},toCommunityGraph:e=>k(m(),e),setReviewDecision(e){const t={...e,updated_at:(new Date).toISOString()};return i=[...i.filter(e=>e.item_id!==t.item_id),t].sort((e,t)=>e.item_id.localeCompare(t.item_id)),s=null,p(),t},clearReviewDecision(e){i=i.filter(t=>t.item_id!==e),s=null,p()},createReviewDecisionExport:e=>C(m(),i,e),subscribe:e=>(o.add(e),()=>{o.delete(e)})}}function k(e,t={}){const r=new Set(e.items.map(e=>e.id)),n=t.relationshipWeights??{},s=new Map;for(const i of e.items)for(const e of i.relationships){if(!r.has(e.target_id)&&!t.includeDanglingRelationships)continue;const a=e.type,o=n[a]??1,c=`${i.id}\0${e.target_id}\0${a}`,u=s.get(c);u?u.weight+=o:s.set(c,{source:i.id,target:e.target_id,kind:a,weight:o})}const i=new Map;for(const r of e.items){const e=I(r,t);for(const t of e){const e=i.get(t)??[];e.push(r.id),i.set(t,e)}}return{nodes:e.items.map(e=>({id:e.id})),edges:Array.from(s.values()).sort((e,t)=>e.source.localeCompare(t.source)||e.target.localeCompare(t.target)||e.kind.localeCompare(t.kind)),communities:Array.from(i.entries()).map(([e,t])=>({id:e,nodes:[...new Set(t)].sort()})).sort((e,t)=>e.id.localeCompare(t.id))}}function I(e,t){if(t.communityFacet){const r=e.facets[t.communityFacet]??[];if(r.length>0)return r.map(e=>`${t.communityFacet}:${e}`)}if(t.communityTagPrefix){const r=t.communityTagPrefix,n=e.tags.filter(e=>e.startsWith(r));if(n.length>0)return n}return e.concepts.length>0?e.concepts.map(e=>`concept:${e}`):[`type:${e.type}`]}export{k as aiwgFortemiIndexToCommunityGraph,f as assertAiwgFortemiChunkManifest,m as assertAiwgFortemiChunkPart,c as assertAiwgFortemiIndexExport,h as createAiwgFetchChunkLoader,A as createAiwgIndexController,C as createAiwgReviewDecisionExport,l as getAiwgFortemiFacets,_ as queryAiwgFortemiIndex,u as validateAiwgFortemiChunkManifest,p as validateAiwgFortemiChunkPart,o as validateAiwgFortemiIndexExport};
1
+ var e=["schema_version","id","type","title","text","facets","tags","concepts","privacy"],t=["schema_version","id","type","source","title","text","facets","tags","concepts","relationships","provenance","privacy","updated_at"],r=new Set(["crm.contact","crm.organization","crm.event","crm.interaction","aiwg.artifact","docs.page"]),i={title:4,tag:3,concept:2,text:1};function s(e){return"string"==typeof e&&e.length>0}function n(e,t,r){e[t]??={},e[t][r]=(e[t][r]??0)+1}function a(e){return Number.isInteger(e)&&"number"==typeof e&&e>=0}function o(e){return Number.isInteger(e)&&"number"==typeof e&&e>0}function c(e){const i=[],n={},a=e;"aiwg.fortemi.index.export.v1"!==a?.schema_version&&i.push("schema_version must be aiwg.fortemi.index.export.v1"),s(a?.generated_at)||i.push("generated_at is required"),s(a?.source?.repo)||i.push("source.repo is required"),s(a?.source?.privacy)||i.push("source.privacy is required"),Array.isArray(a?.items)||i.push("items must be an array");const o=new Set;let c="";for(const[e,u]of(a.items??[]).entries()){for(const r of t)r in u||i.push("items["+e+"]."+r+" is required");"aiwg.fortemi.index.record.v1"!==u.schema_version&&i.push("items["+e+"].schema_version must be aiwg.fortemi.index.record.v1"),s(u.id)||i.push("items["+e+"].id is required"),s(u.id)&&o.has(u.id)&&i.push("duplicate id: "+u.id),s(u.id)&&o.add(u.id),c&&s(u.id)&&c.localeCompare(u.id)>0&&i.push("items must be sorted by id: "+c+" before "+u.id),s(u.id)&&(c=u.id),r.has(u.type)?n[u.type]=(n[u.type]??0)+1:i.push("items["+e+"].type is invalid"),s(u.source?.path)||i.push("items["+e+"].source.path is required"),s(u.source?.repo_relative_path)||i.push("items["+e+"].source.repo_relative_path is required"),s(u.source?.locator)||i.push("items["+e+"].source.locator is required"),Array.isArray(u.tags)||i.push("items["+e+"].tags must be an array"),Array.isArray(u.concepts)||i.push("items["+e+"].concepts must be an array"),Array.isArray(u.relationships)||i.push("items["+e+"].relationships must be an array"),Array.isArray(u.provenance)&&0!==u.provenance.length||i.push("items["+e+"].provenance must be a non-empty array"),u.privacy&&"boolean"==typeof u.privacy.pii&&s(u.privacy.classification)||i.push("items["+e+"].privacy requires classification and pii")}return{valid:0===i.length,errors:i,counts:n}}function u(e){const t=c(e);if(!t.valid)throw new Error("Invalid AIWG Fortemi index export:\n"+t.errors.join("\n"));return e}function f(t){const r=[],i=t;if("aiwg.fortemi.index.chunk-manifest.v1"!==i?.schema_version&&r.push("schema_version must be aiwg.fortemi.index.chunk-manifest.v1"),s(i?.generated_at)||r.push("generated_at is required"),s(i?.source?.repo)||r.push("source.repo is required"),s(i?.source?.privacy)||r.push("source.privacy is required"),a(i?.total)||r.push("total must be a non-negative integer"),o(i?.part_size)||r.push("part_size must be a positive integer"),void 0===i.facets||function(e){return!(!e||"object"!=typeof e||Array.isArray(e))&&Object.values(e).every(e=>!!e&&"object"==typeof e&&!Array.isArray(e)&&Object.values(e).every(e=>a(e)))}(i.facets)||r.push("facets must be a nested string-to-number count object"),void 0!==i.projection)if(Array.isArray(i.projection)&&i.projection.every(e=>"string"==typeof e)){const t=new Set(i.projection);for(const i of e)t.has(i)||r.push("projection must include scan-required field "+i)}else r.push("projection must be an array of field names");void 0!==i.detail&&(s(i.detail.href)?i.detail.href.includes("{id}")||r.push("detail.href must contain the {id} placeholder"):r.push("detail.href is required")),Array.isArray(i?.parts)||r.push("parts must be an array");let n=0;const c=Array.isArray(i?.parts)?i.parts:[];for(const[e,t]of c.entries())s(t.href)||r.push("parts["+e+"].href is required"),a(t.offset)||r.push("parts["+e+"].offset must be a non-negative integer"),a(t.count)||r.push("parts["+e+"].count must be a non-negative integer"),a(t.offset)&&t.offset!==n&&r.push("parts["+e+"].offset must be "+n),a(t.count)&&(n+=t.count);return a(i?.total)&&n!==i.total&&r.push("parts counts must add up to total"),{valid:0===r.length,errors:r}}function d(e){const t=f(e);if(!t.valid)throw new Error("Invalid AIWG Fortemi chunk manifest:\n"+t.errors.join("\n"));return e}function p(e,t,i){const n=[],o=e;if("aiwg.fortemi.index.chunk.v1"!==o?.schema_version&&n.push("schema_version must be aiwg.fortemi.index.chunk.v1"),"aiwg.fortemi.index.chunk-manifest.v1"!==o?.manifest_schema_version&&n.push("manifest_schema_version must be aiwg.fortemi.index.chunk-manifest.v1"),a(o?.offset)||n.push("offset must be a non-negative integer"),Array.isArray(o?.items)||n.push("items must be an array"),t&&a(o?.offset)&&o.offset!==t.offset&&n.push("offset must match manifest part offset "+t.offset),t&&Array.isArray(o?.items)&&o.items.length!==t.count&&n.push("items length must match manifest part count "+t.count),Array.isArray(o?.items))if(i?.projection)n.push(...function(e){const t=[],i=new Set;let n="";for(const[a,o]of e.entries())"aiwg.fortemi.index.record.v1"!==o.schema_version&&t.push("items["+a+"].schema_version must be aiwg.fortemi.index.record.v1"),s(o.id)||t.push("items["+a+"].id is required"),s(o.id)&&i.has(o.id)&&t.push("duplicate id: "+o.id),s(o.id)&&i.add(o.id),n&&s(o.id)&&n.localeCompare(o.id)>0&&t.push("items must be sorted by id: "+n+" before "+o.id),s(o.id)&&(n=o.id),o.type&&r.has(o.type)||t.push("items["+a+"].type is invalid"),s(o.title)||t.push("items["+a+"].title is required"),"string"!=typeof o.text&&t.push("items["+a+"].text is required"),o.facets&&"object"==typeof o.facets&&!Array.isArray(o.facets)||t.push("items["+a+"].facets must be an object"),Array.isArray(o.tags)||t.push("items["+a+"].tags must be an array"),Array.isArray(o.concepts)||t.push("items["+a+"].concepts must be an array"),o.privacy&&s(o.privacy.classification)||t.push("items["+a+"].privacy.classification is required");return t}(o.items).map(e=>"items."+e));else{const e=c({schema_version:"aiwg.fortemi.index.export.v1",generated_at:i?.generated_at??"1970-01-01T00:00:00.000Z",source:i?.source??{repo:"chunk",privacy:"public"},items:o.items});n.push(...e.errors.map(e=>"items."+e))}return{valid:0===n.length,errors:n}}function h(e,t,r){const i=p(e,t,r);if(!i.valid)throw new Error("Invalid AIWG Fortemi chunk part:\n"+i.errors.join("\n"));return e}function m(e){return async t=>{const r=e?new URL(t.href,e).toString():t.href,i=await fetch(r);if(!i.ok)throw new Error("Failed to fetch AIWG index chunk "+r+": "+i.status);return i.json()}}function l(e){return async(t,r)=>{if(!r.detail?.href)throw new Error("Manifest has no detail.href for record resolution");const i=r.detail.href.replace("{id}",encodeURIComponent(t)),s=e?new URL(i,e).toString():i,n=await fetch(s);if(!n.ok)throw new Error("Failed to fetch AIWG index detail "+s+": "+n.status);return n.json()}}function g(e){const t={};for(const r of e){n(t,"type",r.type),n(t,"privacy",r.privacy.classification);for(const e of r.tags)n(t,"tag",e);for(const e of r.concepts)n(t,"concept",e);for(const[e,i]of Object.entries(r.facets))for(const r of i)n(t,e,r)}return t}function y(e,t={}){const r=o(t.partSize)?t.partSize:500,i=t.projection,s=e.items,n=e=>String(e).padStart(4,"0"),a=e=>{if(!i)return e;const t={};for(const r of i)t[r]=e[r];return t},c=[],u=[];for(let e=0,t=0;e<s.length;e+=r,t+=1){const i=s.slice(e,e+r),o="part-"+n(t)+".json";c.push({href:o,part:{schema_version:"aiwg.fortemi.index.chunk.v1",manifest_schema_version:"aiwg.fortemi.index.chunk-manifest.v1",offset:e,items:i.map(a)}}),u.push({href:o,offset:e,count:i.length})}return{manifest:{schema_version:"aiwg.fortemi.index.chunk-manifest.v1",generated_at:t.generatedAt??e.generated_at,source:e.source,total:s.length,part_size:r,facets:g(s),parts:u,...i?{projection:i,detail:{href:t.detailHref??"detail/{id}.json"}}:{}},parts:c,details:i?s.map(e=>({id:e.id,record:e})):[]}}function v(e,t){if(!t||0===t.length)return!0;const r=new Set(e);return t.every(e=>r.has(e))}function w(e,t){if(!t)return[];const r=[];e.title.toLowerCase().includes(t)&&r.push({field:"title",value:e.title}),e.text.toLowerCase().includes(t)&&r.push({field:"text",value:e.text});for(const i of e.tags)i.toLowerCase().includes(t)&&r.push({field:"tag",value:i});for(const i of e.concepts)i.toLowerCase().includes(t)&&r.push({field:"concept",value:i});return r}function x(e,t){return e.reduce((e,r)=>e+t[r.field],0)}function _(e,t,r,i){const s=t.find(e=>"text"===e.field),n=t.find(e=>"title"===e.field),a=s??n??t[0];return function(e,t,r){const i=Math.max(20,r);if(!e)return"";if(!t)return e.length>i?`${e.slice(0,i).trimEnd()}...`:e;const s=e.toLowerCase().indexOf(t);if(s<0)return e.length>i?`${e.slice(0,i).trimEnd()}...`:e;const n=Math.max(0,Math.floor((i-t.length)/2)),a=Math.max(0,s-n),o=Math.min(e.length,a+i),c=a>0?"...":"",u=o<e.length?"...":"";return`${c}${e.slice(a,o).trim()}${u}`}(a?.value??e.text,r,i)}function b(e,t,r,s=0){const n={...i,...r.weights};return e.map((e,r)=>({item:e,ordinal:s+r,matches:w(e,t)})).filter(({item:e,matches:i})=>!(t&&0===i.length||r.types&&!r.types.includes(e.type)||r.privacy&&!r.privacy.includes(e.privacy.classification)||!v(e.tags,r.tags)||!v(e.concepts,r.concepts)||!function(e,t){return!t||Object.entries(t).every(([t,r])=>v(e.facets[t]??[],r))}(e,r.facets)||r.relationshipTargetId&&!(e.relationships??[]).some(e=>e.target_id===r.relationshipTargetId))).map(({item:e,ordinal:t,matches:r})=>({item:e,ordinal:t,rank:x(r,n),matches:r}))}function C(e,t,r){const i=function(e,t){return[...e].sort((e,r)=>t&&r.rank-e.rank||e.ordinal-r.ordinal)}(e,r.rank),s=r.offset??0,n=r.limit??i.length,a=i.slice(s,s+n),o={items:a.map(e=>e.item),total:i.length,facets:g(i.map(e=>e.item))};if(r.rank||r.snippets||r.includeMatches){const e=r.snippetLength??160;o.rankedItems=a.map(i=>({item:i.item,rank:i.rank,...r.snippets?{snippet:_(i.item,i.matches,t,e)}:{},...r.includeMatches?{matches:i.matches}:{}}))}return o}function A(e,t="",r={}){const i=t.trim().toLowerCase();return C(b(e.items,i,r),i,r)}function k(e){return o(e)?e:32}async function j(e,t){const r=function(e){return`${e.offset}:${e.href}`}(t),i=e.partCache.get(r);if(i)return e.partCache.delete(r),e.partCache.set(r,i),{part:i,fetched:!1};const s=h(await e.loader(t,e.manifest),t,e.manifest);for(e.partCache.set(r,s);e.partCache.size>e.maxCachedParts;){const t=e.partCache.keys().next().value;if(void 0===t)break;e.partCache.delete(t)}return{part:s,fetched:!0}}function S(e,t,r=(new Date).toISOString()){return{schema_version:"aiwg.fortemi.review-decisions.v1",generated_at:r,source_export_schema_version:e.schema_version,decisions:[...t].sort((e,t)=>e.item_id.localeCompare(t.item_id))}}function q(e){let t=e??null,r=null,i=null,s=null,n=[];const a=new Set,c=()=>({index:t,chunked:r?{manifest:r.manifest,cachedParts:r.partCache.size,maxCachedParts:r.maxCachedParts}:null,data:i,error:s,reviewDecisions:[...n]}),f=()=>{const e=c();for(const t of a)t(e)},p=()=>{if(!t)throw new Error("No AIWG index export loaded");return t};return{loadIndex(e){try{const a=u(e);return t=a,r=null,i=null,n=[],s=null,f(),a}catch(e){throw s=e instanceof Error?e:new Error(String(e)),f(),s}},loadChunkedIndex(e,a,c={}){try{const p=d(e);return t=null,r={manifest:p,loader:a,maxCachedParts:(u=c.maxCachedParts,o(u)?u:3),partCache:new Map,detailLoader:c.detailLoader,maxCachedDetails:k(c.maxCachedDetails),detailCache:new Map},i=null,n=[],s=null,f(),p}catch(e){throw s=e instanceof Error?e:new Error(String(e)),f(),s}var u},getIndex:()=>t,getChunkedManifest:()=>r?.manifest??null,getSnapshot:()=>c(),query(e="",t){const r=A(p(),e,t);return i=r,s=null,f(),r},async queryChunked(e="",t){if(!r)throw new Error("No AIWG chunked index manifest loaded");try{const n=await async function(e,t="",r={}){const i=t.trim().toLowerCase();let s=0,n=0;if(function(e,t){return!(""!==e.trim()||t.rank||t.snippets||t.includeMatches||t.types||t.facets||t.tags||t.concepts||t.privacy||t.relationshipTargetId)}(t,r)){const t=r.offset??0,i=r.limit??e.manifest.total,a=function(e,t,r){const i=t+r;return e.parts.filter(e=>e.count>0&&e.offset<i&&e.offset+e.count>t)}(e.manifest,t,i),o=[];for(const c of a){const u=await j(e,c);u.fetched&&(n+=1),s+=1,r.onProgress?.({phase:"part",done:s,total:a.length,href:c.href});const f=Math.max(0,t-c.offset),d=Math.min(u.part.items.length,t+i-c.offset);o.push(...u.part.items.slice(f,d))}return{items:o,total:e.manifest.total,facets:e.manifest.facets??{},manifestTotal:e.manifest.total,scannedParts:s,fetchedParts:n,complete:!0}}const a=[];for(const t of e.manifest.parts){const o=await j(e,t);o.fetched&&(n+=1),s+=1,r.onProgress?.({phase:"part",done:s,total:e.manifest.parts.length,href:t.href}),a.push(...b(o.part.items,i,r,t.offset)),r.onProgress?.({phase:"query",done:s,total:e.manifest.parts.length,href:t.href})}return{...C(a,i,r),manifestTotal:e.manifest.total,scannedParts:s,fetchedParts:n,complete:!0}}(r,e,t);return i=n,s=null,f(),n}catch(e){throw s=e instanceof Error?e:new Error(String(e)),f(),s}},async getRecord(e){if(r)try{return await async function(e,t){const r=e.detailCache.get(t);if(r)return e.detailCache.delete(t),e.detailCache.set(t,r),r;if(!e.manifest.projection)for(const r of e.partCache.values()){const e=r.items.find(e=>e.id===t);if(e)return e}if(!e.detailLoader)throw new Error("No detailLoader configured to resolve record "+t);const i=await e.detailLoader(t,e.manifest),s=u({schema_version:"aiwg.fortemi.index.export.v1",generated_at:e.manifest.generated_at,source:e.manifest.source,items:[i]}).items[0];if(s.id!==t)throw new Error("Detail record id mismatch: expected "+t+", got "+s.id);for(e.detailCache.set(t,s);e.detailCache.size>e.maxCachedDetails;){const t=e.detailCache.keys().next().value;if(void 0===t)break;e.detailCache.delete(t)}return s}(r,e)}catch(e){throw s=e instanceof Error?e:new Error(String(e)),f(),s}const t=p().items.find(t=>t.id===e);if(!t)throw new Error("Record not found: "+e);return t},clearChunkCache(){r?.partCache.clear(),r?.detailCache.clear(),s=null,f()},toCommunityGraph:e=>E(p(),e),setReviewDecision(e){const t={...e,updated_at:(new Date).toISOString()};return n=[...n.filter(e=>e.item_id!==t.item_id),t].sort((e,t)=>e.item_id.localeCompare(t.item_id)),s=null,f(),t},clearReviewDecision(e){n=n.filter(t=>t.item_id!==e),s=null,f()},createReviewDecisionExport:e=>S(p(),n,e),subscribe:e=>(a.add(e),()=>{a.delete(e)})}}function E(e,t={}){const r=new Set(e.items.map(e=>e.id)),i=t.relationshipWeights??{},s=new Map;for(const n of e.items)for(const e of n.relationships){if(!r.has(e.target_id)&&!t.includeDanglingRelationships)continue;const a=e.type,o=i[a]??1,c=`${n.id}\0${e.target_id}\0${a}`,u=s.get(c);u?u.weight+=o:s.set(c,{source:n.id,target:e.target_id,kind:a,weight:o})}const n=new Map;for(const r of e.items){const e=I(r,t);for(const t of e){const e=n.get(t)??[];e.push(r.id),n.set(t,e)}}return{nodes:e.items.map(e=>({id:e.id})),edges:Array.from(s.values()).sort((e,t)=>e.source.localeCompare(t.source)||e.target.localeCompare(t.target)||e.kind.localeCompare(t.kind)),communities:Array.from(n.entries()).map(([e,t])=>({id:e,nodes:[...new Set(t)].sort()})).sort((e,t)=>e.id.localeCompare(t.id))}}function I(e,t){if(t.communityFacet){const r=e.facets[t.communityFacet]??[];if(r.length>0)return r.map(e=>`${t.communityFacet}:${e}`)}if(t.communityTagPrefix){const r=t.communityTagPrefix,i=e.tags.filter(e=>e.startsWith(r));if(i.length>0)return i}return e.concepts.length>0?e.concepts.map(e=>`concept:${e}`):[`type:${e.type}`]}export{e as AIWG_SCAN_REQUIRED_FIELDS,E as aiwgFortemiIndexToCommunityGraph,d as assertAiwgFortemiChunkManifest,h as assertAiwgFortemiChunkPart,u as assertAiwgFortemiIndexExport,y as buildAiwgChunkedIndex,m as createAiwgFetchChunkLoader,l as createAiwgFetchDetailLoader,q as createAiwgIndexController,S as createAiwgReviewDecisionExport,g as getAiwgFortemiFacets,A as queryAiwgFortemiIndex,f as validateAiwgFortemiChunkManifest,p as validateAiwgFortemiChunkPart,c as validateAiwgFortemiIndexExport};
@@ -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,8 +1,8 @@
1
1
  /**
2
2
  * VENDORED — @fortemi/core/aiwg-index
3
3
  *
4
- * Source : @fortemi/core@2026.6.2 → dist/aiwg-index.js
5
- * SHA-256: fea23e545e604b1b93438f8bec73520dd16bf4f0ba2a81e35ee8d716495732ac (upstream dist file)
4
+ * Source : @fortemi/core@2026.6.3 → dist/aiwg-index.js
5
+ * SHA-256: f2e6793fd7e52e20441459e18633a9546c7cfc6281cbc63ab28f0767422946bf (upstream dist file)
6
6
  * License: AGPL-3.0-only (compatible with this package's AGPL-3.0-or-later)
7
7
  * Why : Pagenary's publisher build is a no-bundler copy-src→dist pipeline that
8
8
  * loads ES modules by relative path; bare specifiers (`@fortemi/core`)
@@ -11,8 +11,20 @@
11
11
  * browser (runtime search). See .aiwg/architecture/adr/ADR-015-*.md.
12
12
  * Update : Re-vendor by copying the dist file from a newer @fortemi/core release
13
13
  * and refreshing the SHA-256 above. Do not hand-edit below this banner.
14
+ * 6.3 adds buildAiwgChunkedIndex / createAiwgFetchDetailLoader (additive).
14
15
  */
15
16
  // src/aiwg-index.ts
17
+ var AIWG_SCAN_REQUIRED_FIELDS = [
18
+ "schema_version",
19
+ "id",
20
+ "type",
21
+ "title",
22
+ "text",
23
+ "facets",
24
+ "tags",
25
+ "concepts",
26
+ "privacy"
27
+ ];
16
28
  var REQUIRED_RECORD_FIELDS = [
17
29
  "schema_version",
18
30
  "id",
@@ -124,6 +136,20 @@ function validateAiwgFortemiChunkManifest(value) {
124
136
  if (data.facets !== void 0 && !isFacetCounts(data.facets)) {
125
137
  errors.push("facets must be a nested string-to-number count object");
126
138
  }
139
+ if (data.projection !== void 0) {
140
+ if (!Array.isArray(data.projection) || !data.projection.every((field) => typeof field === "string")) {
141
+ errors.push("projection must be an array of field names");
142
+ } else {
143
+ const present = new Set(data.projection);
144
+ for (const field of AIWG_SCAN_REQUIRED_FIELDS) {
145
+ if (!present.has(field)) errors.push("projection must include scan-required field " + field);
146
+ }
147
+ }
148
+ }
149
+ if (data.detail !== void 0) {
150
+ if (!hasString(data.detail.href)) errors.push("detail.href is required");
151
+ else if (!data.detail.href.includes("{id}")) errors.push("detail.href must contain the {id} placeholder");
152
+ }
127
153
  if (!Array.isArray(data?.parts)) errors.push("parts must be an array");
128
154
  let expectedOffset = 0;
129
155
  const parts = Array.isArray(data?.parts) ? data.parts : [];
@@ -148,6 +174,35 @@ function assertAiwgFortemiChunkManifest(value) {
148
174
  }
149
175
  return value;
150
176
  }
177
+ function validateProjectedRecords(items) {
178
+ const errors = [];
179
+ const ids = /* @__PURE__ */ new Set();
180
+ let previousId = "";
181
+ for (const [index, item] of items.entries()) {
182
+ if (item.schema_version !== "aiwg.fortemi.index.record.v1") {
183
+ errors.push("items[" + index + "].schema_version must be aiwg.fortemi.index.record.v1");
184
+ }
185
+ if (!hasString(item.id)) errors.push("items[" + index + "].id is required");
186
+ if (hasString(item.id) && ids.has(item.id)) errors.push("duplicate id: " + item.id);
187
+ if (hasString(item.id)) ids.add(item.id);
188
+ if (previousId && hasString(item.id) && previousId.localeCompare(item.id) > 0) {
189
+ errors.push("items must be sorted by id: " + previousId + " before " + item.id);
190
+ }
191
+ if (hasString(item.id)) previousId = item.id;
192
+ if (!item.type || !VALID_TYPES.has(item.type)) errors.push("items[" + index + "].type is invalid");
193
+ if (!hasString(item.title)) errors.push("items[" + index + "].title is required");
194
+ if (typeof item.text !== "string") errors.push("items[" + index + "].text is required");
195
+ if (!item.facets || typeof item.facets !== "object" || Array.isArray(item.facets)) {
196
+ errors.push("items[" + index + "].facets must be an object");
197
+ }
198
+ if (!Array.isArray(item.tags)) errors.push("items[" + index + "].tags must be an array");
199
+ if (!Array.isArray(item.concepts)) errors.push("items[" + index + "].concepts must be an array");
200
+ if (!item.privacy || !hasString(item.privacy.classification)) {
201
+ errors.push("items[" + index + "].privacy.classification is required");
202
+ }
203
+ }
204
+ return errors;
205
+ }
151
206
  function validateAiwgFortemiChunkPart(value, partRef, manifest) {
152
207
  const errors = [];
153
208
  const data = value;
@@ -166,13 +221,17 @@ function validateAiwgFortemiChunkPart(value, partRef, manifest) {
166
221
  errors.push("items length must match manifest part count " + partRef.count);
167
222
  }
168
223
  if (Array.isArray(data?.items)) {
169
- const validation = validateAiwgFortemiIndexExport({
170
- schema_version: "aiwg.fortemi.index.export.v1",
171
- generated_at: manifest?.generated_at ?? "1970-01-01T00:00:00.000Z",
172
- source: manifest?.source ?? { repo: "chunk", privacy: "public" },
173
- items: data.items
174
- });
175
- errors.push(...validation.errors.map((error) => "items." + error));
224
+ if (manifest?.projection) {
225
+ errors.push(...validateProjectedRecords(data.items).map((error) => "items." + error));
226
+ } else {
227
+ const validation = validateAiwgFortemiIndexExport({
228
+ schema_version: "aiwg.fortemi.index.export.v1",
229
+ generated_at: manifest?.generated_at ?? "1970-01-01T00:00:00.000Z",
230
+ source: manifest?.source ?? { repo: "chunk", privacy: "public" },
231
+ items: data.items
232
+ });
233
+ errors.push(...validation.errors.map((error) => "items." + error));
234
+ }
176
235
  }
177
236
  return { valid: errors.length === 0, errors };
178
237
  }
@@ -191,6 +250,16 @@ function createAiwgFetchChunkLoader(baseUrl) {
191
250
  return response.json();
192
251
  };
193
252
  }
253
+ function createAiwgFetchDetailLoader(baseUrl) {
254
+ return async (id, manifest) => {
255
+ if (!manifest.detail?.href) throw new Error("Manifest has no detail.href for record resolution");
256
+ const relative = manifest.detail.href.replace("{id}", encodeURIComponent(id));
257
+ const href = baseUrl ? new URL(relative, baseUrl).toString() : relative;
258
+ const response = await fetch(href);
259
+ if (!response.ok) throw new Error("Failed to fetch AIWG index detail " + href + ": " + response.status);
260
+ return response.json();
261
+ };
262
+ }
194
263
  function getAiwgFortemiFacets(items) {
195
264
  const result = {};
196
265
  for (const item of items) {
@@ -204,6 +273,49 @@ function getAiwgFortemiFacets(items) {
204
273
  }
205
274
  return result;
206
275
  }
276
+ function buildAiwgChunkedIndex(index, options = {}) {
277
+ const partSize = hasPositiveInteger(options.partSize) ? options.partSize : 500;
278
+ const projection = options.projection;
279
+ const items = index.items;
280
+ const pad = (value) => String(value).padStart(4, "0");
281
+ const project = (record) => {
282
+ if (!projection) return record;
283
+ const slim = {};
284
+ for (const field of projection) slim[field] = record[field];
285
+ return slim;
286
+ };
287
+ const parts = [];
288
+ const partRefs = [];
289
+ for (let offset = 0, partIndex = 0; offset < items.length; offset += partSize, partIndex += 1) {
290
+ const slice = items.slice(offset, offset + partSize);
291
+ const href = "part-" + pad(partIndex) + ".json";
292
+ parts.push({
293
+ href,
294
+ part: {
295
+ schema_version: "aiwg.fortemi.index.chunk.v1",
296
+ manifest_schema_version: "aiwg.fortemi.index.chunk-manifest.v1",
297
+ offset,
298
+ items: slice.map(project)
299
+ }
300
+ });
301
+ partRefs.push({ href, offset, count: slice.length });
302
+ }
303
+ const manifest = {
304
+ schema_version: "aiwg.fortemi.index.chunk-manifest.v1",
305
+ generated_at: options.generatedAt ?? index.generated_at,
306
+ source: index.source,
307
+ total: items.length,
308
+ part_size: partSize,
309
+ facets: getAiwgFortemiFacets(items),
310
+ parts: partRefs,
311
+ ...projection ? { projection, detail: { href: options.detailHref ?? "detail/{id}.json" } } : {}
312
+ };
313
+ return {
314
+ manifest,
315
+ parts,
316
+ details: projection ? items.map((record) => ({ id: record.id, record })) : []
317
+ };
318
+ }
207
319
  function includesAll(actual, expected) {
208
320
  if (!expected || expected.length === 0) return true;
209
321
  const actualSet = new Set(actual);
@@ -258,7 +370,7 @@ function createRankedEntries(items, q, options, ordinalBase = 0) {
258
370
  if (!includesAll(item.tags, options.tags)) return false;
259
371
  if (!includesAll(item.concepts, options.concepts)) return false;
260
372
  if (!matchesFacetFilters(item, options.facets)) return false;
261
- if (options.relationshipTargetId && !item.relationships.some((rel) => rel.target_id === options.relationshipTargetId)) {
373
+ if (options.relationshipTargetId && !(item.relationships ?? []).some((rel) => rel.target_id === options.relationshipTargetId)) {
262
374
  return false;
263
375
  }
264
376
  return true;
@@ -307,6 +419,10 @@ function clampMaxCachedParts(value) {
307
419
  if (!hasPositiveInteger(value)) return 3;
308
420
  return value;
309
421
  }
422
+ function clampMaxCachedDetails(value) {
423
+ if (!hasPositiveInteger(value)) return 32;
424
+ return value;
425
+ }
310
426
  function isDirectChunkBrowse(query, options) {
311
427
  return query.trim() === "" && !options.rank && !options.snippets && !options.includeMatches && !options.types && !options.facets && !options.tags && !options.concepts && !options.privacy && !options.relationshipTargetId;
312
428
  }
@@ -331,6 +447,40 @@ async function loadChunkPart(runtime, part) {
331
447
  }
332
448
  return { part: parsed, fetched: true };
333
449
  }
450
+ async function getChunkRecord(runtime, id) {
451
+ const cached = runtime.detailCache.get(id);
452
+ if (cached) {
453
+ runtime.detailCache.delete(id);
454
+ runtime.detailCache.set(id, cached);
455
+ return cached;
456
+ }
457
+ if (!runtime.manifest.projection) {
458
+ for (const part of runtime.partCache.values()) {
459
+ const found = part.items.find((item) => item.id === id);
460
+ if (found) return found;
461
+ }
462
+ }
463
+ if (!runtime.detailLoader) {
464
+ throw new Error("No detailLoader configured to resolve record " + id);
465
+ }
466
+ const raw = await runtime.detailLoader(id, runtime.manifest);
467
+ const record = assertAiwgFortemiIndexExport({
468
+ schema_version: "aiwg.fortemi.index.export.v1",
469
+ generated_at: runtime.manifest.generated_at,
470
+ source: runtime.manifest.source,
471
+ items: [raw]
472
+ }).items[0];
473
+ if (record.id !== id) {
474
+ throw new Error("Detail record id mismatch: expected " + id + ", got " + record.id);
475
+ }
476
+ runtime.detailCache.set(id, record);
477
+ while (runtime.detailCache.size > runtime.maxCachedDetails) {
478
+ const oldest = runtime.detailCache.keys().next().value;
479
+ if (oldest === void 0) break;
480
+ runtime.detailCache.delete(oldest);
481
+ }
482
+ return record;
483
+ }
334
484
  async function queryChunkedAiwgFortemiIndex(runtime, query = "", options = {}) {
335
485
  const q = query.trim().toLowerCase();
336
486
  let scannedParts = 0;
@@ -435,7 +585,10 @@ function createAiwgIndexController(initialIndex) {
435
585
  manifest: parsed,
436
586
  loader,
437
587
  maxCachedParts: clampMaxCachedParts(options.maxCachedParts),
438
- partCache: /* @__PURE__ */ new Map()
588
+ partCache: /* @__PURE__ */ new Map(),
589
+ detailLoader: options.detailLoader,
590
+ maxCachedDetails: clampMaxCachedDetails(options.maxCachedDetails),
591
+ detailCache: /* @__PURE__ */ new Map()
439
592
  };
440
593
  data = null;
441
594
  reviewDecisions = [];
@@ -478,8 +631,23 @@ function createAiwgIndexController(initialIndex) {
478
631
  throw error;
479
632
  }
480
633
  },
634
+ async getRecord(id) {
635
+ if (chunked) {
636
+ try {
637
+ return await getChunkRecord(chunked, id);
638
+ } catch (err) {
639
+ error = err instanceof Error ? err : new Error(String(err));
640
+ notify();
641
+ throw error;
642
+ }
643
+ }
644
+ const found = requireIndex().items.find((item) => item.id === id);
645
+ if (!found) throw new Error("Record not found: " + id);
646
+ return found;
647
+ },
481
648
  clearChunkCache() {
482
649
  chunked?.partCache.clear();
650
+ chunked?.detailCache.clear();
483
651
  error = null;
484
652
  notify();
485
653
  },
@@ -559,6 +727,6 @@ function communityIdsFor(item, options) {
559
727
  return [`type:${item.type}`];
560
728
  }
561
729
 
562
- export { aiwgFortemiIndexToCommunityGraph, assertAiwgFortemiChunkManifest, assertAiwgFortemiChunkPart, assertAiwgFortemiIndexExport, createAiwgFetchChunkLoader, createAiwgIndexController, createAiwgReviewDecisionExport, getAiwgFortemiFacets, queryAiwgFortemiIndex, validateAiwgFortemiChunkManifest, validateAiwgFortemiChunkPart, validateAiwgFortemiIndexExport };
730
+ export { AIWG_SCAN_REQUIRED_FIELDS, aiwgFortemiIndexToCommunityGraph, assertAiwgFortemiChunkManifest, assertAiwgFortemiChunkPart, assertAiwgFortemiIndexExport, buildAiwgChunkedIndex, createAiwgFetchChunkLoader, createAiwgFetchDetailLoader, createAiwgIndexController, createAiwgReviewDecisionExport, getAiwgFortemiFacets, queryAiwgFortemiIndex, validateAiwgFortemiChunkManifest, validateAiwgFortemiChunkPart, validateAiwgFortemiIndexExport };
563
731
  //# sourceMappingURL=aiwg-index.js.map
564
732
  //# sourceMappingURL=aiwg-index.js.map