@pagenary/publisher 2026.6.0 → 2026.6.1

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
@@ -6,6 +6,9 @@
6
6
 
7
7
  `@pagenary/publisher` is the static site generator behind Pagenary — it turns one shared template catalog into many branded, tenant-specific documentation sites. Zero runtime dependencies, hash-based routing, full-text search, and a Git-aware build pipeline. Install it as a dev dependency and drive it with the `pagenary` CLI.
8
8
 
9
+ Built with [AIWG](https://aiwg.io), the multi-agent AI framework used to plan,
10
+ audit, and ship this project.
11
+
9
12
  ```bash
10
13
  npm install --save-dev @pagenary/publisher # add Pagenary to your project
11
14
  npx pagenary build:tenants my-docs # build your docs tenant
@@ -17,6 +20,8 @@ npx pagenary serve # serve on http://localhost:5173
17
20
  [![Docs](https://img.shields.io/badge/docs-docs.pagenary.com-22d3ee?style=flat-square&logo=readthedocs&logoColor=white)](https://docs.pagenary.com)
18
21
  [![License: AGPL v3](https://img.shields.io/badge/License-AGPL_v3-blue.svg?style=flat-square)](../../LICENSE)
19
22
  [![Node Version](https://img.shields.io/badge/node-%E2%89%A516.0.0-brightgreen?style=flat-square&logo=node.js)](https://nodejs.org)
23
+ [![Search: @fortemi/core](https://img.shields.io/npm/v/@fortemi/core?label=search%20%C2%B7%20%40fortemi%2Fcore&color=CB3837&logo=npm&style=flat-square)](https://www.npmjs.com/package/@fortemi/core)
24
+ [![Built with AIWG](https://img.shields.io/npm/v/aiwg?label=built%20with%20%C2%B7%20aiwg&color=7c3aed&logo=npm&style=flat-square)](https://www.npmjs.com/package/aiwg)
20
25
 
21
26
  [**Docs Site**](https://docs.pagenary.com) · [**Quick Start**](#quick-start) · [**Features**](#features) · [**Tenant Workflow**](#tenant-content-workflow) · [**Documentation**](#documentation)
22
27
 
@@ -53,6 +58,9 @@ npm run dev # build + serve with watch mode
53
58
  npm run build # build default bundle to dist/
54
59
  ```
55
60
 
61
+ Pagenary development uses [AIWG](https://aiwg.io). On this host, maintainers can
62
+ inspect, build, or run the AIWG project from `~/dev/aiwg`.
63
+
56
64
  ---
57
65
 
58
66
  ## Features
@@ -78,7 +86,10 @@ npm run build # build default bundle to dist/
78
86
 
79
87
  ### Navigation & Search
80
88
  - **Command Palette** — `Ctrl/Cmd+K` or `/` opens a global finder
81
- - **Full-Text Search** — searches all content, not just titles
89
+ - **Fortemi-backed full-text search** — ranked results with snippets over a static
90
+ chunked index emitted at build time; lazy chunk fetch (precache) and offset
91
+ paging for infinite scroll, with a clean in-browser fallback. No server, no WASM.
92
+ See `docs/ARCHITECTURE.md` and `.aiwg/architecture/adr/ADR-015-fortemi-core-search-adapter.md`.
82
93
  - **Manifest-Driven Nav** — declarative navigation structure
83
94
  - **Keyboard Navigation** — arrow keys, Enter to select
84
95
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pagenary/publisher",
3
- "version": "2026.6.0",
3
+ "version": "2026.6.1",
4
4
  "type": "module",
5
5
  "description": "Multi-tenant static publishing component for Pagenary platform.",
6
6
  "license": "AGPL-3.0-or-later",
@@ -9,6 +9,7 @@ import os from 'os';
9
9
  import { generateSeoArtifacts, resolveBaseUrl, resolveOgImage } from './lib/seo-generator.js';
10
10
  import { generateCollections } from './lib/collections-generator.js';
11
11
  import { parseFrontmatter } from './lib/frontmatter.js';
12
+ import { generateSearchIndex } from './lib/search-index-generator.js';
12
13
  import { fileURLToPath } from 'node:url';
13
14
 
14
15
  const root = process.cwd();
@@ -2575,14 +2576,14 @@ async function materializeScannedSections(sections, context) {
2575
2576
  title,
2576
2577
  summary,
2577
2578
  ...metadata,
2578
- module: `/sections/${outFile}`,
2579
+ module: `./sections/${outFile}`,
2579
2580
  subsections: processedSubsections
2580
2581
  };
2581
2582
  if (type) entry.type = type;
2582
2583
  if (collapsed) entry.collapsed = true;
2583
2584
  processed.push(entry);
2584
2585
  } else {
2585
- const entry = { id, title, summary, ...metadata, module: `/sections/${outFile}` };
2586
+ const entry = { id, title, summary, ...metadata, module: `./sections/${outFile}` };
2586
2587
  if (type) entry.type = type;
2587
2588
  if (collapsed) entry.collapsed = true;
2588
2589
  processed.push(entry);
@@ -2811,6 +2812,14 @@ async function processNestedContent(sourceDir, distDir, tenantId, contentRoot, o
2811
2812
  await fsp.writeFile(path.join(distDir, 'manifest.js'), manifestModule, 'utf8');
2812
2813
  console.log(` ↳ applied nested content structure for ${tenantId} (${context.leafOrder.length} sections)`);
2813
2814
 
2815
+ // Emit the static Fortemi chunked search index. Failure here never breaks the
2816
+ // bundle — the runtime search adapter degrades to its legacy in-browser index.
2817
+ try {
2818
+ await generateSearchIndex(distDir, processedManifest, { tenantId });
2819
+ } catch (err) {
2820
+ console.warn(` ↳ search index generation skipped for ${tenantId}: ${err.message}`);
2821
+ }
2822
+
2814
2823
  return { success: true, sectionsCount: context.leafOrder.length };
2815
2824
  }
2816
2825
 
@@ -2887,7 +2896,7 @@ async function materializeSectionModule(entry, context) {
2887
2896
  return null;
2888
2897
  }
2889
2898
 
2890
- return `/sections/${outFile}`;
2899
+ return `./sections/${outFile}`;
2891
2900
  }
2892
2901
 
2893
2902
  function buildManifestModuleSource(manifestEntries, defaultSection, siteConfig = {}, exportConfig = {}) {
@@ -3111,6 +3120,14 @@ async function processTenantManifestLegacy(sourceDir, distDir, tenantId, options
3111
3120
  const manifestModule = buildManifestModuleSource(processedManifest, defaultSection, context.siteConfig);
3112
3121
  await fsp.writeFile(path.join(distDir, 'manifest.js'), manifestModule, 'utf8');
3113
3122
  console.log(` ↳ applied manifest-driven content for ${tenantId}`);
3123
+
3124
+ // Emit the static Fortemi chunked search index (legacy manifest path).
3125
+ try {
3126
+ await generateSearchIndex(distDir, processedManifest, { tenantId });
3127
+ } catch (err) {
3128
+ console.warn(` ↳ search index generation skipped for ${tenantId}: ${err.message}`);
3129
+ }
3130
+
3114
3131
  return { success: true };
3115
3132
  }
3116
3133
 
@@ -0,0 +1,111 @@
1
+ /**
2
+ * Build-time Fortemi search-index generator.
3
+ *
4
+ * After a tenant's manifest.js + sections/ are materialized, this emits a
5
+ * deterministic chunked static index that the runtime adapter (src/lib/search.js)
6
+ * loads through the vendored @fortemi/core controller:
7
+ *
8
+ * dist/<tenant>/search-index/manifest.json (AiwgFortemiChunkManifest)
9
+ * dist/<tenant>/search-index/part-0000.json (AiwgFortemiChunkPart)
10
+ * ...
11
+ *
12
+ * Text is extracted by importing each section module and calling load(), the
13
+ * same contract the renderer uses, then stripping HTML without a DOM. The corpus
14
+ * is deterministic (sorted by record id, content-hashed generated_at) so repeat
15
+ * builds are byte-identical and incremental rebuilds refresh stale entries.
16
+ */
17
+ import fsp from 'fs/promises';
18
+ import fs from 'fs';
19
+ import path from 'path';
20
+ import { pathToFileURL } from 'node:url';
21
+ import {
22
+ buildFortemiIndexExport,
23
+ chunkFortemiIndex,
24
+ stripHtml,
25
+ DEFAULT_PART_SIZE
26
+ } from '../../src/lib/fortemi-corpus.js';
27
+ import { flattenManifest } from '../../src/lib/search.js';
28
+
29
+ const SEARCH_INDEX_DIR = 'search-index';
30
+
31
+ async function pathExists(target) {
32
+ try {
33
+ await fsp.access(target, fs.constants.F_OK);
34
+ return true;
35
+ } catch {
36
+ return false;
37
+ }
38
+ }
39
+
40
+ /**
41
+ * Import a materialized section module and return its plain-text content.
42
+ * Resilient: a module that fails to load degrades to title/summary text, exactly
43
+ * as the runtime search index does.
44
+ * @param {object} section - Flattened section with a `module` path (./sections/x.js)
45
+ * @param {string} distDir - Tenant output directory
46
+ * @returns {Promise<string>}
47
+ */
48
+ async function extractSectionText(section, distDir) {
49
+ if (!section.module) return '';
50
+ const rel = section.module.replace(/^\.?\//, '');
51
+ const abs = path.resolve(distDir, rel);
52
+ if (!(await pathExists(abs))) return '';
53
+ try {
54
+ const mod = await import(pathToFileURL(abs).href);
55
+ const loader = mod.load || mod.default;
56
+ if (typeof loader !== 'function') return '';
57
+ const payload = await loader();
58
+ return stripHtml(payload && payload.html ? payload.html : '');
59
+ } catch {
60
+ return '';
61
+ }
62
+ }
63
+
64
+ /**
65
+ * Generate and write the chunked Fortemi index for one tenant bundle.
66
+ * @param {string} distDir - Tenant output directory (contains manifest.js, sections/)
67
+ * @param {Array} processedManifest - The nested manifest array written to manifest.js
68
+ * @param {object} [options]
69
+ * @param {string} [options.tenantId='pagenary']
70
+ * @param {number} [options.partSize=DEFAULT_PART_SIZE]
71
+ * @returns {Promise<{ total: number, parts: number, buildHash: string }>}
72
+ */
73
+ export async function generateSearchIndex(distDir, processedManifest, options = {}) {
74
+ const tenantId = options.tenantId || 'pagenary';
75
+ const partSize = options.partSize || DEFAULT_PART_SIZE;
76
+ const outDir = path.join(distDir, SEARCH_INDEX_DIR);
77
+
78
+ const flat = flattenManifest(processedManifest || []);
79
+ const entries = [];
80
+ for (const section of flat) {
81
+ if (!section || !section.id) continue;
82
+ const content = await extractSectionText(section, distDir);
83
+ const text = `${section.title || ''} ${section.summary || ''} ${section.group || ''} ${content}`.trim();
84
+ entries.push({ section, text });
85
+ }
86
+
87
+ const { index, buildHash } = buildFortemiIndexExport(entries, { repo: tenantId });
88
+ const { manifest, parts } = chunkFortemiIndex(index, { partSize });
89
+
90
+ // Replace the directory wholesale so stale parts from a larger prior corpus
91
+ // never linger (incremental-build correctness).
92
+ await fsp.rm(outDir, { recursive: true, force: true });
93
+ await fsp.mkdir(outDir, { recursive: true });
94
+
95
+ await fsp.writeFile(
96
+ path.join(outDir, 'manifest.json'),
97
+ `${JSON.stringify(manifest, null, 2)}\n`,
98
+ 'utf8'
99
+ );
100
+ await Promise.all(parts.map((part, i) =>
101
+ fsp.writeFile(
102
+ path.join(outDir, manifest.parts[i].href),
103
+ `${JSON.stringify(part, null, 2)}\n`,
104
+ 'utf8'
105
+ )));
106
+
107
+ console.log(` ↳ search index: ${manifest.total} record(s) in ${parts.length} part(s) [${buildHash.slice(0, 8)}]`);
108
+ return { total: manifest.total, parts: parts.length, buildHash };
109
+ }
110
+
111
+ export { SEARCH_INDEX_DIR };
package/site/app.js CHANGED
@@ -1 +1 @@
1
- import{MANIFEST as e,DEFAULT_SECTION as t,findSection as n,getAdjacentSections as a,SITE_CONFIG as s,EXPORT_CONFIG as o}from"./manifest.js";import{updateMetaTags as i}from"./seo.js";import{escapeRegExp as l,searchContent as r,flattenManifest as c,findPreferredIndex as d}from"./lib/search.js";import{resolveTarget as m,resolveEntry as p}from"./lib/router.js";import{composeExportDocument as u,collectExportableSections as v}from"./lib/export.js";import{renderMermaidBlocks as h}from"./mermaid-init.js";import{highlightCodeBlocks as f}from"./syntax-highlight.js";const y=document.getElementById("app"),g=document.getElementById("nav"),b=document.getElementById("year"),E=document.getElementById("exportBtn"),L=document.getElementById("commandToggle"),T=document.getElementById("commandPalette"),x=document.getElementById("commandInput"),w=document.getElementById("commandList"),N=document.getElementById("mobileMenuToggle"),C=document.querySelector(".sidebar"),k="docs-toolkit-command-query",$=new Map,H=new Map,I=new Map,M=new Set;let S=[],A=0,P=!1,q=(localStorage.getItem(k)||"").trim(),B=!1;function D(e,t){const n=document.createElement("a");return n.href=e.url,n.target="_blank",n.rel="noopener noreferrer",n.className=`${t} nav-external`,n.title=e.summary||e.title,n.innerHTML=`\n <span class="nav-title">${e.title}<span class="nav-external-icon" aria-label="(opens in new tab)">↗</span></span>\n ${e.summary?`<span class="nav-summary">${e.summary}</span>`:""}\n `,n}function R(e,t={}){const{scrollToHighlight:a=!1}=t,{targetId:s,parentId:o}=function(e){return m(e,n)}(e);P&&W(),o&&O(o,!0),B=a||Boolean(q),location.hash.replace("#","")===s?j():location.hash=`#${s}`}function F(){return location.hash.replace("#","")||t}async function j(){const e=F(),t=p(e,n);if(!t)return;const{entry:o,targetId:l,parentId:r}=t;l===e?(r&&O(r,!0),function(e,t=null){H.forEach(e=>{e.setAttribute("aria-current","false")});const n=H.get(e);if(n&&n.setAttribute("aria-current","page"),t){const e=H.get(t);e&&e.setAttribute("aria-current","page")}}(o.id,r),await async function(e){if(!e)return;const t=await import(e.module),n=t.load||t.default;if("function"!=typeof n)return void(y.innerHTML='<article class="section"><p>Section failed to load.</p></article>');const o=await n();y.innerHTML=o.html||"",function(e){if(!e||!e.showDate&&!e.showReadingTime&&!e.showSummary)return;const t=(y.querySelector(".doc-content")||y.querySelector("article, section")||y).querySelector("h1");if(!t)return;const n=[];e.showDate&&e.date&&n.push(function(e){const t=String(e||"").trim();if(!t)return"";const n=/^\d{4}-\d{2}-\d{2}$/.test(t)?`${t}T00:00:00Z`:t,a=new Date(n);return Number.isNaN(a.getTime())?t:new Intl.DateTimeFormat(void 0,{year:"numeric",month:"long",day:"numeric",timeZone:"UTC"}).format(a)}(e.date)),e.showReadingTime&&e.reading_time&&n.push(`${e.reading_time} min read`);let a=t;if(n.length>0){const e=document.createElement("p");e.className="doc-meta",e.textContent=n.join(" · "),t.after(e),a=e}if(e.showSummary&&e.summary){const t=document.createElement("p");t.className="doc-summary",t.textContent=e.summary,a.after(t)}}(e),await h(y),await f(y),function(e){const t=y.querySelector(".bottom-nav");if(t&&t.remove(),"never"===s.bottomNav)return;const n=(s.bottomNavSections||[]).some(t=>e.startsWith(t)),o="mobile"===s.bottomNav&&!n,{prev:i,next:l}=a(e);if(!i&&!l)return;const r=document.createElement("nav");if(r.className="bottom-nav",o&&r.classList.add("mobile-only"),i){const e=document.createElement("div");e.className="bottom-nav-item bottom-nav-prev",e.innerHTML='<span class="bottom-nav-chevron">‹</span>';const t=document.createElement("a");t.href=`#${i.id}`,t.className="bottom-nav-link",t.title=`Previous: ${i.title}`,t.textContent=i.title,t.addEventListener("click",e=>{e.preventDefault(),R(i.id)}),e.appendChild(t),r.appendChild(e)}else{const e=document.createElement("div");e.className="bottom-nav-spacer",r.appendChild(e)}if(l){const e=document.createElement("div");e.className="bottom-nav-item bottom-nav-next";const t=document.createElement("a");t.href=`#${l.id}`,t.className="bottom-nav-link",t.title=`Next: ${l.title}`,t.textContent=l.title,t.addEventListener("click",e=>{e.preventDefault(),R(l.id)}),e.appendChild(t),e.innerHTML+='<span class="bottom-nav-chevron">›</span>',r.appendChild(e)}(y.querySelector("section")||y).appendChild(r)}(e.id),y.scrollTop=0,window.scrollTo(0,0),"function"==typeof o.afterRender&&o.afterRender(y),i({title:e.title,description:e.summary,siteTitle:s.siteTitle,siteUrl:s.siteUrl,sectionId:e.id,ogImage:e.ogImage||s.ogImage}),$.set(e.id,Date.now());const l=B;B=!1,K(l),requestAnimationFrame(()=>y.focus())}(o)):location.replace(`#${l}`)}function O(e,t){if(!e)return;const n=I.get(e);t?(M.add(e),n&&n.group.classList.add("expanded")):(M.delete(e),n&&n.group.classList.remove("expanded"))}function _(){if(!T||!x)return;P=!0,T.hidden=!1;const e=q;x.value=e,z(e),requestAnimationFrame(()=>{x.focus(),e&&x.select()})}function W(){T&&x&&(P=!1,T.hidden=!0,x.blur())}!function(){g.innerHTML="",H.clear(),I.clear();let t=M.size>0;e.forEach((e,n)=>{if(e.url){const t=D(e,"nav-leaf");return void g.appendChild(t)}if(e.subsections&&e.subsections.length){const n=document.createElement("div");n.className="nav-group";const a=Boolean(e.module),s=document.createElement("button");s.type="button",s.className="nav-parent"+(a?" nav-parent-with-content":""),s.dataset.section=e.id,s.title=e.summary,a?(s.innerHTML=`\n <span class="nav-title-link">${e.title}</span>\n <span class="nav-expand-toggle" aria-label="Expand"></span>\n ${e.summary?`<span class="nav-summary">${e.summary}</span>`:""}\n `,s.querySelector(".nav-title-link").addEventListener("click",t=>{t.stopPropagation(),R(e.id,{scrollToHighlight:Boolean(q)})}),s.querySelector(".nav-expand-toggle").addEventListener("click",t=>{t.stopPropagation();const n=!M.has(e.id);O(e.id,n)}),s.addEventListener("click",t=>{if(t.target===s){const t=!M.has(e.id);O(e.id,t)}})):(s.innerHTML=`\n <span class="nav-title">${e.title}</span>\n ${e.summary?`<span class="nav-summary">${e.summary}</span>`:""}\n `,s.addEventListener("click",()=>{const t=!M.has(e.id);O(e.id,t)}));const o=document.createElement("div");o.className="nav-sublist",e.subsections.forEach(e=>{if(e.url){const t=D(e,"nav-item");return void o.appendChild(t)}if(e.subsections&&e.subsections.length){const t=document.createElement("div");t.className="nav-group nav-group-nested";const n=document.createElement("button");n.type="button",n.className="nav-parent nav-parent-nested",n.dataset.section=e.id,n.title=e.summary||e.title,n.innerHTML=`<span class="nav-title">${e.title}</span>`,n.addEventListener("click",()=>{const t=!M.has(e.id);O(e.id,t)});const a=document.createElement("div");a.className="nav-sublist nav-sublist-nested",e.subsections.forEach(e=>{if(e.url){const t=D(e,"nav-item");return void a.appendChild(t)}if(e.subsections&&e.subsections.length){const t=document.createElement("div");t.className="nav-group nav-group-deep";const n=document.createElement("button");n.type="button",n.className="nav-parent nav-parent-deep",n.dataset.section=e.id,n.title=e.summary||e.title,n.innerHTML=`<span class="nav-title">${e.title}</span>`,n.addEventListener("click",()=>{const t=!M.has(e.id);O(e.id,t)});const s=document.createElement("div");s.className="nav-sublist nav-sublist-deep",e.subsections.forEach(e=>{if(e.url){const t=D(e,"nav-item");return void s.appendChild(t)}if(e.subsections&&e.subsections.length){const t=document.createElement("div");t.className="nav-group nav-group-ultra";const n=document.createElement("button");n.type="button",n.className="nav-parent nav-parent-ultra",n.dataset.section=e.id,n.title=e.summary||e.title,n.innerHTML=`<span class="nav-title">${e.title}</span>`,n.addEventListener("click",()=>{const t=!M.has(e.id);O(e.id,t)});const a=document.createElement("div");a.className="nav-sublist nav-sublist-ultra",e.subsections.forEach(e=>{if(e.url){const t=D(e,"nav-item");return void a.appendChild(t)}const t=document.createElement("button");t.type="button",t.className="nav-item nav-item-ultra"+(e.type?` nav-type-${e.type}`:""),t.dataset.section=e.id,t.title=e.summary||e.title,t.innerHTML=`\n <span class="nav-title">${e.title}</span>\n <span class="nav-summary">${e.summary||""}</span>\n `,t.addEventListener("click",()=>R(e.id,{scrollToHighlight:Boolean(q)})),a.appendChild(t),H.set(e.id,t)}),t.append(n,a),s.appendChild(t),H.set(e.id,n),I.set(e.id,{group:t,button:n,list:a});const o=M.has(e.id)&&!e.collapsed;return void O(e.id,o)}const t=document.createElement("button");t.type="button",t.className="nav-item nav-item-deep"+(e.type?` nav-type-${e.type}`:""),t.dataset.section=e.id,t.title=e.summary||e.title,t.innerHTML=`\n <span class="nav-title">${e.title}${"press-release"===e.type?'<span class="nav-type-icon" aria-label="Press Release"></span>':""}</span>\n <span class="nav-summary">${e.summary||""}</span>\n `,t.addEventListener("click",()=>R(e.id,{scrollToHighlight:Boolean(q)})),s.appendChild(t),H.set(e.id,t)}),t.append(n,s),a.appendChild(t),H.set(e.id,n),I.set(e.id,{group:t,button:n,list:s});const o=M.has(e.id)&&!e.collapsed;return void O(e.id,o)}const t=document.createElement("button");t.type="button",t.className="nav-item nav-item-nested"+(e.type?` nav-type-${e.type}`:""),t.dataset.section=e.id,t.title=e.summary||e.title,t.innerHTML=`\n <span class="nav-title">${e.title}${"press-release"===e.type?'<span class="nav-type-icon" aria-label="Press Release"></span>':""}</span>\n <span class="nav-summary">${e.summary||""}</span>\n `,t.addEventListener("click",()=>R(e.id,{scrollToHighlight:Boolean(q)})),a.appendChild(t),H.set(e.id,t)}),t.append(n,a),o.appendChild(t),H.set(e.id,n),I.set(e.id,{group:t,button:n,list:a});const s=M.has(e.id)&&!e.collapsed;return void O(e.id,s)}const t=document.createElement("button");t.type="button",t.className="nav-item"+(e.type?` nav-type-${e.type}`:""),t.dataset.section=e.id,t.title=e.summary,t.innerHTML=`\n <span class="nav-title">${e.title}${"press-release"===e.type?'<span class="nav-type-icon" aria-label="Press Release"></span>':""}</span>\n <span class="nav-summary">${e.summary}</span>\n `,t.addEventListener("click",()=>R(e.id,{scrollToHighlight:Boolean(q)})),o.appendChild(t),H.set(e.id,t)}),n.append(s,o),g.appendChild(n),H.set(e.id,s),I.set(e.id,{group:n,button:s,list:o});const i=!e.collapsed&&(M.has(e.id)||!t&&!M.size);O(e.id,i),i&&(t=!0)}else{const t=document.createElement("button");t.type="button",t.className="nav-leaf"+(e.type?` nav-type-${e.type}`:""),t.dataset.section=e.id,t.title=e.summary,t.innerHTML=`\n <span class="nav-title">${e.title}${"press-release"===e.type?'<span class="nav-type-icon" aria-label="Press Release"></span>':""}</span>\n <span class="nav-summary">${e.summary}</span>\n `,t.addEventListener("click",()=>R(e.id,{scrollToHighlight:Boolean(q)})),g.appendChild(t),H.set(e.id,t)}})}(),q&&(B=!0),window.addEventListener("hashchange",()=>{q&&(B=!0),j()}),b.textContent=(new Date).getFullYear(),j(),L&&T&&x&&w&&(L.addEventListener("click",()=>{P?W():_()}),x.addEventListener("input",()=>{const e=x.value;J(e,!0),z(e)}),x.addEventListener("keydown",e=>{const t=S.length-1;if("ArrowDown"===e.key)e.preventDefault(),A=Math.min(t,A+1),X();else if("ArrowUp"===e.key)e.preventDefault(),A=Math.max(0,A-1),X();else if("Enter"===e.key){e.preventDefault();const t=S[A];t&&(J(x.value,!0),R(t.id,{scrollToHighlight:!0}),W())}else"Escape"===e.key&&(e.preventDefault(),W())}),w.addEventListener("click",e=>{const t=e.target.closest("[data-section]");if(!t)return;const n=t.dataset.section;n&&(J(x.value,!0),R(n,{scrollToHighlight:!0}),W())}),T.addEventListener("click",e=>{e.target===T&&W()}),window.addEventListener("keydown",e=>{const t=e.target,n=t&&("INPUT"===t.tagName||"TEXTAREA"===t.tagName||t.isContentEditable),a=e.metaKey||e.ctrlKey;"k"===e.key.toLowerCase()&&a||"/"===e.key&&!n?(e.preventDefault(),P?W():_()):"Escape"===e.key&&P&&(e.preventDefault(),W())}));let U=null,V=!1;async function z(t){w&&(!V&&t.trim()&&(V=!0,w.innerHTML='<li class="cmd-item cmd-loading">Indexing content...</li>'),clearTimeout(U),U=setTimeout(async()=>{S=await r(e,t);const n=F();A=d(S,n),function(){if(w){if(w.innerHTML="",!S.length){const e=document.createElement("li");return e.className="cmd-item",e.setAttribute("aria-selected","false"),e.textContent="No matches.",void w.appendChild(e)}S.forEach(e=>{const t=document.createElement("li");t.className="cmd-item",t.dataset.section=e.id,t.setAttribute("role","option");const n=document.createElement("span");if(n.className="cmd-item-title",n.textContent=e.title,e.group){const t=document.createElement("span");t.className="cmd-item-group",t.textContent=e.group,n.prepend(t)}const a=document.createElement("span");a.className="cmd-item-summary",a.textContent=e.summary||"",t.append(n,a),w.appendChild(t)})}}(),X(),V=!1},t.trim()?150:0))}function X(){w&&Array.from(w.children).forEach((e,t)=>{const n=t===A&&S.length;e.setAttribute("aria-selected",n?"true":"false"),n&&e.scrollIntoView({block:"nearest"})})}function G(e){const t=document.createElement("div");t.innerHTML=e,t.querySelectorAll("script").forEach(e=>e.remove()),t.querySelectorAll("button").forEach(e=>e.removeAttribute("onclick")),t.querySelectorAll("mark.hl").forEach(e=>{const t=document.createTextNode(e.textContent||"");e.replaceWith(t)});const n=t.querySelector("section");return n?n.innerHTML:t.innerHTML}function J(e,t=!1){q=e.trim(),t&&(q?localStorage.setItem(k,q):localStorage.removeItem(k)),K()}function K(e=!1){y&&function(e,t,{scrollToFirst:n=!1}={}){if(!e)return;if(function(e){e&&e.querySelectorAll("mark.hl").forEach(e=>{const t=document.createTextNode(e.textContent||"");e.replaceWith(t)})}(e),!t)return;const a=t.split(/\s+/).map(e=>e.trim()).filter(Boolean);if(!a.length)return;const s=a.map(e=>e.toLowerCase()),o=new Set(["SCRIPT","STYLE","CODE","PRE"]),i=document.createTreeWalker(e,NodeFilter.SHOW_TEXT,{acceptNode(e){if(!e.nodeValue||!e.nodeValue.trim())return NodeFilter.FILTER_REJECT;const t=e.parentNode;return t&&o.has(t.tagName)?NodeFilter.FILTER_REJECT:NodeFilter.FILTER_ACCEPT}}),r=[];let c;for(;c=i.nextNode();){const e=c.nodeValue.toLowerCase();s.some(t=>e.includes(t))&&r.push(c)}const d=new RegExp(`(${a.map(l).join("|")})`,"gi");r.forEach(e=>{const t=e.nodeValue,n=[];let a=0;t.replace(d,(e,s,o)=>{o>a&&n.push(document.createTextNode(t.slice(a,o)));const i=document.createElement("mark");return i.className="hl",i.textContent=e,n.push(i),a=o+e.length,e}),a<t.length&&n.push(document.createTextNode(t.slice(a)));const s=document.createDocumentFragment();n.forEach(e=>s.appendChild(e)),e.parentNode.replaceChild(s,e)}),n&&requestAnimationFrame(()=>{const t=e.querySelector("mark.hl");t&&t.scrollIntoView({behavior:"smooth",block:"center"})})}(y,q,{scrollToFirst:e})}E&&E.addEventListener("click",function(){const t=document.createElement("div");t.className="export-options-overlay",t.innerHTML='\n <div class="export-options-modal">\n <div class="export-options-header">EXPORT OPTIONS</div>\n <div class="export-options-buttons">\n <button type="button" class="export-option-btn" data-scope="page">\n <span class="export-option-title">Current Page</span>\n <span class="export-option-desc">Export only this section</span>\n </button>\n <button type="button" class="export-option-btn" data-scope="site">\n <span class="export-option-title">Entire Site</span>\n <span class="export-option-desc">Export all documentation</span>\n </button>\n </div>\n <button type="button" class="export-cancel-btn">Cancel</button>\n </div>\n ',document.body.appendChild(t),setTimeout(()=>t.classList.add("active"),10);const n=()=>{t.classList.remove("active"),setTimeout(()=>t.remove(),200)};t.querySelector(".export-cancel-btn").addEventListener("click",n),t.addEventListener("click",e=>{e.target===t&&n()}),t.querySelectorAll(".export-option-btn").forEach(t=>{t.addEventListener("click",()=>{const a=t.dataset.scope;n(),async function(t="site"){if(!E)return;const n=E.innerHTML,a=window.open("","_blank","width=1,height=1,left=0,top=0");if(!a||a.closed||void 0===a.closed)return void confirm("Pop-ups are blocked. Please allow pop-ups for this site to export the document.\n\nWould you like to try again after enabling pop-ups?");a.close(),E.disabled=!0;const s=document.createElement("div");s.className="export-loading-overlay",s.innerHTML='\n <div class="export-loading-modal">\n <div class="export-loading-header">\n <div class="export-loading-title">COMPILING DOCUMENTATION</div>\n <div class="export-loading-subtitle">Assembling all sections into unified document</div>\n </div>\n <div class="export-loading-progress">\n <div class="export-loading-bar">\n <div class="export-loading-fill"></div>\n </div>\n <div class="export-loading-status-container">\n <div class="export-loading-status">Initializing...</div>\n </div>\n </div>\n <div class="export-loading-scanner">\n <div class="scanner-line"></div>\n </div>\n </div>\n ',document.body.appendChild(s),setTimeout(()=>s.classList.add("active"),10);const i=s.querySelector(".export-loading-fill"),l=s.querySelector(".export-loading-status");try{let n;if("page"===t){const t=F(),a=v(e).find(e=>e.id===t);n=a?[a]:[]}else n=v(e);if(0===n.length)return alert("No content available to export."),s.remove(),void(E.disabled=!1);const a=[],r=n.length;let c=0;for(const e of n){c++;const n=c/r*100;i.style.width=`${n}%`,l.textContent="page"===t?`Exporting: ${e.title}`:`Processing section ${c} of ${r}: ${e.title}`,await new Promise(e=>setTimeout(e,50));try{const t=await import(e.module),n=t.load||t.default;if("function"!=typeof n)continue;const s=G((await n()).html||"");a.push({section:e,html:s})}catch(t){console.error("Failed to include section in export",e.id,t)}}l.textContent="Generating document...",await new Promise(e=>setTimeout(e,200));const d=u(a,o);l.textContent="Opening document viewer...",await new Promise(e=>setTimeout(e,100));const m=window.open("","_blank","width=900,height=860,scrollbars=yes,resizable=yes");if(!m)return alert("Please allow pop-ups to export the document."),void s.remove();m.document.open(),m.document.write(d),m.document.close(),m.focus(),s.classList.remove("active"),setTimeout(()=>s.remove(),300)}catch(e){console.error("Export failed",e),alert("Export failed. Check console for details."),s.remove()}finally{E.disabled=!1,E.innerHTML=n}}(a)})})}),N&&C&&(N.addEventListener("click",()=>{C.classList.contains("mobile-open")?(C.classList.remove("mobile-open"),document.body.classList.remove("menu-open"),N.setAttribute("aria-expanded","false")):(C.classList.add("mobile-open"),document.body.classList.add("menu-open"),N.setAttribute("aria-expanded","true"))}),g.addEventListener("click",e=>{if(window.innerWidth<=960){const t=e.target.closest(".nav-item, .nav-leaf, .nav-parent");t&&(t.classList.contains("nav-item")||t.classList.contains("nav-leaf"))&&(C.classList.remove("mobile-open"),document.body.classList.remove("menu-open"),N.setAttribute("aria-expanded","false"))}}),document.addEventListener("click",e=>{window.innerWidth<=960&&C.classList.contains("mobile-open")&&!C.contains(e.target)&&!N.contains(e.target)&&(C.classList.remove("mobile-open"),document.body.classList.remove("menu-open"),N.setAttribute("aria-expanded","false"))}));
1
+ import{MANIFEST as e,DEFAULT_SECTION as t,findSection as n,getAdjacentSections as a,SITE_CONFIG as s,EXPORT_CONFIG as o}from"./manifest.js";import{updateMetaTags as i}from"./seo.js";import{escapeRegExp as l,searchContentPage as c,flattenManifest as r,findPreferredIndex as d}from"./lib/search.js";import{resolveTarget as m,resolveEntry as p}from"./lib/router.js";import{composeExportDocument as u,collectExportableSections as v}from"./lib/export.js";import{renderMermaidBlocks as h}from"./mermaid-init.js";import{highlightCodeBlocks as f}from"./syntax-highlight.js";const g=document.getElementById("app"),y=document.getElementById("nav"),b=document.getElementById("year"),E=document.getElementById("exportBtn"),L=document.getElementById("commandToggle"),T=document.getElementById("commandPalette"),x=document.getElementById("commandInput"),N=document.getElementById("commandList"),w=document.getElementById("mobileMenuToggle"),C=document.querySelector(".sidebar"),k="docs-toolkit-command-query",$=new Map,H=new Map,S=new Map,I=new Set;let M=[],A=0,q=!1,P=(localStorage.getItem(k)||"").trim(),R=!1;function B(e,t){const n=document.createElement("a");return n.href=e.url,n.target="_blank",n.rel="noopener noreferrer",n.className=`${t} nav-external`,n.title=e.summary||e.title,n.innerHTML=`\n <span class="nav-title">${e.title}<span class="nav-external-icon" aria-label="(opens in new tab)">↗</span></span>\n ${e.summary?`<span class="nav-summary">${e.summary}</span>`:""}\n `,n}function D(e,t={}){const{scrollToHighlight:a=!1}=t,{targetId:s,parentId:o}=function(e){return m(e,n)}(e);q&&W(),o&&O(o,!0),R=a||Boolean(P),location.hash.replace("#","")===s?j():location.hash=`#${s}`}function F(){return location.hash.replace("#","")||t}async function j(){const e=F(),t=p(e,n);if(!t)return;const{entry:o,targetId:l,parentId:c}=t;l===e?(c&&O(c,!0),function(e,t=null){H.forEach(e=>{e.setAttribute("aria-current","false")});const n=H.get(e);if(n&&n.setAttribute("aria-current","page"),t){const e=H.get(t);e&&e.setAttribute("aria-current","page")}}(o.id,c),await async function(e){if(!e)return;const t=await import(e.module),n=t.load||t.default;if("function"!=typeof n)return void(g.innerHTML='<article class="section"><p>Section failed to load.</p></article>');const o=await n();g.innerHTML=o.html||"",function(e){if(!e||!e.showDate&&!e.showReadingTime&&!e.showSummary)return;const t=(g.querySelector(".doc-content")||g.querySelector("article, section")||g).querySelector("h1");if(!t)return;const n=[];e.showDate&&e.date&&n.push(function(e){const t=String(e||"").trim();if(!t)return"";const n=/^\d{4}-\d{2}-\d{2}$/.test(t)?`${t}T00:00:00Z`:t,a=new Date(n);return Number.isNaN(a.getTime())?t:new Intl.DateTimeFormat(void 0,{year:"numeric",month:"long",day:"numeric",timeZone:"UTC"}).format(a)}(e.date)),e.showReadingTime&&e.reading_time&&n.push(`${e.reading_time} min read`);let a=t;if(n.length>0){const e=document.createElement("p");e.className="doc-meta",e.textContent=n.join(" · "),t.after(e),a=e}if(e.showSummary&&e.summary){const t=document.createElement("p");t.className="doc-summary",t.textContent=e.summary,a.after(t)}}(e),await h(g),await f(g),function(e){const t=g.querySelector(".bottom-nav");if(t&&t.remove(),"never"===s.bottomNav)return;const n=(s.bottomNavSections||[]).some(t=>e.startsWith(t)),o="mobile"===s.bottomNav&&!n,{prev:i,next:l}=a(e);if(!i&&!l)return;const c=document.createElement("nav");if(c.className="bottom-nav",o&&c.classList.add("mobile-only"),i){const e=document.createElement("div");e.className="bottom-nav-item bottom-nav-prev",e.innerHTML='<span class="bottom-nav-chevron">‹</span>';const t=document.createElement("a");t.href=`#${i.id}`,t.className="bottom-nav-link",t.title=`Previous: ${i.title}`,t.textContent=i.title,t.addEventListener("click",e=>{e.preventDefault(),D(i.id)}),e.appendChild(t),c.appendChild(e)}else{const e=document.createElement("div");e.className="bottom-nav-spacer",c.appendChild(e)}if(l){const e=document.createElement("div");e.className="bottom-nav-item bottom-nav-next";const t=document.createElement("a");t.href=`#${l.id}`,t.className="bottom-nav-link",t.title=`Next: ${l.title}`,t.textContent=l.title,t.addEventListener("click",e=>{e.preventDefault(),D(l.id)}),e.appendChild(t),e.innerHTML+='<span class="bottom-nav-chevron">›</span>',c.appendChild(e)}(g.querySelector("section")||g).appendChild(c)}(e.id),g.scrollTop=0,window.scrollTo(0,0),"function"==typeof o.afterRender&&o.afterRender(g),i({title:e.title,description:e.summary,siteTitle:s.siteTitle,siteUrl:s.siteUrl,sectionId:e.id,ogImage:e.ogImage||s.ogImage}),$.set(e.id,Date.now());const l=R;R=!1,Q(l),requestAnimationFrame(()=>g.focus())}(o)):location.replace(`#${l}`)}function O(e,t){if(!e)return;const n=S.get(e);t?(I.add(e),n&&n.group.classList.add("expanded")):(I.delete(e),n&&n.group.classList.remove("expanded"))}function _(){if(!T||!x)return;q=!0,T.hidden=!1;const e=P;x.value=e,G(e),requestAnimationFrame(()=>{x.focus(),e&&x.select()})}function W(){T&&x&&(q=!1,T.hidden=!0,x.blur())}!function(){y.innerHTML="",H.clear(),S.clear();let t=I.size>0;e.forEach((e,n)=>{if(e.url){const t=B(e,"nav-leaf");return void y.appendChild(t)}if(e.subsections&&e.subsections.length){const n=document.createElement("div");n.className="nav-group";const a=Boolean(e.module),s=document.createElement("button");s.type="button",s.className="nav-parent"+(a?" nav-parent-with-content":""),s.dataset.section=e.id,s.title=e.summary,a?(s.innerHTML=`\n <span class="nav-title-link">${e.title}</span>\n <span class="nav-expand-toggle" aria-label="Expand"></span>\n ${e.summary?`<span class="nav-summary">${e.summary}</span>`:""}\n `,s.querySelector(".nav-title-link").addEventListener("click",t=>{t.stopPropagation(),D(e.id,{scrollToHighlight:Boolean(P)})}),s.querySelector(".nav-expand-toggle").addEventListener("click",t=>{t.stopPropagation();const n=!I.has(e.id);O(e.id,n)}),s.addEventListener("click",t=>{if(t.target===s){const t=!I.has(e.id);O(e.id,t)}})):(s.innerHTML=`\n <span class="nav-title">${e.title}</span>\n ${e.summary?`<span class="nav-summary">${e.summary}</span>`:""}\n `,s.addEventListener("click",()=>{const t=!I.has(e.id);O(e.id,t)}));const o=document.createElement("div");o.className="nav-sublist",e.subsections.forEach(e=>{if(e.url){const t=B(e,"nav-item");return void o.appendChild(t)}if(e.subsections&&e.subsections.length){const t=document.createElement("div");t.className="nav-group nav-group-nested";const n=document.createElement("button");n.type="button",n.className="nav-parent nav-parent-nested",n.dataset.section=e.id,n.title=e.summary||e.title,n.innerHTML=`<span class="nav-title">${e.title}</span>`,n.addEventListener("click",()=>{const t=!I.has(e.id);O(e.id,t)});const a=document.createElement("div");a.className="nav-sublist nav-sublist-nested",e.subsections.forEach(e=>{if(e.url){const t=B(e,"nav-item");return void a.appendChild(t)}if(e.subsections&&e.subsections.length){const t=document.createElement("div");t.className="nav-group nav-group-deep";const n=document.createElement("button");n.type="button",n.className="nav-parent nav-parent-deep",n.dataset.section=e.id,n.title=e.summary||e.title,n.innerHTML=`<span class="nav-title">${e.title}</span>`,n.addEventListener("click",()=>{const t=!I.has(e.id);O(e.id,t)});const s=document.createElement("div");s.className="nav-sublist nav-sublist-deep",e.subsections.forEach(e=>{if(e.url){const t=B(e,"nav-item");return void s.appendChild(t)}if(e.subsections&&e.subsections.length){const t=document.createElement("div");t.className="nav-group nav-group-ultra";const n=document.createElement("button");n.type="button",n.className="nav-parent nav-parent-ultra",n.dataset.section=e.id,n.title=e.summary||e.title,n.innerHTML=`<span class="nav-title">${e.title}</span>`,n.addEventListener("click",()=>{const t=!I.has(e.id);O(e.id,t)});const a=document.createElement("div");a.className="nav-sublist nav-sublist-ultra",e.subsections.forEach(e=>{if(e.url){const t=B(e,"nav-item");return void a.appendChild(t)}const t=document.createElement("button");t.type="button",t.className="nav-item nav-item-ultra"+(e.type?` nav-type-${e.type}`:""),t.dataset.section=e.id,t.title=e.summary||e.title,t.innerHTML=`\n <span class="nav-title">${e.title}</span>\n <span class="nav-summary">${e.summary||""}</span>\n `,t.addEventListener("click",()=>D(e.id,{scrollToHighlight:Boolean(P)})),a.appendChild(t),H.set(e.id,t)}),t.append(n,a),s.appendChild(t),H.set(e.id,n),S.set(e.id,{group:t,button:n,list:a});const o=I.has(e.id)&&!e.collapsed;return void O(e.id,o)}const t=document.createElement("button");t.type="button",t.className="nav-item nav-item-deep"+(e.type?` nav-type-${e.type}`:""),t.dataset.section=e.id,t.title=e.summary||e.title,t.innerHTML=`\n <span class="nav-title">${e.title}${"press-release"===e.type?'<span class="nav-type-icon" aria-label="Press Release"></span>':""}</span>\n <span class="nav-summary">${e.summary||""}</span>\n `,t.addEventListener("click",()=>D(e.id,{scrollToHighlight:Boolean(P)})),s.appendChild(t),H.set(e.id,t)}),t.append(n,s),a.appendChild(t),H.set(e.id,n),S.set(e.id,{group:t,button:n,list:s});const o=I.has(e.id)&&!e.collapsed;return void O(e.id,o)}const t=document.createElement("button");t.type="button",t.className="nav-item nav-item-nested"+(e.type?` nav-type-${e.type}`:""),t.dataset.section=e.id,t.title=e.summary||e.title,t.innerHTML=`\n <span class="nav-title">${e.title}${"press-release"===e.type?'<span class="nav-type-icon" aria-label="Press Release"></span>':""}</span>\n <span class="nav-summary">${e.summary||""}</span>\n `,t.addEventListener("click",()=>D(e.id,{scrollToHighlight:Boolean(P)})),a.appendChild(t),H.set(e.id,t)}),t.append(n,a),o.appendChild(t),H.set(e.id,n),S.set(e.id,{group:t,button:n,list:a});const s=I.has(e.id)&&!e.collapsed;return void O(e.id,s)}const t=document.createElement("button");t.type="button",t.className="nav-item"+(e.type?` nav-type-${e.type}`:""),t.dataset.section=e.id,t.title=e.summary,t.innerHTML=`\n <span class="nav-title">${e.title}${"press-release"===e.type?'<span class="nav-type-icon" aria-label="Press Release"></span>':""}</span>\n <span class="nav-summary">${e.summary}</span>\n `,t.addEventListener("click",()=>D(e.id,{scrollToHighlight:Boolean(P)})),o.appendChild(t),H.set(e.id,t)}),n.append(s,o),y.appendChild(n),H.set(e.id,s),S.set(e.id,{group:n,button:s,list:o});const i=!e.collapsed&&(I.has(e.id)||!t&&!I.size);O(e.id,i),i&&(t=!0)}else{const t=document.createElement("button");t.type="button",t.className="nav-leaf"+(e.type?` nav-type-${e.type}`:""),t.dataset.section=e.id,t.title=e.summary,t.innerHTML=`\n <span class="nav-title">${e.title}${"press-release"===e.type?'<span class="nav-type-icon" aria-label="Press Release"></span>':""}</span>\n <span class="nav-summary">${e.summary}</span>\n `,t.addEventListener("click",()=>D(e.id,{scrollToHighlight:Boolean(P)})),y.appendChild(t),H.set(e.id,t)}})}(),P&&(R=!0),window.addEventListener("hashchange",()=>{P&&(R=!0),j()}),b.textContent=(new Date).getFullYear(),j(),L&&T&&x&&N&&(L.addEventListener("click",()=>{q?W():_()}),x.addEventListener("input",()=>{const e=x.value;Z(e,!0),G(e)}),x.addEventListener("keydown",e=>{const t=M.length-1;if("ArrowDown"===e.key)e.preventDefault(),A=Math.min(t,A+1),K();else if("ArrowUp"===e.key)e.preventDefault(),A=Math.max(0,A-1),K();else if("Enter"===e.key){e.preventDefault();const t=M[A];t&&(Z(x.value,!0),D(t.id,{scrollToHighlight:!0}),W())}else"Escape"===e.key&&(e.preventDefault(),W())}),N.addEventListener("click",e=>{const t=e.target.closest("[data-section]");if(!t)return;const n=t.dataset.section;n&&(Z(x.value,!0),D(n,{scrollToHighlight:!0}),W())}),T.addEventListener("click",e=>{e.target===T&&W()}),N.addEventListener("scroll",()=>{X.loading||X.complete||N.scrollTop+N.clientHeight>=N.scrollHeight-48&&J(!1)}),window.addEventListener("keydown",e=>{const t=e.target,n=t&&("INPUT"===t.tagName||"TEXTAREA"===t.tagName||t.isContentEditable),a=e.metaKey||e.ctrlKey;"k"===e.key.toLowerCase()&&a||"/"===e.key&&!n?(e.preventDefault(),q?W():_()):"Escape"===e.key&&q&&(e.preventDefault(),W())}));let U=null,V=!1;const z=25;let X={query:"",offset:0,total:0,complete:!0,loading:!1};async function G(e){N&&(X={query:e,offset:0,total:0,complete:!1,loading:!1},M=[],!V&&e.trim()&&(V=!0,N.innerHTML='<li class="cmd-item cmd-loading">Indexing content...</li>'),clearTimeout(U),U=setTimeout(async()=>{await J(!0);const e=F();A=d(M,e),K(),V=!1},e.trim()?150:0))}async function J(t=!1){if(X.loading)return;if(!t&&X.complete)return;const n=X.query;let a;X.loading=!0;try{a=await c(e,n,{offset:X.offset,limit:z})}catch{return void(X.loading=!1)}n===X.query?(M=t?a.items:M.concat(a.items),X.offset=M.length,X.total=a.total,X.complete=a.complete||0===a.items.length,X.loading=!1,function(){if(N){if(N.innerHTML="",!M.length){const e=document.createElement("li");return e.className="cmd-item",e.setAttribute("aria-selected","false"),e.textContent="No matches.",void N.appendChild(e)}if(M.forEach(e=>{const t=document.createElement("li");t.className="cmd-item",t.dataset.section=e.id,t.setAttribute("role","option");const n=document.createElement("span");if(n.className="cmd-item-title",n.textContent=e.title,e.group){const t=document.createElement("span");t.className="cmd-item-group",t.textContent=e.group,n.prepend(t)}const a=document.createElement("span");if(a.className="cmd-item-summary",a.textContent=e.summary||"",t.append(n,a),e.searchSnippet&&e.searchSnippet!==e.summary){const n=document.createElement("span");n.className="cmd-item-snippet",n.textContent=e.searchSnippet,t.appendChild(n)}if("number"==typeof e.searchRank&&e.searchRank>0){const n=document.createElement("span");n.className="cmd-item-score",n.textContent=`Rank ${e.searchRank}`,t.appendChild(n)}N.appendChild(t)}),!X.complete&&X.total>M.length){const e=document.createElement("li");e.className="cmd-item cmd-more",e.setAttribute("aria-selected","false"),e.setAttribute("role","presentation"),e.textContent=`Showing ${M.length} of ${X.total} — scroll for more`,N.appendChild(e)}}}()):X.loading=!1}function K(){N&&Array.from(N.children).forEach((e,t)=>{const n=t===A&&M.length;e.setAttribute("aria-selected",n?"true":"false"),n&&e.scrollIntoView({block:"nearest"})})}function Y(e){const t=document.createElement("div");t.innerHTML=e,t.querySelectorAll("script").forEach(e=>e.remove()),t.querySelectorAll("button").forEach(e=>e.removeAttribute("onclick")),t.querySelectorAll("mark.hl").forEach(e=>{const t=document.createTextNode(e.textContent||"");e.replaceWith(t)});const n=t.querySelector("section");return n?n.innerHTML:t.innerHTML}function Z(e,t=!1){P=e.trim(),t&&(P?localStorage.setItem(k,P):localStorage.removeItem(k)),Q()}function Q(e=!1){g&&function(e,t,{scrollToFirst:n=!1}={}){if(!e)return;if(function(e){e&&e.querySelectorAll("mark.hl").forEach(e=>{const t=document.createTextNode(e.textContent||"");e.replaceWith(t)})}(e),!t)return;const a=t.split(/\s+/).map(e=>e.trim()).filter(Boolean);if(!a.length)return;const s=a.map(e=>e.toLowerCase()),o=new Set(["SCRIPT","STYLE","CODE","PRE"]),i=document.createTreeWalker(e,NodeFilter.SHOW_TEXT,{acceptNode(e){if(!e.nodeValue||!e.nodeValue.trim())return NodeFilter.FILTER_REJECT;const t=e.parentNode;return t&&o.has(t.tagName)?NodeFilter.FILTER_REJECT:NodeFilter.FILTER_ACCEPT}}),c=[];let r;for(;r=i.nextNode();){const e=r.nodeValue.toLowerCase();s.some(t=>e.includes(t))&&c.push(r)}const d=new RegExp(`(${a.map(l).join("|")})`,"gi");c.forEach(e=>{const t=e.nodeValue,n=[];let a=0;t.replace(d,(e,s,o)=>{o>a&&n.push(document.createTextNode(t.slice(a,o)));const i=document.createElement("mark");return i.className="hl",i.textContent=e,n.push(i),a=o+e.length,e}),a<t.length&&n.push(document.createTextNode(t.slice(a)));const s=document.createDocumentFragment();n.forEach(e=>s.appendChild(e)),e.parentNode.replaceChild(s,e)}),n&&requestAnimationFrame(()=>{const t=e.querySelector("mark.hl");t&&t.scrollIntoView({behavior:"smooth",block:"center"})})}(g,P,{scrollToFirst:e})}E&&E.addEventListener("click",function(){const t=document.createElement("div");t.className="export-options-overlay",t.innerHTML='\n <div class="export-options-modal">\n <div class="export-options-header">EXPORT OPTIONS</div>\n <div class="export-options-buttons">\n <button type="button" class="export-option-btn" data-scope="page">\n <span class="export-option-title">Current Page</span>\n <span class="export-option-desc">Export only this section</span>\n </button>\n <button type="button" class="export-option-btn" data-scope="site">\n <span class="export-option-title">Entire Site</span>\n <span class="export-option-desc">Export all documentation</span>\n </button>\n </div>\n <button type="button" class="export-cancel-btn">Cancel</button>\n </div>\n ',document.body.appendChild(t),setTimeout(()=>t.classList.add("active"),10);const n=()=>{t.classList.remove("active"),setTimeout(()=>t.remove(),200)};t.querySelector(".export-cancel-btn").addEventListener("click",n),t.addEventListener("click",e=>{e.target===t&&n()}),t.querySelectorAll(".export-option-btn").forEach(t=>{t.addEventListener("click",()=>{const a=t.dataset.scope;n(),async function(t="site"){if(!E)return;const n=E.innerHTML,a=window.open("","_blank","width=1,height=1,left=0,top=0");if(!a||a.closed||void 0===a.closed)return void confirm("Pop-ups are blocked. Please allow pop-ups for this site to export the document.\n\nWould you like to try again after enabling pop-ups?");a.close(),E.disabled=!0;const s=document.createElement("div");s.className="export-loading-overlay",s.innerHTML='\n <div class="export-loading-modal">\n <div class="export-loading-header">\n <div class="export-loading-title">COMPILING DOCUMENTATION</div>\n <div class="export-loading-subtitle">Assembling all sections into unified document</div>\n </div>\n <div class="export-loading-progress">\n <div class="export-loading-bar">\n <div class="export-loading-fill"></div>\n </div>\n <div class="export-loading-status-container">\n <div class="export-loading-status">Initializing...</div>\n </div>\n </div>\n <div class="export-loading-scanner">\n <div class="scanner-line"></div>\n </div>\n </div>\n ',document.body.appendChild(s),setTimeout(()=>s.classList.add("active"),10);const i=s.querySelector(".export-loading-fill"),l=s.querySelector(".export-loading-status");try{let n;if("page"===t){const t=F(),a=v(e).find(e=>e.id===t);n=a?[a]:[]}else n=v(e);if(0===n.length)return alert("No content available to export."),s.remove(),void(E.disabled=!1);const a=[],c=n.length;let r=0;for(const e of n){r++;const n=r/c*100;i.style.width=`${n}%`,l.textContent="page"===t?`Exporting: ${e.title}`:`Processing section ${r} of ${c}: ${e.title}`,await new Promise(e=>setTimeout(e,50));try{const t=await import(e.module),n=t.load||t.default;if("function"!=typeof n)continue;const s=Y((await n()).html||"");a.push({section:e,html:s})}catch(t){console.error("Failed to include section in export",e.id,t)}}l.textContent="Generating document...",await new Promise(e=>setTimeout(e,200));const d=u(a,o);l.textContent="Opening document viewer...",await new Promise(e=>setTimeout(e,100));const m=window.open("","_blank","width=900,height=860,scrollbars=yes,resizable=yes");if(!m)return alert("Please allow pop-ups to export the document."),void s.remove();m.document.open(),m.document.write(d),m.document.close(),m.focus(),s.classList.remove("active"),setTimeout(()=>s.remove(),300)}catch(e){console.error("Export failed",e),alert("Export failed. Check console for details."),s.remove()}finally{E.disabled=!1,E.innerHTML=n}}(a)})})}),w&&C&&(w.addEventListener("click",()=>{C.classList.contains("mobile-open")?(C.classList.remove("mobile-open"),document.body.classList.remove("menu-open"),w.setAttribute("aria-expanded","false")):(C.classList.add("mobile-open"),document.body.classList.add("menu-open"),w.setAttribute("aria-expanded","true"))}),y.addEventListener("click",e=>{if(window.innerWidth<=960){const t=e.target.closest(".nav-item, .nav-leaf, .nav-parent");t&&(t.classList.contains("nav-item")||t.classList.contains("nav-leaf"))&&(C.classList.remove("mobile-open"),document.body.classList.remove("menu-open"),w.setAttribute("aria-expanded","false"))}}),document.addEventListener("click",e=>{window.innerWidth<=960&&C.classList.contains("mobile-open")&&!C.contains(e.target)&&!w.contains(e.target)&&(C.classList.remove("mobile-open"),document.body.classList.remove("menu-open"),w.setAttribute("aria-expanded","false"))}));
package/site/index.html CHANGED
@@ -7,7 +7,7 @@
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
  <link rel="icon" type="image/png" href="/favicon.png" />
9
9
  <link rel="stylesheet" href="/styles.css" />
10
- <meta name="x-build" content="2026-06-13T16:49:25.891Z" />
10
+ <meta name="x-build" content="2026-06-15T18:27:48.659Z" />
11
11
  </head>
12
12
  <body>
13
13
  <a class="skip-link" href="#app">Skip to content</a>
@@ -0,0 +1 @@
1
+ export const FORTEMI_INDEX_SCHEMA="aiwg.fortemi.index.export.v1";export const FORTEMI_RECORD_SCHEMA="aiwg.fortemi.index.record.v1";export const FORTEMI_CHUNK_MANIFEST_SCHEMA="aiwg.fortemi.index.chunk-manifest.v1";export const FORTEMI_CHUNK_PART_SCHEMA="aiwg.fortemi.index.chunk.v1";export const DEFAULT_PART_SIZE=100;export function stableHash(e){let t=0xcbf29ce484222325n;for(let r=0;r<e.length;r+=1)t^=BigInt(255&e.charCodeAt(r)),t=1099511628211n*t&18446744073709551615n;return t.toString(16).padStart(16,"0")}export function stripHtml(e){return e?String(e).replace(/<(script|style)[^>]*>[\s\S]*?<\/\1>/gi," ").replace(/<[^>]+>/g," ").replace(/&nbsp;/gi," ").replace(/&amp;/gi,"&").replace(/&lt;/gi,"<").replace(/&gt;/gi,">").replace(/&quot;/gi,'"').replace(/&#39;/gi,"'").replace(/&mdash;/gi,"—").replace(/&hellip;/gi,"…").replace(/\s+/g," ").trim():""}export function normalizeFacetValue(e){return String(e||"").trim().toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/^-|-$/g,"")||"general"}export function recordToSectionId(e){if(!e)return null;const t=e.source?.locator||"",r=/^#\/?(.+)$/.exec(t);if(r)return r[1];const o=e.facets?.section?.[0];return o||e.id?.replace(/^docs:page:/,"")||null}export function sectionToFortemiRecord(e,t,r){const o=String(e.group||e.title||"Documentation").split(">").map(e=>e.trim()).filter(Boolean),i=e.module||"",a=i?i.replace(/^\.?\//,"").replace(/^sections\//,"sections/"):`${e.id}.md`,n=Array.from(new Set([...o.map(normalizeFacetValue),e.type?normalizeFacetValue(e.type):null].filter(Boolean))),c=t&&t.trim()?t.trim():`${e.title||""} ${e.summary||""}`.trim();return{schema_version:FORTEMI_RECORD_SCHEMA,id:`docs:page:${e.id}`,type:"docs.page",source:{path:a,repo_relative_path:a,locator:`#/${e.id}`},title:e.title||e.id,text:c,facets:{section:[e.id],group:o.length?o.map(normalizeFacetValue):["documentation"]},tags:e.type?[normalizeFacetValue(e.type)]:[],concepts:n,relationships:[],provenance:[{field:"text",source:a,path:"$.text",confidence:"source",privacy:"public"}],privacy:{classification:"public",pii:!1},updated_at:r}}export function buildFortemiIndexExport(e,t={}){const r=t.repo||"pagenary",o=new Map;for(const{section:t,text:r}of e)t&&t.id&&(o.has(t.id)||o.set(t.id,{section:t,text:r||""}));const i=Array.from(o.values()).sort((e,t)=>`docs:page:${e.section.id}`.localeCompare(`docs:page:${t.section.id}`)),a=stableHash(`${r}${i.map(({section:e,text:t})=>`${e.id}\0${e.title||""}\0${t||""}`).join("")}`),n=Number(BigInt(`0x${a.slice(0,8)}`)),c=new Date(1e3*n).toISOString(),s=i.map(({section:e,text:t})=>sectionToFortemiRecord(e,t,e.date||c));return{index:{schema_version:FORTEMI_INDEX_SCHEMA,generated_at:c,source:{repo:r,privacy:"public",build_hash:a},items:s},buildHash:a,generatedAt:c}}export function computeFacetCounts(e){const t={},r=(e,r)=>{null!=r&&(t[e]||={},t[e][r]=(t[e][r]||0)+1)};for(const t of e)r("type",t.type),r("privacy",t.privacy?.classification),(t.tags||[]).forEach(e=>r("tag",e)),(t.concepts||[]).forEach(e=>r("concept",e)),Object.entries(t.facets||{}).forEach(([e,t])=>(t||[]).forEach(t=>r(e,t)));return t}export function chunkFortemiIndex(e,t={}){const r=Math.max(1,t.partSize||100),o=t.partHref||(e=>`part-${String(e).padStart(4,"0")}.json`),i=e.items||[],a=[],n=[];let c=0,s=0;do{const e=i.slice(c,c+r),t=o(s);a.push({schema_version:FORTEMI_CHUNK_PART_SCHEMA,manifest_schema_version:FORTEMI_CHUNK_MANIFEST_SCHEMA,offset:c,items:e}),n.push({href:t,offset:c,count:e.length}),c+=e.length||r,s+=1}while(c<i.length);return{manifest:{schema_version:FORTEMI_CHUNK_MANIFEST_SCHEMA,generated_at:e.generated_at,source:e.source,total:i.length,part_size:r,facets:computeFacetCounts(i),parts:n},parts:a}}
@@ -1 +1 @@
1
- let t=null,e=null;export function escapeRegExp(t){return t.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}export function flattenManifest(t,e=""){const n=[];for(const r of t){const t=e?`${e} > ${r.title}`:r.title,o=Array.isArray(r.subsections)&&r.subsections.length>0;if(!r.module&&o||n.push({...r,group:e||r.title}),o){const e=flattenManifest(r.subsections,t);n.push(...e)}}return n}export async function buildSearchIndex(n){return t||e||(e=(async()=>{const e=flattenManifest(n),r=await Promise.all(e.map(async t=>{let e="";try{if(t.module){const n=t.module.replace("./","../"),r=await import(n);r.load&&(e=function(t){const e=document.createElement("div");return e.innerHTML=t,e.textContent||e.innerText||""}((await r.load()).html||""))}}catch(t){}return{...t,searchContent:`${t.title||""} ${t.summary||""} ${t.group||""} ${e}`.toLowerCase()}}));return t=r,r})(),e)}export function filterSections(t,e){const n=flattenManifest(t),r=e.trim().toLowerCase();return r?n.filter(t=>`${t.title||""} ${t.summary||""} ${t.group||""}`.toLowerCase().includes(r)):n}export async function searchContent(t,e){const n=await buildSearchIndex(t),r=e.trim().toLowerCase();return r?n.filter(t=>t.searchContent.includes(r)):n}export function parseSearchTerms(t){return t.split(/\s+/).map(t=>t.trim()).filter(Boolean)}export function findPreferredIndex(t,e){const n=t.findIndex(t=>t.id===e);return n>=0?n:0}
1
+ import{queryAiwgFortemiIndex as t,getAiwgFortemiFacets as e,createAiwgIndexController as n,createAiwgFetchChunkLoader as r,aiwgFortemiIndexToCommunityGraph as i,validateAiwgFortemiChunkManifest as o,validateAiwgFortemiIndexExport as a}from"../vendor/fortemi-aiwg-index.js";import{buildFortemiIndexExport as s,recordToSectionId as l}from"./fortemi-corpus.js";export{t as queryAiwgFortemiIndex,e as getAiwgFortemiFacets,n as createAiwgIndexController,r as createAiwgFetchChunkLoader,i as aiwgFortemiIndexToCommunityGraph};let u=null,c=null,f=!1,m=null,p=null,d=null,h=null;export function escapeRegExp(t){return t.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}export function flattenManifest(t,e=""){const n=[];for(const r of t){const t=e?`${e} > ${r.title}`:r.title,i=Array.isArray(r.subsections)&&r.subsections.length>0;if(!r.module&&i||n.push({...r,group:e||r.title}),i){const e=flattenManifest(r.subsections,t);n.push(...e)}}return n}function y(t,e){const n=l(t.item);return n?{...e.get(n)||{id:n,title:t.item.title,summary:""},searchRank:t.rank,searchSnippet:t.snippet,searchMatches:t.matches||[]}:null}export function filterSections(t,e){const n=flattenManifest(t),r=e.trim().toLowerCase();return r?n.filter(t=>`${t.title||""} ${t.summary||""} ${t.group||""}`.toLowerCase().includes(r)):n}export async function searchContentPage(e,i,a={}){const l=Math.max(0,a.offset||0),g=Math.max(1,a.limit||25),x=function(t){if(d&&h===t)return d;d=new Map;for(const e of flattenManifest(t))e&&e.id&&!d.has(e.id)&&d.set(e.id,e);return h=t,d}(e),w={types:["docs.page"],rank:!0,snippets:!0,includeMatches:!0,snippetLength:140,limit:g,offset:l},M=await async function(){if(u)return u;if(f)return null;if(c)return c;c=(async()=>{const t=function(){if("undefined"==typeof document||"undefined"==typeof URL)return null;try{return new URL("search-index/",document.baseURI).toString()}catch{return null}}();if(!t||"function"!=typeof fetch)return null;try{const e=await fetch(new URL("manifest.json",t).toString());if(!e.ok)return null;const i=await e.json();if(!o(i).valid)return null;const a=n();return a.loadChunkedIndex(i,r(t),{maxCachedParts:4}),u=a,a}catch{return null}})();const t=await c;return t||(f=!0),c=null,t}();if(M)try{const t=await M.queryChunked(i,{...w,onProgress:a.onProgress}),e=(t.rankedItems||[]).map(t=>y(t,x)).filter(Boolean);return{items:e,total:t.total,offset:l,limit:g,complete:l+e.length>=t.total,source:"static"}}catch{}const $=await async function(t){return m||p||(p=(async()=>{const e=flattenManifest(t),n=await Promise.all(e.map(async t=>{let e="";try{if(t.module){const n=t.module.replace("./","../"),r=await import(n);r.load&&(e=function(t){if("undefined"==typeof document)return"";const e=document.createElement("div");return e.innerHTML=t,e.textContent||e.innerText||""}((await r.load()).html||""))}}catch{}return{section:t,text:`${t.title||""} ${t.summary||""} ${t.group||""} ${e}`.trim()}})),{index:r}=s(n,{repo:"pagenary"}),i=new Map(e.map(t=>[t.id,t]));return m={index:r,byId:i},m})(),p)}(e);if(!i.trim()){const t=Array.from($.byId.values()),e=t.slice(l,l+g);return{items:e,total:t.length,offset:l,limit:g,complete:l+e.length>=t.length,source:"legacy"}}const C=t($.index,i,w),I=(C.rankedItems||[]).map(t=>y(t,$.byId)).filter(Boolean);return{items:I,total:C.total,offset:l,limit:g,complete:l+I.length>=C.total,source:"legacy"}}export async function searchContent(t,e,n={}){return e.trim()?(await searchContentPage(t,e,{limit:25,...n})).items:(await searchContentPage(t,"",{offset:0,limit:Number.MAX_SAFE_INTEGER,...n})).items}export function buildCommunityGraph(t,e={}){const n=flattenManifest(t).map(t=>({section:t,text:`${t.title||""} ${t.summary||""}`.trim()})),{index:r}=s(n,{repo:"pagenary"});return a(r).valid?i(r,e):{nodes:[],edges:[],communities:[]}}export function parseSearchTerms(t){return t.split(/\s+/).map(t=>t.trim()).filter(Boolean)}export function findPreferredIndex(t,e){const n=t.findIndex(t=>t.id===e);return n>=0?n:0}export function resetSearchState(){u=null,c=null,f=!1,m=null,p=null,d=null,h=null}
package/site/manifest.js CHANGED
@@ -3,7 +3,7 @@ export const MANIFEST = [
3
3
  "id": "welcome",
4
4
  "title": "Welcome",
5
5
  "summary": "What Pagenary is and how this dogfooded portal is built.",
6
- "module": "/sections/welcome.js"
6
+ "module": "./sections/welcome.js"
7
7
  },
8
8
  {
9
9
  "id": "getting-started",
@@ -14,7 +14,7 @@ export const MANIFEST = [
14
14
  "id": "quickstart",
15
15
  "title": "Quickstart",
16
16
  "summary": "Install, build the default bundle, and serve it locally.",
17
- "module": "/sections/quickstart.js"
17
+ "module": "./sections/quickstart.js"
18
18
  }
19
19
  ]
20
20
  },
@@ -27,19 +27,19 @@ export const MANIFEST = [
27
27
  "id": "developer-guide",
28
28
  "title": "Developer Guide",
29
29
  "summary": "Project layout, scripts, and the content authoring workflow.",
30
- "module": "/sections/developer-guide.js"
30
+ "module": "./sections/developer-guide.js"
31
31
  },
32
32
  {
33
33
  "id": "tenant-config",
34
34
  "title": "Tenant Configuration",
35
35
  "summary": "Every config.json option: branding, theming, SEO, and export.",
36
- "module": "/sections/tenant-config.js"
36
+ "module": "./sections/tenant-config.js"
37
37
  },
38
38
  {
39
39
  "id": "extending",
40
40
  "title": "Extending",
41
41
  "summary": "Add section templates, content types, and build behaviors.",
42
- "module": "/sections/extending.js"
42
+ "module": "./sections/extending.js"
43
43
  }
44
44
  ]
45
45
  },
@@ -52,25 +52,25 @@ export const MANIFEST = [
52
52
  "id": "architecture",
53
53
  "title": "Architecture",
54
54
  "summary": "The static SPA pattern, build pipeline, and tenant content model.",
55
- "module": "/sections/architecture.js"
55
+ "module": "./sections/architecture.js"
56
56
  },
57
57
  {
58
58
  "id": "api",
59
59
  "title": "API Reference",
60
60
  "summary": "Module-level documentation for the publisher internals.",
61
- "module": "/sections/api.js"
61
+ "module": "./sections/api.js"
62
62
  },
63
63
  {
64
64
  "id": "deployment",
65
65
  "title": "Deployment",
66
66
  "summary": "Hosting the static output and multi-tenant domain routing.",
67
- "module": "/sections/deployment.js"
67
+ "module": "./sections/deployment.js"
68
68
  },
69
69
  {
70
70
  "id": "seo-strategy",
71
71
  "title": "SEO Strategy",
72
72
  "summary": "Metadata, hash-routing considerations, and discoverability.",
73
- "module": "/sections/seo-strategy.js"
73
+ "module": "./sections/seo-strategy.js"
74
74
  }
75
75
  ]
76
76
  }
@@ -27,7 +27,7 @@
27
27
  "headline": "API Reference",
28
28
  "description": "Module-level documentation for the publisher internals.",
29
29
  "url": "https://docs.pagenary.com/pages/api.html",
30
- "dateModified": "2026-06-13",
30
+ "dateModified": "2026-06-15",
31
31
  "mainEntityOfPage": {
32
32
  "@type": "WebPage",
33
33
  "@id": "https://docs.pagenary.com/pages/api.html"
@@ -164,8 +164,19 @@ await loadSection(section);</code></pre>
164
164
  });</code></pre>
165
165
  <hr>
166
166
  <h2 id="library-modules">Library Modules</h2>
167
- <h3 id="libsearchjs-full-text-search">lib/search.js - Full-Text Search</h3>
168
- <p>Search functionality with lazy content indexing.</p>
167
+ <h3 id="libsearchjs-fortemi-backed-search">lib/search.js - Fortemi-backed search</h3>
168
+ <p>Search runs on the <strong>real, vendored `@fortemi/core` static-index engine</strong></p>
169
+ <p>(`src/vendor/fortemi-aiwg-index.js`). At build time, `scripts/build-tenants.js`</p>
170
+ <p>emits a deterministic <strong>chunked</strong> index per tenant under `dist/&lt;tenant&gt;/search-index/`</p>
171
+ <p>(`manifest.json` + `part-NNNN.json`, the `aiwg.fortemi.index.*.v1` contract). At</p>
172
+ <p>runtime the adapter loads that index through an index controller +</p>
173
+ <p>fetch chunk-loader: parts are fetched lazily and cached (<strong>precache</strong>), results</p>
174
+ <p>are ranked with snippets, and pages are returned by offset for <strong>infinite scroll</strong>.</p>
175
+ <p>If the static index is missing or invalid, the adapter falls back to an</p>
176
+ <p>in-browser index built from section modules — same ranking engine, same result</p>
177
+ <p>shape. See `.aiwg/architecture/adr/ADR-015-fortemi-core-search-adapter.md`.</p>
178
+ <p>Build-time and fallback share the deterministic corpus builder in</p>
179
+ <p>`lib/fortemi-corpus.js`.</p>
169
180
  <h4 id="functions-3">Functions</h4>
170
181
  <p><strong>`escapeRegExp(value: string): string`</strong></p>
171
182
  <p>Escape special regex characters.</p>
@@ -174,19 +185,34 @@ await loadSection(section);</code></pre>
174
185
  <p>Flatten nested manifest into searchable sections.</p>
175
186
  <pre><code class="language-javascript">const flat = flattenManifest(MANIFEST);
176
187
  // Returns all navigable sections with group info</code></pre>
177
- <p><strong>`buildSearchIndex(manifest: SectionEntry[]): Promise&lt;IndexedSection[]&gt;`</strong></p>
178
- <p>Build search index by loading all section modules. Cached after first call.</p>
179
- <pre><code class="language-javascript">const index = await buildSearchIndex(MANIFEST);
180
- // Each entry has searchContent: lowercase text for matching</code></pre>
181
188
  <p><strong>`filterSections(manifest: SectionEntry[], query: string): FlatSection[]`</strong></p>
182
189
  <p>Synchronous title/summary search (no content).</p>
183
190
  <pre><code class="language-javascript">const results = filterSections(MANIFEST, &#39;setup&#39;);</code></pre>
184
- <p><strong>`searchContent(manifest: SectionEntry[], query: string): Promise&lt;IndexedSection[]&gt;`</strong></p>
185
- <p>Full-text search across all content.</p>
186
- <pre><code class="language-javascript">const results = await searchContent(MANIFEST, &#39;authentication&#39;);
187
- // Searches titles, summaries, and full content</code></pre>
191
+ <p><strong>`searchContentPage(manifest, query, options?): Promise&lt;SearchPage&gt;`</strong></p>
192
+ <p>Paged full-text search the primary entry point, used for infinite scroll.</p>
193
+ <pre><code class="language-javascript">const page = await searchContentPage(MANIFEST, &#39;auth&#39;, { offset: 0, limit: 25 });
194
+ // page = { items, total, offset, limit, complete, source: &#39;static&#39; | &#39;legacy&#39; }
195
+ // items: section objects with searchRank, searchSnippet, searchMatches</code></pre>
196
+ <p><strong>`searchContent(manifest, query, options?): Promise&lt;Section[]&gt;`</strong></p>
197
+ <p>First-page convenience wrapper (back-compatible array). Empty query returns all</p>
198
+ <p>sections.</p>
199
+ <pre><code class="language-javascript">const results = await searchContent(MANIFEST, &#39;authentication&#39;);</code></pre>
200
+ <p><strong>`buildCommunityGraph(manifest, options?): { nodes, edges, communities }`</strong></p>
201
+ <p>Project the corpus into a Fortemi community graph (relationships/facets) — the</p>
202
+ <p>&quot;graph&quot; capability, no full-text required.</p>
203
+ <pre><code class="language-javascript">const graph = buildCommunityGraph(MANIFEST);</code></pre>
188
204
  <p><strong>`findPreferredIndex(entries: Section[], currentId: string): number`</strong></p>
189
205
  <p>Find index of current section in filtered results.</p>
206
+ <p>Re-exported from the vendored engine for advanced use:</p>
207
+ <p>`queryAiwgFortemiIndex`, `getAiwgFortemiFacets`, `createAiwgIndexController`,</p>
208
+ <p>`createAiwgFetchChunkLoader`, `aiwgFortemiIndexToCommunityGraph`.</p>
209
+ <hr>
210
+ <h3 id="libfortemi-corpusjs-deterministic-corpus-builder">lib/fortemi-corpus.js - Deterministic corpus builder</h3>
211
+ <p>Pure, DOM-free, `Date.now()`-free helpers shared by the build-time generator and</p>
212
+ <p>the runtime fallback: `buildFortemiIndexExport(entries, { repo })` (records sorted</p>
213
+ <p>by id, deduped, content-hashed `generated_at` + `source.build_hash`),</p>
214
+ <p>`chunkFortemiIndex(index, { partSize })`, `sectionToFortemiRecord`, `stripHtml`,</p>
215
+ <p>`recordToSectionId`, `stableHash`.</p>
190
216
  <hr>
191
217
  <h3 id="librouterjs-hash-routing">lib/router.js - Hash Routing</h3>
192
218
  <p>URL hash parsing and resolution.</p>
@@ -291,8 +317,19 @@ interface FlatSection extends SectionEntry {
291
317
  group?: string; // Parent group title
292
318
  }
293
319
 
294
- interface IndexedSection extends FlatSection {
295
- searchContent: string; // Lowercase text for searching
320
+ interface SearchResultSection extends FlatSection {
321
+ searchRank?: number; // Relevance rank from the Fortemi engine
322
+ searchSnippet?: string; // Highlighted-context snippet
323
+ searchMatches?: { field: string; value: string }[];
324
+ }
325
+
326
+ interface SearchPage {
327
+ items: SearchResultSection[];
328
+ total: number;
329
+ offset: number;
330
+ limit: number;
331
+ complete: boolean; // true when no further pages remain
332
+ source: &#39;static&#39; | &#39;legacy&#39;;
296
333
  }
297
334
 
298
335
  interface SiteConfig {