@pagenary/publisher 2026.6.3 → 2026.6.5

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
@@ -31,7 +31,7 @@ npx pagenary serve # serve on http://localhost:5173
31
31
 
32
32
  ## What It Is
33
33
 
34
- The publisher takes a catalog of shared section templates plus per-tenant content and configuration and produces a self-contained documentation bundle for each tenant. Each bundle is a static single-page app — hash-based routing (`#/page-id`), no server-side rendering, no runtime dependencies — that you build once and host anywhere that serves files. Tenants share the template catalog but keep isolated content, branding, navigation, and domains, so one repository can publish a dozen distinct sites.
34
+ The publisher takes a catalog of shared section templates plus per-tenant content and configuration and produces a self-contained documentation bundle for each tenant. Each bundle is a static single-page app — hash-based routing (`#/page-id`), no server-side rendering, no runtime dependencies — that you build once and host anywhere that serves files. A per-tenant `<base>` resolves asset and module URLs to the tenant root, so the same bundle serves correctly at a domain root *or* under a subpath mount. Tenants share the template catalog but keep isolated content, branding, navigation, and domains — each with ranked client-side search and SEO-ready output — so one repository can publish a dozen distinct sites.
35
35
 
36
36
  ---
37
37
 
@@ -99,10 +99,12 @@ inspect, build, or run the AIWG project from `~/dev/aiwg`.
99
99
  - **Typography** — IBM Plex Sans/Mono defaults, customizable
100
100
 
101
101
  ### SEO (built in)
102
+ - **Metadata-driven titles** — the shell `<title>` derives from the default page's metadata title (`"<page title> · <brand>"`), and each section sets its own title/description at runtime; the generic brand is only a fallback
102
103
  - **Absolute URLs** — declare a `domain` (or `seo.siteUrl`) and the sitemap, canonical, `og:url`, and `robots` URLs become fully-qualified
103
104
  - **Static snapshots** — crawler-friendly `/pages/<id>.html` for every section, self-canonical (the SPA hash route isn't crawlable)
104
105
  - **`sitemap.xml`, `robots.txt`, `llms.txt`** — generated automatically
105
106
  - **JSON-LD + Open Graph** — `TechArticle`/`BreadcrumbList` per page, optional Organization data, and `og:image`/`twitter:image` via `seo.ogImage`
107
+ - **Subpath-safe assets** — a per-tenant `<base>` keeps stylesheet/script/snapshot URLs valid whether served at a domain root or a subpath
106
108
 
107
109
  ### Export & Sharing
108
110
  - **Export Options** — Current Page or Entire Site
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pagenary/publisher",
3
- "version": "2026.6.3",
3
+ "version": "2026.6.5",
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,8 +3,8 @@
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>
7
- <meta name="description" content="Pagenary developer documentation building, configuring, deploying, and extending the multi-tenant documentation publisher, published with Pagenary itself." />
6
+ <title>Welcome · Pagenary Docs</title>
7
+ <meta name="description" content="Turn a git repo of Markdown into a fast, searchable, SEO-ready docs site you host yourself for next to nothing — with the features you&#39;d expect from a paid docs platform. Pagenary&#39;s own developer docs, built and published with Pagenary." />
8
8
  <script>
9
9
  // Resolve all asset/module URLs against the tenant root in both deploy
10
10
  // modes: domain-per-tenant at root (production, with SPA fallback) and a
@@ -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:46:01.346Z" />
25
25
  </head>
26
26
  <body>
27
27
  <a class="skip-link" href="#app">Skip to content</a>
package/site/llms.txt CHANGED
@@ -1,6 +1,6 @@
1
1
  # Pagenary Docs
2
2
 
3
- > Pagenary developer documentation building, configuring, deploying, and extending the multi-tenant documentation publisher, published with Pagenary itself.
3
+ > Turn a git repo of Markdown into a fast, searchable, SEO-ready docs site you host yourself for next to nothing — with the features you'd expect from a paid docs platform. Pagenary's own developer docs, built and published with Pagenary.
4
4
 
5
5
  - [Welcome](https://docs.pagenary.com/pages/welcome.html): What Pagenary is and how this dogfooded portal is built.
6
6
 
@@ -116,7 +116,9 @@ npx pagenary --help # confirm the CLI is available</code></pre>
116
116
  <h2 id="key-features">Key Features</h2>
117
117
  <p>Pagenary includes several powerful features out of the box:</p>
118
118
  <ul>
119
- <li><strong>Command Palette</strong> - Press `Ctrl+K` (or `Cmd+K` on Mac) to quickly navigate, search, and export</li>
119
+ <li><strong>Command Palette</strong> - Press `Ctrl+K` (or `Cmd+K` on Mac) for ranked full-text search (with snippets + infinite scroll), navigation, and export</li>
120
+ <li><strong>SEO-first output</strong> - metadata-driven titles, crawlable `/pages/` snapshots, sitemap/robots/llms.txt, JSON-LD, and Open Graph, generated at build time</li>
121
+ <li><strong>Deploy anywhere</strong> - the same bundle serves at a domain root or under a subpath; any static host, CDN, or the bundled Caddy</li>
120
122
  <li><strong>Mermaid Diagrams</strong> - Embed flowcharts, sequence diagrams, and more using Mermaid syntax</li>
121
123
  <li><strong>Smart External Links</strong> - External links automatically open in new tabs with security headers</li>
122
124
  </ul>
@@ -99,7 +99,7 @@
99
99
  <p>time by `scripts/lib/seo-generator.js` and configured via the tenant</p>
100
100
  <p><a href="#tenant-config#seo-seo">`seo` block</a>.</p>
101
101
  <h2 id="what-the-build-generates">What the build generates</h2>
102
- <table><thead><tr><th style="text-align: left">Artifact</th><th style="text-align: left">Purpose</th></tr></thead><tbody><tr><td style="text-align: left">`sitemap.xml`</td><td style="text-align: left">Absolute `&lt;loc&gt;` for the home page and every section&#39;s static snapshot</td></tr><tr><td style="text-align: left">`robots.txt`</td><td style="text-align: left">Allows `/` and `/pages/`, points at the sitemap</td></tr><tr><td style="text-align: left">`llms.txt`</td><td style="text-align: left">LLM-friendly site index (<a href="https://llmstxt.org/" target="_blank" rel="noopener noreferrer">llmstxt.org</a>)</td></tr><tr><td style="text-align: left">`/pages/&lt;id&gt;.html`</td><td style="text-align: left">Per-section static snapshots with full metadata + JSON-LD, for crawlers</td></tr><tr><td style="text-align: left">JSON-LD</td><td style="text-align: left">`TechArticle` + `BreadcrumbList` per page; `WebSite` + optional `Organization` site-wide</td></tr><tr><td style="text-align: left">Runtime meta</td><td style="text-align: left">`src/seo.js` keeps `&lt;title&gt;`, description, canonical, OG, and Twitter tags in sync as the SPA navigates</td></tr></tbody></table>
102
+ <table><thead><tr><th style="text-align: left">Artifact</th><th style="text-align: left">Purpose</th></tr></thead><tbody><tr><td style="text-align: left">`sitemap.xml`</td><td style="text-align: left">Absolute `&lt;loc&gt;` for the home page and every section&#39;s static snapshot</td></tr><tr><td style="text-align: left">`robots.txt`</td><td style="text-align: left">Allows `/` and `/pages/`, points at the sitemap</td></tr><tr><td style="text-align: left">`llms.txt`</td><td style="text-align: left">LLM-friendly site index (<a href="https://llmstxt.org/" target="_blank" rel="noopener noreferrer">llmstxt.org</a>)</td></tr><tr><td style="text-align: left">`/pages/&lt;id&gt;.html`</td><td style="text-align: left">Per-section static snapshots with full metadata + JSON-LD, for crawlers</td></tr><tr><td style="text-align: left">JSON-LD</td><td style="text-align: left">`TechArticle` + `BreadcrumbList` per page; `WebSite` + optional `Organization` site-wide</td></tr><tr><td style="text-align: left">Shell `&lt;title&gt;`</td><td style="text-align: left">The build sets the static shell title from the <strong>default page&#39;s metadata title</strong> (`&quot;&lt;page title&gt; · &lt;brand&gt;&quot;`), so the crawler-visible root URL is specific, not generic. The brand alone is only a fallback</td></tr><tr><td style="text-align: left">Runtime meta</td><td style="text-align: left">`src/seo.js` keeps `&lt;title&gt;`, description, canonical, OG, and Twitter tags in sync as the SPA navigates</td></tr></tbody></table>
103
103
  <h2 id="make-urls-absolute">Make URLs absolute</h2>
104
104
  <p>Declare a `domain` (or `seo.siteUrl`) on the tenant. This is what turns the</p>
105
105
  <p>sitemap `&lt;loc&gt;`, canonical, `og:url`, and `robots` `Sitemap:` into fully-qualified</p>
@@ -88,26 +88,28 @@
88
88
  <section class="section doc markdown">
89
89
  <div class="doc-content">
90
90
  <h1 id="where-documentation-takes-shape">Where documentation takes shape</h1>
91
- <p>Pagenary is a multi-tenant documentation publishing platform that turns one</p>
92
- <p>shared set of templates into many branded, tenant-specific static sites. Zero</p>
93
- <p>runtime dependencies, hash-based routing, and a Git-aware build pipeline make</p>
94
- <p>it suited to white-label documentation portals one source of truth, any</p>
95
- <p>number of published sites.</p>
91
+ <p>Pagenary turns a folder of Markdown in a git repo into a fast, searchable,</p>
92
+ <p>SEO-ready documentation site you host yourself for next to nothing. Write your</p>
93
+ <p>docs, run one tool, and deploy the static output to any free static host</p>
94
+ <p>(GitHub/Gitea Pages, Netlify, Cloudflare Pages, S3, a CDN, or your own box). No</p>
95
+ <p>server, no database, no monthly SaaS bill — just the things you&#39;d expect from a</p>
96
+ <p>paid docs platform: command-palette search with ranking, theming and branding,</p>
97
+ <p>Mermaid diagrams, syntax highlighting, SEO, and one-click export.</p>
96
98
  <p><strong>This portal is built by Pagenary, from Pagenary&#39;s own documentation.</strong> Every</p>
97
- <p>page you see here is the same publisher pipeline applied to the developer docs</p>
98
- <p>in the repository, served as a static single-page app.</p>
99
+ <p>page you see here is the same publisher pipeline applied to the developer docs in</p>
100
+ <p>the repository, served as a static single-page app.</p>
99
101
  <h2 id="start-here">Start here</h2>
100
102
  <ul>
101
- <li><strong><a href="#quickstart">Quickstart</a></strong> — install, build the default bundle, and serve it locally.</li>
102
- <li><strong><a href="#architecture">Architecture</a></strong> — the static SPA pattern, build pipeline, and tenant content model.</li>
103
- <li><strong><a href="#tenant-config">Tenant Configuration</a></strong> — every `config.json` option for branding, theming, and export.</li>
103
+ <li><strong><a href="#quickstart">Quickstart</a></strong> — install, build your first site, and serve it locally.</li>
104
+ <li><strong><a href="#architecture">Architecture</a></strong> — the static SPA pattern, build pipeline, and content model.</li>
105
+ <li><strong><a href="#tenant-config">Tenant Configuration</a></strong> — every `config.json` option for branding, theming, SEO, and export.</li>
104
106
  </ul>
105
- <h2 id="how-a-tenant-works">How a tenant works</h2>
106
- <p>A tenant is a thin layer — content, a `config.json`, and a `manifest.json` —</p>
107
- <p>over the shared template catalog. Branding, theming, and navigation are **data,</p>
108
- <p>not code**: a tenant changes its look through configuration, never by forking</p>
109
- <p>the generator. The build produces a self-contained bundle under</p>
110
- <p>`dist/&lt;tenant-id&gt;/` that you can host anywhere that serves files.</p>
107
+ <h2 id="scale-to-many-sites-when-you-need-to">Scale to many sites when you need to</h2>
108
+ <p>One site is just content, a `config.json`, and a `manifest.json`. Need more than</p>
109
+ <p>one? The same tool publishes many sites from a shared template catalog — branding,</p>
110
+ <p>theming, and navigation are <strong>data, not code</strong>, so standing up another branded</p>
111
+ <p>site is cheap, and a Git-aware build rebuilds only what changed. Pagenary scales</p>
112
+ <p>from a weekend project to a multi-product portal without changing tools.</p>
111
113
  </div>
112
114
  </section>
113
115
  </div>
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:46:01.946Z
3
3
 
4
4
  User-agent: *
5
5
  Allow: /
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "schema_version": "aiwg.fortemi.index.chunk-manifest.v1",
3
- "generated_at": "1977-09-23T15:35:58.000Z",
3
+ "generated_at": "1989-11-27T18:58:05.000Z",
4
4
  "source": {
5
5
  "repo": "pagenary",
6
6
  "privacy": "public",
7
- "build_hash": "0e89445e615e2d69"
7
+ "build_hash": "257183bd1fb7ac71"
8
8
  },
9
9
  "total": 9,
10
10
  "part_size": 100,
@@ -40,7 +40,7 @@
40
40
  "classification": "public",
41
41
  "pii": false
42
42
  },
43
- "updated_at": "1977-09-23T15:35:58.000Z"
43
+ "updated_at": "1989-11-27T18:58:05.000Z"
44
44
  },
45
45
  {
46
46
  "schema_version": "aiwg.fortemi.index.record.v1",
@@ -79,7 +79,7 @@
79
79
  "classification": "public",
80
80
  "pii": false
81
81
  },
82
- "updated_at": "1977-09-23T15:35:58.000Z"
82
+ "updated_at": "1989-11-27T18:58:05.000Z"
83
83
  },
84
84
  {
85
85
  "schema_version": "aiwg.fortemi.index.record.v1",
@@ -118,7 +118,7 @@
118
118
  "classification": "public",
119
119
  "pii": false
120
120
  },
121
- "updated_at": "1977-09-23T15:35:58.000Z"
121
+ "updated_at": "1989-11-27T18:58:05.000Z"
122
122
  },
123
123
  {
124
124
  "schema_version": "aiwg.fortemi.index.record.v1",
@@ -157,7 +157,7 @@
157
157
  "classification": "public",
158
158
  "pii": false
159
159
  },
160
- "updated_at": "1977-09-23T15:35:58.000Z"
160
+ "updated_at": "1989-11-27T18:58:05.000Z"
161
161
  },
162
162
  {
163
163
  "schema_version": "aiwg.fortemi.index.record.v1",
@@ -196,7 +196,7 @@
196
196
  "classification": "public",
197
197
  "pii": false
198
198
  },
199
- "updated_at": "1977-09-23T15:35:58.000Z"
199
+ "updated_at": "1989-11-27T18:58:05.000Z"
200
200
  },
201
201
  {
202
202
  "schema_version": "aiwg.fortemi.index.record.v1",
@@ -208,7 +208,7 @@
208
208
  "locator": "#/quickstart"
209
209
  },
210
210
  "title": "Quickstart",
211
- "text": "Quickstart Install, build the default bundle, and serve it locally. Getting Started Quick Start Guide Create your first Pagenary documentation site in 10 minutes. For a more guided, zero-assumptions walkthrough, see Getting Started . Prerequisites Node.js ≥ 16 (20+ recommended) Step 1: Install Pagenary Use the published package — no clone required: npm install --save-dev @pagenary/publisher npx pagenary --help # confirm the CLI is available Building Pagenary from source instead? Clone the repo and work from the workspace (for contributors / modifying the generator): ```bash git clone https://github.com/jmagly/pagenary.git cd pagenary && npm run bootstrap npm run publisher:build && npm run publisher:serve ``` Key Features Pagenary includes several powerful features out of the box: Command Palette - Press `Ctrl+K` (or `Cmd+K` on Mac) to quickly navigate, search, and export Mermaid Diagrams - Embed flowcharts, sequence diagrams, and more using Mermaid syntax Smart External Links - External links automatically open in new tabs with security headers Step 2: Create Your Tenant Directory Create a new directory for your documentation (can be anywhere on your system): mkdir ~/my-docs cd ~/my-docs Create the basic structure: mkdir content Step 3: Add Branding Configuration Create `config.json`: { \"title\": \"My Product Documentation\", \"description\": \"Complete guide to using My Product\", \"brandMark\": \"MY\", \"brandSub\": \"PRODUCT\", \"tagline\": \"Documentation that works\", \"copyright\": \"My Company\", \"accentColor\": \"#3B82F6\", \"surfaceColor\": \"#F8FAFC\" } Step 4: Create Your First Content Create `content/welcome.md`: # Welcome to My Product This is your documentation home page. ## Getting Started Here's what you need to know to get started with My Product. ### Installation \\`\\`\\`bash npm install my-product \\`\\`\\` ### Quick Example \\`\\`\\`javascript import { MyProduct } from 'my-product'; const app = new MyProduct(); app.start(); \\`\\`\\` ## Features - **Fast** - Built for speed - **Simple** - Easy to use - **Powerful** - Full-featured ## Architecture \\`\\`\\`mermaid graph TD A[User] --> B[Frontend] B --> C[API] C --> D[Database] \\`\\`\\` Create `content/installation.md`: # Installation Guide ## Requirements - Node.js 18 or higher - npm or yarn ## Install via npm \\`\\`\\`bash npm install my-product \\`\\`\\` ## Install via yarn \\`\\`\\`bash yarn add my-product \\`\\`\\` ## Verify Installation \\`\\`\\`bash npx my-product --version \\`\\`\\` Step 5: Create Navigation Manifest Create `manifest.json`: [ { \"id\": \"welcome\", \"title\": \"Welcome\", \"summary\": \"Introduction to My Product\", \"file\": \"welcome.md\" }, { \"id\": \"installation\", \"title\": \"Installation\", \"summary\": \"How to install My Product\", \"file\": \"installation.md\" } ] Step 6: Register Your Tenant Create (or edit) a `tenants.json` at your project root and add your tenant. The registry is an array; `source` is an object (`local` or `git`): { \"tenants\": [ { \"id\": \"my-docs\", \"source\": { \"type\": \"local\", \"path\": \"./my-docs\" }, \"strictLinks\": true } ] } Building from source? Edit `apps/publisher/tenants.json` in the cloned repo instead. Step 7: Build and Preview # Build your tenant npx pagenary build:tenants my-docs # Start the server npx pagenary serve # Visit http://localhost:5173/my-docs/ From source, the equivalents are `npm run build:tenants my-docs` and `npm run serve`. You should see your documentation with your branding applied. Step 8: Set Up Local Domain (Optional) For a more realistic preview with custom domains: 1. Edit `/etc/hosts` (Linux/Mac) or `C:\\Windows\\System32\\drivers\\etc\\hosts` (Windows): 127.0.0.1 my-docs.local 2. Start the Caddy server: npm run caddy:up 3. Visit http://my-docs.local Next Steps Add More Content Create additional `.md`, `.html`, or `.js` files in `content/`: content/ ├── welcome.md ├── installation.md ├── guides/ │ ├── _manifest.json │ ├── getting-started.md │ └── advanced.md └── api/ ├── _manifest.json └── reference.md Organize with Section Manifests Create `content/guides/_manifest.json`: { \"title\": \"Guides\", \"sections\": [ { \"id\": \"getting-started\", \"title\": \"Getting Started\", \"file\": \"getting-started.md\" }, { \"id\": \"advanced\", \"title\": \"Advanced Usage\", \"file\": \"advanced.md\" } ] } Add Rich Content Tables: Feature Status Search Ready Export Ready Diagrams: sequenceDiagram User->>API: Request API->>DB: Query DB-->>API: Results API-->>User: Response Customize Theme Adjust colors in `config.json`: Color Purpose Example `accentColor` Links, buttons, highlights `#3B82F6` (blue) `surfaceColor` Page background `#F8FAFC` (off-white) Deploy Your built site is in `dist/my-docs/`. Deploy it anywhere that serves static files: Netlify/Vercel : Point to `dist/my-docs/` S3/GCS : Upload the folder Docker : Use the included Caddy setup Troubleshooting Content not appearing? 1. Check that `manifest.json` references the correct file paths 2. Verify files exist in `content/` 3. Run `npx pagenary build:tenants my-docs` and check for errors Styles not applied? 1. Verify `config.json` is valid JSON 2. Check color values are valid hex codes (e.g., `#3B82F6`) Search not working? The command palette loads a prebuilt static index (`dist/<tenant>/search-index/`) on first open — wait a moment for \"Indexing content…\" to clear. If that directory is missing (e.g., an older build), search falls back to indexing in the browser on first use. Rebuild with `npm run build:tenants` to regenerate the static index. Resources Tenant Configuration - All config options Architecture - How it works API Reference - Module documentation Deployment - Hosting guide",
211
+ "text": "Quickstart Install, build the default bundle, and serve it locally. Getting Started Quick Start Guide Create your first Pagenary documentation site in 10 minutes. For a more guided, zero-assumptions walkthrough, see Getting Started . Prerequisites Node.js ≥ 16 (20+ recommended) Step 1: Install Pagenary Use the published package — no clone required: npm install --save-dev @pagenary/publisher npx pagenary --help # confirm the CLI is available Building Pagenary from source instead? Clone the repo and work from the workspace (for contributors / modifying the generator): ```bash git clone https://github.com/jmagly/pagenary.git cd pagenary && npm run bootstrap npm run publisher:build && npm run publisher:serve ``` Key Features Pagenary includes several powerful features out of the box: Command Palette - Press `Ctrl+K` (or `Cmd+K` on Mac) for ranked full-text search (with snippets + infinite scroll), navigation, and export SEO-first output - metadata-driven titles, crawlable `/pages/` snapshots, sitemap/robots/llms.txt, JSON-LD, and Open Graph, generated at build time Deploy anywhere - the same bundle serves at a domain root or under a subpath; any static host, CDN, or the bundled Caddy Mermaid Diagrams - Embed flowcharts, sequence diagrams, and more using Mermaid syntax Smart External Links - External links automatically open in new tabs with security headers Step 2: Create Your Tenant Directory Create a new directory for your documentation (can be anywhere on your system): mkdir ~/my-docs cd ~/my-docs Create the basic structure: mkdir content Step 3: Add Branding Configuration Create `config.json`: { \"title\": \"My Product Documentation\", \"description\": \"Complete guide to using My Product\", \"brandMark\": \"MY\", \"brandSub\": \"PRODUCT\", \"tagline\": \"Documentation that works\", \"copyright\": \"My Company\", \"accentColor\": \"#3B82F6\", \"surfaceColor\": \"#F8FAFC\" } Step 4: Create Your First Content Create `content/welcome.md`: # Welcome to My Product This is your documentation home page. ## Getting Started Here's what you need to know to get started with My Product. ### Installation \\`\\`\\`bash npm install my-product \\`\\`\\` ### Quick Example \\`\\`\\`javascript import { MyProduct } from 'my-product'; const app = new MyProduct(); app.start(); \\`\\`\\` ## Features - **Fast** - Built for speed - **Simple** - Easy to use - **Powerful** - Full-featured ## Architecture \\`\\`\\`mermaid graph TD A[User] --> B[Frontend] B --> C[API] C --> D[Database] \\`\\`\\` Create `content/installation.md`: # Installation Guide ## Requirements - Node.js 18 or higher - npm or yarn ## Install via npm \\`\\`\\`bash npm install my-product \\`\\`\\` ## Install via yarn \\`\\`\\`bash yarn add my-product \\`\\`\\` ## Verify Installation \\`\\`\\`bash npx my-product --version \\`\\`\\` Step 5: Create Navigation Manifest Create `manifest.json`: [ { \"id\": \"welcome\", \"title\": \"Welcome\", \"summary\": \"Introduction to My Product\", \"file\": \"welcome.md\" }, { \"id\": \"installation\", \"title\": \"Installation\", \"summary\": \"How to install My Product\", \"file\": \"installation.md\" } ] Step 6: Register Your Tenant Create (or edit) a `tenants.json` at your project root and add your tenant. The registry is an array; `source` is an object (`local` or `git`): { \"tenants\": [ { \"id\": \"my-docs\", \"source\": { \"type\": \"local\", \"path\": \"./my-docs\" }, \"strictLinks\": true } ] } Building from source? Edit `apps/publisher/tenants.json` in the cloned repo instead. Step 7: Build and Preview # Build your tenant npx pagenary build:tenants my-docs # Start the server npx pagenary serve # Visit http://localhost:5173/my-docs/ From source, the equivalents are `npm run build:tenants my-docs` and `npm run serve`. You should see your documentation with your branding applied. Step 8: Set Up Local Domain (Optional) For a more realistic preview with custom domains: 1. Edit `/etc/hosts` (Linux/Mac) or `C:\\Windows\\System32\\drivers\\etc\\hosts` (Windows): 127.0.0.1 my-docs.local 2. Start the Caddy server: npm run caddy:up 3. Visit http://my-docs.local Next Steps Add More Content Create additional `.md`, `.html`, or `.js` files in `content/`: content/ ├── welcome.md ├── installation.md ├── guides/ │ ├── _manifest.json │ ├── getting-started.md │ └── advanced.md └── api/ ├── _manifest.json └── reference.md Organize with Section Manifests Create `content/guides/_manifest.json`: { \"title\": \"Guides\", \"sections\": [ { \"id\": \"getting-started\", \"title\": \"Getting Started\", \"file\": \"getting-started.md\" }, { \"id\": \"advanced\", \"title\": \"Advanced Usage\", \"file\": \"advanced.md\" } ] } Add Rich Content Tables: Feature Status Search Ready Export Ready Diagrams: sequenceDiagram User->>API: Request API->>DB: Query DB-->>API: Results API-->>User: Response Customize Theme Adjust colors in `config.json`: Color Purpose Example `accentColor` Links, buttons, highlights `#3B82F6` (blue) `surfaceColor` Page background `#F8FAFC` (off-white) Deploy Your built site is in `dist/my-docs/`. Deploy it anywhere that serves static files: Netlify/Vercel : Point to `dist/my-docs/` S3/GCS : Upload the folder Docker : Use the included Caddy setup Troubleshooting Content not appearing? 1. Check that `manifest.json` references the correct file paths 2. Verify files exist in `content/` 3. Run `npx pagenary build:tenants my-docs` and check for errors Styles not applied? 1. Verify `config.json` is valid JSON 2. Check color values are valid hex codes (e.g., `#3B82F6`) Search not working? The command palette loads a prebuilt static index (`dist/<tenant>/search-index/`) on first open — wait a moment for \"Indexing content…\" to clear. If that directory is missing (e.g., an older build), search falls back to indexing in the browser on first use. Rebuild with `npm run build:tenants` to regenerate the static index. Resources Tenant Configuration - All config options Architecture - How it works API Reference - Module documentation Deployment - Hosting guide",
212
212
  "facets": {
213
213
  "section": [
214
214
  "quickstart"
@@ -235,7 +235,7 @@
235
235
  "classification": "public",
236
236
  "pii": false
237
237
  },
238
- "updated_at": "1977-09-23T15:35:58.000Z"
238
+ "updated_at": "1989-11-27T18:58:05.000Z"
239
239
  },
240
240
  {
241
241
  "schema_version": "aiwg.fortemi.index.record.v1",
@@ -247,7 +247,7 @@
247
247
  "locator": "#/seo-strategy"
248
248
  },
249
249
  "title": "SEO Strategy",
250
- "text": "SEO Strategy Metadata, hash-routing considerations, and discoverability. Reference SEO Strategy Pagenary publishes a hash-routed single-page app, so it generates **crawler-facing static artifacts** alongside the SPA. These are produced automatically at build time by `scripts/lib/seo-generator.js` and configured via the tenant `seo` block . What the build generates Artifact Purpose `sitemap.xml` Absolute `<loc>` for the home page and every section's static snapshot `robots.txt` Allows `/` and `/pages/`, points at the sitemap `llms.txt` LLM-friendly site index ( llmstxt.org ) `/pages/<id>.html` Per-section static snapshots with full metadata + JSON-LD, for crawlers JSON-LD `TechArticle` + `BreadcrumbList` per page; `WebSite` + optional `Organization` site-wide Runtime meta `src/seo.js` keeps `<title>`, description, canonical, OG, and Twitter tags in sync as the SPA navigates Make URLs absolute Declare a `domain` (or `seo.siteUrl`) on the tenant. This is what turns the sitemap `<loc>`, canonical, `og:url`, and `robots` `Sitemap:` into fully-qualified URLs. The sitemap protocol requires absolute URLs, so a tenant with neither set emits a non-compliant sitemap — the build prints a warning when that happens. Precedence: `seo.siteUrl` → `domain` (https-prefixed) → relative (warned). Canonical strategy Static snapshots and the runtime SPA canonicalize to the crawlable static URL (`/pages/<id>.html`), not the SPA `#hash` route. Search engines ignore URL fragments, so hash canonicals (`/#section`) would collapse every page onto the homepage. The `#hash` route is still used for the human-facing \"interactive version\" link and the JavaScript redirect on the static page. Social cards Set `seo.ogImage` (absolute or site-relative) to emit `og:image` / `twitter:image` and upgrade `twitter:card` to `summary_large_image`. Individual pages can override it with an `ogImage` field on the manifest entry. Authoring practices Keep manifest `summary` values concise — they power the meta description, search results, link previews, and the export document. Use human-readable, hyphenated, lowercase section `id`s — they become both the hash route and the static page filename (`/` becomes `--`). Gate broken links in CI with `strictLinks: true` (see Tenant Configuration ).",
250
+ "text": "SEO Strategy Metadata, hash-routing considerations, and discoverability. Reference SEO Strategy Pagenary publishes a hash-routed single-page app, so it generates **crawler-facing static artifacts** alongside the SPA. These are produced automatically at build time by `scripts/lib/seo-generator.js` and configured via the tenant `seo` block . What the build generates Artifact Purpose `sitemap.xml` Absolute `<loc>` for the home page and every section's static snapshot `robots.txt` Allows `/` and `/pages/`, points at the sitemap `llms.txt` LLM-friendly site index ( llmstxt.org ) `/pages/<id>.html` Per-section static snapshots with full metadata + JSON-LD, for crawlers JSON-LD `TechArticle` + `BreadcrumbList` per page; `WebSite` + optional `Organization` site-wide Shell `<title>` The build sets the static shell title from the default page's metadata title (`\"<page title> · <brand>\"`), so the crawler-visible root URL is specific, not generic. The brand alone is only a fallback Runtime meta `src/seo.js` keeps `<title>`, description, canonical, OG, and Twitter tags in sync as the SPA navigates Make URLs absolute Declare a `domain` (or `seo.siteUrl`) on the tenant. This is what turns the sitemap `<loc>`, canonical, `og:url`, and `robots` `Sitemap:` into fully-qualified URLs. The sitemap protocol requires absolute URLs, so a tenant with neither set emits a non-compliant sitemap — the build prints a warning when that happens. Precedence: `seo.siteUrl` → `domain` (https-prefixed) → relative (warned). Canonical strategy Static snapshots and the runtime SPA canonicalize to the crawlable static URL (`/pages/<id>.html`), not the SPA `#hash` route. Search engines ignore URL fragments, so hash canonicals (`/#section`) would collapse every page onto the homepage. The `#hash` route is still used for the human-facing \"interactive version\" link and the JavaScript redirect on the static page. Social cards Set `seo.ogImage` (absolute or site-relative) to emit `og:image` / `twitter:image` and upgrade `twitter:card` to `summary_large_image`. Individual pages can override it with an `ogImage` field on the manifest entry. Authoring practices Keep manifest `summary` values concise — they power the meta description, search results, link previews, and the export document. Use human-readable, hyphenated, lowercase section `id`s — they become both the hash route and the static page filename (`/` becomes `--`). Gate broken links in CI with `strictLinks: true` (see Tenant Configuration ).",
251
251
  "facets": {
252
252
  "section": [
253
253
  "seo-strategy"
@@ -274,7 +274,7 @@
274
274
  "classification": "public",
275
275
  "pii": false
276
276
  },
277
- "updated_at": "1977-09-23T15:35:58.000Z"
277
+ "updated_at": "1989-11-27T18:58:05.000Z"
278
278
  },
279
279
  {
280
280
  "schema_version": "aiwg.fortemi.index.record.v1",
@@ -313,7 +313,7 @@
313
313
  "classification": "public",
314
314
  "pii": false
315
315
  },
316
- "updated_at": "1977-09-23T15:35:58.000Z"
316
+ "updated_at": "1989-11-27T18:58:05.000Z"
317
317
  },
318
318
  {
319
319
  "schema_version": "aiwg.fortemi.index.record.v1",
@@ -325,7 +325,7 @@
325
325
  "locator": "#/welcome"
326
326
  },
327
327
  "title": "Welcome",
328
- "text": "Welcome What Pagenary is and how this dogfooded portal is built. Welcome Where documentation takes shape Pagenary is a multi-tenant documentation publishing platform that turns one shared set of templates into many branded, tenant-specific static sites. Zero runtime dependencies, hash-based routing, and a Git-aware build pipeline make it suited to white-label documentation portalsone source of truth, any number of published sites. This portal is built by Pagenary, from Pagenary's own documentation. Every page you see here is the same publisher pipeline applied to the developer docs in the repository, served as a static single-page app. Start here Quickstart — install, build the default bundle, and serve it locally. Architecture — the static SPA pattern, build pipeline, and tenant content model. Tenant Configuration — every `config.json` option for branding, theming, and export. How a tenant works A tenant is a thin layer content, a `config.json`, and a `manifest.json` over the shared template catalog. Branding, theming, and navigation are **data, not code**: a tenant changes its look through configuration, never by forking the generator. The build produces a self-contained bundle under `dist/<tenant-id>/` that you can host anywhere that serves files.",
328
+ "text": "Welcome What Pagenary is and how this dogfooded portal is built. Welcome Where documentation takes shape Pagenary turns a folder of Markdown in a git repo into a fast, searchable, SEO-ready documentation site you host yourself for next to nothing. Write your docs, run one tool, and deploy the static output to any free static host (GitHub/Gitea Pages, Netlify, Cloudflare Pages, S3, a CDN, or your own box). No server, no database, no monthly SaaS bill just the things you'd expect from a paid docs platform: command-palette search with ranking, theming and branding, Mermaid diagrams, syntax highlighting, SEO, and one-click export. This portal is built by Pagenary, from Pagenary's own documentation. Every page you see here is the same publisher pipeline applied to the developer docs in the repository, served as a static single-page app. Start here Quickstart — install, build your first site, and serve it locally. Architecture — the static SPA pattern, build pipeline, and content model. Tenant Configuration — every `config.json` option for branding, theming, SEO, and export. Scale to many sites when you need to One site is just content, a `config.json`, and a `manifest.json`. Need more than one? The same tool publishes many sites from a shared template catalog — branding, theming, and navigation are data, not code , so standing up another branded site is cheap, and a Git-aware build rebuilds only what changed. Pagenary scales from a weekend project to a multi-product portal without changing tools.",
329
329
  "facets": {
330
330
  "section": [
331
331
  "welcome"
@@ -352,7 +352,7 @@
352
352
  "classification": "public",
353
353
  "pii": false
354
354
  },
355
- "updated_at": "1977-09-23T15:35:58.000Z"
355
+ "updated_at": "1989-11-27T18:58:05.000Z"
356
356
  }
357
357
  ]
358
358
  }
@@ -1,3 +1,3 @@
1
1
  export async function load() {
2
- return { html: "<section class=\"section doc markdown\">\n <div class=\"doc-content\">\n<h1 id=\"quick-start-guide\">Quick Start Guide</h1>\n<p>Create your first Pagenary documentation site in 10 minutes. For a more guided,</p>\n<p>zero-assumptions walkthrough, see <a href=\"#getting-started\">Getting Started</a>.</p>\n<h2 id=\"prerequisites\">Prerequisites</h2>\n<ul>\n<li>Node.js ≥ 16 (20+ recommended)</li>\n</ul>\n<h2 id=\"step-1-install-pagenary\">Step 1: Install Pagenary</h2>\n<p>Use the published package — no clone required:</p>\n<pre><code class=\"language-bash\">npm install --save-dev @pagenary/publisher\nnpx pagenary --help # confirm the CLI is available</code></pre>\n<blockquote>\n<p><strong>Building Pagenary from source instead?</strong> Clone the repo and work from the</p>\n<p>workspace (for contributors / modifying the generator):</p>\n<p>```bash</p>\n<p>git clone https://github.com/jmagly/pagenary.git</p>\n<p>cd pagenary &amp;&amp; npm run bootstrap</p>\n<p>npm run publisher:build &amp;&amp; npm run publisher:serve</p>\n<p>```</p>\n</blockquote>\n<h2 id=\"key-features\">Key Features</h2>\n<p>Pagenary includes several powerful features out of the box:</p>\n<ul>\n<li><strong>Command Palette</strong> - Press `Ctrl+K` (or `Cmd+K` on Mac) to quickly navigate, search, and export</li>\n<li><strong>Mermaid Diagrams</strong> - Embed flowcharts, sequence diagrams, and more using Mermaid syntax</li>\n<li><strong>Smart External Links</strong> - External links automatically open in new tabs with security headers</li>\n</ul>\n<h2 id=\"step-2-create-your-tenant-directory\">Step 2: Create Your Tenant Directory</h2>\n<p>Create a new directory for your documentation (can be anywhere on your system):</p>\n<pre><code class=\"language-bash\">mkdir ~/my-docs\ncd ~/my-docs</code></pre>\n<p>Create the basic structure:</p>\n<pre><code class=\"language-bash\">mkdir content</code></pre>\n<h2 id=\"step-3-add-branding-configuration\">Step 3: Add Branding Configuration</h2>\n<p>Create `config.json`:</p>\n<pre><code class=\"language-json\">{\n &quot;title&quot;: &quot;My Product Documentation&quot;,\n &quot;description&quot;: &quot;Complete guide to using My Product&quot;,\n &quot;brandMark&quot;: &quot;MY&quot;,\n &quot;brandSub&quot;: &quot;PRODUCT&quot;,\n &quot;tagline&quot;: &quot;Documentation that works&quot;,\n &quot;copyright&quot;: &quot;My Company&quot;,\n &quot;accentColor&quot;: &quot;#3B82F6&quot;,\n &quot;surfaceColor&quot;: &quot;#F8FAFC&quot;\n}</code></pre>\n<h2 id=\"step-4-create-your-first-content\">Step 4: Create Your First Content</h2>\n<p>Create `content/welcome.md`:</p>\n<pre><code class=\"language-markdown\"># Welcome to My Product\n\nThis is your documentation home page.\n\n## Getting Started\n\nHere&#39;s what you need to know to get started with My Product.\n\n### Installation\n\n\\`\\`\\`bash\nnpm install my-product\n\\`\\`\\`\n\n### Quick Example\n\n\\`\\`\\`javascript\nimport { MyProduct } from &#39;my-product&#39;;\n\nconst app = new MyProduct();\napp.start();\n\\`\\`\\`\n\n## Features\n\n- **Fast** - Built for speed\n- **Simple** - Easy to use\n- **Powerful** - Full-featured\n\n## Architecture\n\n\\`\\`\\`mermaid\ngraph TD\n A[User] --&gt; B[Frontend]\n B --&gt; C[API]\n C --&gt; D[Database]\n\\`\\`\\`</code></pre>\n<p>Create `content/installation.md`:</p>\n<pre><code class=\"language-markdown\"># Installation Guide\n\n## Requirements\n\n- Node.js 18 or higher\n- npm or yarn\n\n## Install via npm\n\n\\`\\`\\`bash\nnpm install my-product\n\\`\\`\\`\n\n## Install via yarn\n\n\\`\\`\\`bash\nyarn add my-product\n\\`\\`\\`\n\n## Verify Installation\n\n\\`\\`\\`bash\nnpx my-product --version\n\\`\\`\\`</code></pre>\n<h2 id=\"step-5-create-navigation-manifest\">Step 5: Create Navigation Manifest</h2>\n<p>Create `manifest.json`:</p>\n<pre><code class=\"language-json\">[\n {\n &quot;id&quot;: &quot;welcome&quot;,\n &quot;title&quot;: &quot;Welcome&quot;,\n &quot;summary&quot;: &quot;Introduction to My Product&quot;,\n &quot;file&quot;: &quot;welcome.md&quot;\n },\n {\n &quot;id&quot;: &quot;installation&quot;,\n &quot;title&quot;: &quot;Installation&quot;,\n &quot;summary&quot;: &quot;How to install My Product&quot;,\n &quot;file&quot;: &quot;installation.md&quot;\n }\n]</code></pre>\n<h2 id=\"step-6-register-your-tenant\">Step 6: Register Your Tenant</h2>\n<p>Create (or edit) a `tenants.json` at your project root and add your tenant. The</p>\n<p>registry is an array; `source` is an object (`local` or `git`):</p>\n<pre><code class=\"language-json\">{\n &quot;tenants&quot;: [\n {\n &quot;id&quot;: &quot;my-docs&quot;,\n &quot;source&quot;: { &quot;type&quot;: &quot;local&quot;, &quot;path&quot;: &quot;./my-docs&quot; },\n &quot;strictLinks&quot;: true\n }\n ]\n}</code></pre>\n<blockquote>\n<p>Building from source? Edit `apps/publisher/tenants.json` in the cloned repo instead.</p>\n</blockquote>\n<h2 id=\"step-7-build-and-preview\">Step 7: Build and Preview</h2>\n<pre><code class=\"language-bash\"># Build your tenant\nnpx pagenary build:tenants my-docs\n\n# Start the server\nnpx pagenary serve\n\n# Visit http://localhost:5173/my-docs/</code></pre>\n<blockquote>\n<p>From source, the equivalents are `npm run build:tenants my-docs` and `npm run serve`.</p>\n</blockquote>\n<p>You should see your documentation with your branding applied.</p>\n<h2 id=\"step-8-set-up-local-domain-optional\">Step 8: Set Up Local Domain (Optional)</h2>\n<p>For a more realistic preview with custom domains:</p>\n<p>1. Edit `/etc/hosts` (Linux/Mac) or `C:\\Windows\\System32\\drivers\\etc\\hosts` (Windows):</p>\n<pre><code>127.0.0.1 my-docs.local</code></pre>\n<p>2. Start the Caddy server:</p>\n<pre><code class=\"language-bash\">npm run caddy:up</code></pre>\n<p>3. Visit http://my-docs.local</p>\n<h2 id=\"next-steps\">Next Steps</h2>\n<h3 id=\"add-more-content\">Add More Content</h3>\n<p>Create additional `.md`, `.html`, or `.js` files in `content/`:</p>\n<pre><code>content/\n├── welcome.md\n├── installation.md\n├── guides/\n│ ├── _manifest.json\n│ ├── getting-started.md\n│ └── advanced.md\n└── api/\n ├── _manifest.json\n └── reference.md</code></pre>\n<h3 id=\"organize-with-section-manifests\">Organize with Section Manifests</h3>\n<p>Create `content/guides/_manifest.json`:</p>\n<pre><code class=\"language-json\">{\n &quot;title&quot;: &quot;Guides&quot;,\n &quot;sections&quot;: [\n { &quot;id&quot;: &quot;getting-started&quot;, &quot;title&quot;: &quot;Getting Started&quot;, &quot;file&quot;: &quot;getting-started.md&quot; },\n { &quot;id&quot;: &quot;advanced&quot;, &quot;title&quot;: &quot;Advanced Usage&quot;, &quot;file&quot;: &quot;advanced.md&quot; }\n ]\n}</code></pre>\n<h3 id=\"add-rich-content\">Add Rich Content</h3>\n<p><strong>Tables:</strong></p>\n<div class=\"html-block\"><table class=\"spec-table\">\n <thead>\n <tr><th>Feature</th><th>Status</th></tr>\n </thead>\n <tbody>\n <tr><td>Search</td><td>Ready</td></tr>\n <tr><td>Export</td><td>Ready</td></tr>\n </tbody>\n</table></div>\n<p><strong>Diagrams:</strong></p>\n<pre><code class=\"language-mermaid\">sequenceDiagram\n User-&gt;&gt;API: Request\n API-&gt;&gt;DB: Query\n DB--&gt;&gt;API: Results\n API--&gt;&gt;User: Response</code></pre>\n<h3 id=\"customize-theme\">Customize Theme</h3>\n<p>Adjust colors in `config.json`:</p>\n<table><thead><tr><th style=\"text-align: left\">Color</th><th style=\"text-align: left\">Purpose</th><th style=\"text-align: left\">Example</th></tr></thead><tbody><tr><td style=\"text-align: left\">`accentColor`</td><td style=\"text-align: left\">Links, buttons, highlights</td><td style=\"text-align: left\">`#3B82F6` (blue)</td></tr><tr><td style=\"text-align: left\">`surfaceColor`</td><td style=\"text-align: left\">Page background</td><td style=\"text-align: left\">`#F8FAFC` (off-white)</td></tr></tbody></table>\n<h3 id=\"deploy\">Deploy</h3>\n<p>Your built site is in `dist/my-docs/`. Deploy it anywhere that serves static files:</p>\n<ul>\n<li><strong>Netlify/Vercel</strong>: Point to `dist/my-docs/`</li>\n<li><strong>S3/GCS</strong>: Upload the folder</li>\n<li><strong>Docker</strong>: Use the included Caddy setup</li>\n</ul>\n<h2 id=\"troubleshooting\">Troubleshooting</h2>\n<h3 id=\"content-not-appearing\">Content not appearing?</h3>\n<p>1. Check that `manifest.json` references the correct file paths</p>\n<p>2. Verify files exist in `content/`</p>\n<p>3. Run `npx pagenary build:tenants my-docs` and check for errors</p>\n<h3 id=\"styles-not-applied\">Styles not applied?</h3>\n<p>1. Verify `config.json` is valid JSON</p>\n<p>2. Check color values are valid hex codes (e.g., `#3B82F6`)</p>\n<h3 id=\"search-not-working\">Search not working?</h3>\n<p>The command palette loads a prebuilt static index (`dist/&lt;tenant&gt;/search-index/`)</p>\n<p>on first open — wait a moment for &quot;Indexing content…&quot; to clear. If that directory</p>\n<p>is missing (e.g., an older build), search falls back to indexing in the browser on</p>\n<p>first use. Rebuild with `npm run build:tenants` to regenerate the static index.</p>\n<h2 id=\"resources\">Resources</h2>\n<ul>\n<li><a href=\"#tenant-config\">Tenant Configuration</a> - All config options</li>\n<li><a href=\"#architecture\">Architecture</a> - How it works</li>\n<li><a href=\"#api\">API Reference</a> - Module documentation</li>\n<li><a href=\"#deployment\">Deployment</a> - Hosting guide</li>\n</ul>\n </div>\n</section>" };
2
+ return { html: "<section class=\"section doc markdown\">\n <div class=\"doc-content\">\n<h1 id=\"quick-start-guide\">Quick Start Guide</h1>\n<p>Create your first Pagenary documentation site in 10 minutes. For a more guided,</p>\n<p>zero-assumptions walkthrough, see <a href=\"#getting-started\">Getting Started</a>.</p>\n<h2 id=\"prerequisites\">Prerequisites</h2>\n<ul>\n<li>Node.js ≥ 16 (20+ recommended)</li>\n</ul>\n<h2 id=\"step-1-install-pagenary\">Step 1: Install Pagenary</h2>\n<p>Use the published package — no clone required:</p>\n<pre><code class=\"language-bash\">npm install --save-dev @pagenary/publisher\nnpx pagenary --help # confirm the CLI is available</code></pre>\n<blockquote>\n<p><strong>Building Pagenary from source instead?</strong> Clone the repo and work from the</p>\n<p>workspace (for contributors / modifying the generator):</p>\n<p>```bash</p>\n<p>git clone https://github.com/jmagly/pagenary.git</p>\n<p>cd pagenary &amp;&amp; npm run bootstrap</p>\n<p>npm run publisher:build &amp;&amp; npm run publisher:serve</p>\n<p>```</p>\n</blockquote>\n<h2 id=\"key-features\">Key Features</h2>\n<p>Pagenary includes several powerful features out of the box:</p>\n<ul>\n<li><strong>Command Palette</strong> - Press `Ctrl+K` (or `Cmd+K` on Mac) for ranked full-text search (with snippets + infinite scroll), navigation, and export</li>\n<li><strong>SEO-first output</strong> - metadata-driven titles, crawlable `/pages/` snapshots, sitemap/robots/llms.txt, JSON-LD, and Open Graph, generated at build time</li>\n<li><strong>Deploy anywhere</strong> - the same bundle serves at a domain root or under a subpath; any static host, CDN, or the bundled Caddy</li>\n<li><strong>Mermaid Diagrams</strong> - Embed flowcharts, sequence diagrams, and more using Mermaid syntax</li>\n<li><strong>Smart External Links</strong> - External links automatically open in new tabs with security headers</li>\n</ul>\n<h2 id=\"step-2-create-your-tenant-directory\">Step 2: Create Your Tenant Directory</h2>\n<p>Create a new directory for your documentation (can be anywhere on your system):</p>\n<pre><code class=\"language-bash\">mkdir ~/my-docs\ncd ~/my-docs</code></pre>\n<p>Create the basic structure:</p>\n<pre><code class=\"language-bash\">mkdir content</code></pre>\n<h2 id=\"step-3-add-branding-configuration\">Step 3: Add Branding Configuration</h2>\n<p>Create `config.json`:</p>\n<pre><code class=\"language-json\">{\n &quot;title&quot;: &quot;My Product Documentation&quot;,\n &quot;description&quot;: &quot;Complete guide to using My Product&quot;,\n &quot;brandMark&quot;: &quot;MY&quot;,\n &quot;brandSub&quot;: &quot;PRODUCT&quot;,\n &quot;tagline&quot;: &quot;Documentation that works&quot;,\n &quot;copyright&quot;: &quot;My Company&quot;,\n &quot;accentColor&quot;: &quot;#3B82F6&quot;,\n &quot;surfaceColor&quot;: &quot;#F8FAFC&quot;\n}</code></pre>\n<h2 id=\"step-4-create-your-first-content\">Step 4: Create Your First Content</h2>\n<p>Create `content/welcome.md`:</p>\n<pre><code class=\"language-markdown\"># Welcome to My Product\n\nThis is your documentation home page.\n\n## Getting Started\n\nHere&#39;s what you need to know to get started with My Product.\n\n### Installation\n\n\\`\\`\\`bash\nnpm install my-product\n\\`\\`\\`\n\n### Quick Example\n\n\\`\\`\\`javascript\nimport { MyProduct } from &#39;my-product&#39;;\n\nconst app = new MyProduct();\napp.start();\n\\`\\`\\`\n\n## Features\n\n- **Fast** - Built for speed\n- **Simple** - Easy to use\n- **Powerful** - Full-featured\n\n## Architecture\n\n\\`\\`\\`mermaid\ngraph TD\n A[User] --&gt; B[Frontend]\n B --&gt; C[API]\n C --&gt; D[Database]\n\\`\\`\\`</code></pre>\n<p>Create `content/installation.md`:</p>\n<pre><code class=\"language-markdown\"># Installation Guide\n\n## Requirements\n\n- Node.js 18 or higher\n- npm or yarn\n\n## Install via npm\n\n\\`\\`\\`bash\nnpm install my-product\n\\`\\`\\`\n\n## Install via yarn\n\n\\`\\`\\`bash\nyarn add my-product\n\\`\\`\\`\n\n## Verify Installation\n\n\\`\\`\\`bash\nnpx my-product --version\n\\`\\`\\`</code></pre>\n<h2 id=\"step-5-create-navigation-manifest\">Step 5: Create Navigation Manifest</h2>\n<p>Create `manifest.json`:</p>\n<pre><code class=\"language-json\">[\n {\n &quot;id&quot;: &quot;welcome&quot;,\n &quot;title&quot;: &quot;Welcome&quot;,\n &quot;summary&quot;: &quot;Introduction to My Product&quot;,\n &quot;file&quot;: &quot;welcome.md&quot;\n },\n {\n &quot;id&quot;: &quot;installation&quot;,\n &quot;title&quot;: &quot;Installation&quot;,\n &quot;summary&quot;: &quot;How to install My Product&quot;,\n &quot;file&quot;: &quot;installation.md&quot;\n }\n]</code></pre>\n<h2 id=\"step-6-register-your-tenant\">Step 6: Register Your Tenant</h2>\n<p>Create (or edit) a `tenants.json` at your project root and add your tenant. The</p>\n<p>registry is an array; `source` is an object (`local` or `git`):</p>\n<pre><code class=\"language-json\">{\n &quot;tenants&quot;: [\n {\n &quot;id&quot;: &quot;my-docs&quot;,\n &quot;source&quot;: { &quot;type&quot;: &quot;local&quot;, &quot;path&quot;: &quot;./my-docs&quot; },\n &quot;strictLinks&quot;: true\n }\n ]\n}</code></pre>\n<blockquote>\n<p>Building from source? Edit `apps/publisher/tenants.json` in the cloned repo instead.</p>\n</blockquote>\n<h2 id=\"step-7-build-and-preview\">Step 7: Build and Preview</h2>\n<pre><code class=\"language-bash\"># Build your tenant\nnpx pagenary build:tenants my-docs\n\n# Start the server\nnpx pagenary serve\n\n# Visit http://localhost:5173/my-docs/</code></pre>\n<blockquote>\n<p>From source, the equivalents are `npm run build:tenants my-docs` and `npm run serve`.</p>\n</blockquote>\n<p>You should see your documentation with your branding applied.</p>\n<h2 id=\"step-8-set-up-local-domain-optional\">Step 8: Set Up Local Domain (Optional)</h2>\n<p>For a more realistic preview with custom domains:</p>\n<p>1. Edit `/etc/hosts` (Linux/Mac) or `C:\\Windows\\System32\\drivers\\etc\\hosts` (Windows):</p>\n<pre><code>127.0.0.1 my-docs.local</code></pre>\n<p>2. Start the Caddy server:</p>\n<pre><code class=\"language-bash\">npm run caddy:up</code></pre>\n<p>3. Visit http://my-docs.local</p>\n<h2 id=\"next-steps\">Next Steps</h2>\n<h3 id=\"add-more-content\">Add More Content</h3>\n<p>Create additional `.md`, `.html`, or `.js` files in `content/`:</p>\n<pre><code>content/\n├── welcome.md\n├── installation.md\n├── guides/\n│ ├── _manifest.json\n│ ├── getting-started.md\n│ └── advanced.md\n└── api/\n ├── _manifest.json\n └── reference.md</code></pre>\n<h3 id=\"organize-with-section-manifests\">Organize with Section Manifests</h3>\n<p>Create `content/guides/_manifest.json`:</p>\n<pre><code class=\"language-json\">{\n &quot;title&quot;: &quot;Guides&quot;,\n &quot;sections&quot;: [\n { &quot;id&quot;: &quot;getting-started&quot;, &quot;title&quot;: &quot;Getting Started&quot;, &quot;file&quot;: &quot;getting-started.md&quot; },\n { &quot;id&quot;: &quot;advanced&quot;, &quot;title&quot;: &quot;Advanced Usage&quot;, &quot;file&quot;: &quot;advanced.md&quot; }\n ]\n}</code></pre>\n<h3 id=\"add-rich-content\">Add Rich Content</h3>\n<p><strong>Tables:</strong></p>\n<div class=\"html-block\"><table class=\"spec-table\">\n <thead>\n <tr><th>Feature</th><th>Status</th></tr>\n </thead>\n <tbody>\n <tr><td>Search</td><td>Ready</td></tr>\n <tr><td>Export</td><td>Ready</td></tr>\n </tbody>\n</table></div>\n<p><strong>Diagrams:</strong></p>\n<pre><code class=\"language-mermaid\">sequenceDiagram\n User-&gt;&gt;API: Request\n API-&gt;&gt;DB: Query\n DB--&gt;&gt;API: Results\n API--&gt;&gt;User: Response</code></pre>\n<h3 id=\"customize-theme\">Customize Theme</h3>\n<p>Adjust colors in `config.json`:</p>\n<table><thead><tr><th style=\"text-align: left\">Color</th><th style=\"text-align: left\">Purpose</th><th style=\"text-align: left\">Example</th></tr></thead><tbody><tr><td style=\"text-align: left\">`accentColor`</td><td style=\"text-align: left\">Links, buttons, highlights</td><td style=\"text-align: left\">`#3B82F6` (blue)</td></tr><tr><td style=\"text-align: left\">`surfaceColor`</td><td style=\"text-align: left\">Page background</td><td style=\"text-align: left\">`#F8FAFC` (off-white)</td></tr></tbody></table>\n<h3 id=\"deploy\">Deploy</h3>\n<p>Your built site is in `dist/my-docs/`. Deploy it anywhere that serves static files:</p>\n<ul>\n<li><strong>Netlify/Vercel</strong>: Point to `dist/my-docs/`</li>\n<li><strong>S3/GCS</strong>: Upload the folder</li>\n<li><strong>Docker</strong>: Use the included Caddy setup</li>\n</ul>\n<h2 id=\"troubleshooting\">Troubleshooting</h2>\n<h3 id=\"content-not-appearing\">Content not appearing?</h3>\n<p>1. Check that `manifest.json` references the correct file paths</p>\n<p>2. Verify files exist in `content/`</p>\n<p>3. Run `npx pagenary build:tenants my-docs` and check for errors</p>\n<h3 id=\"styles-not-applied\">Styles not applied?</h3>\n<p>1. Verify `config.json` is valid JSON</p>\n<p>2. Check color values are valid hex codes (e.g., `#3B82F6`)</p>\n<h3 id=\"search-not-working\">Search not working?</h3>\n<p>The command palette loads a prebuilt static index (`dist/&lt;tenant&gt;/search-index/`)</p>\n<p>on first open — wait a moment for &quot;Indexing content…&quot; to clear. If that directory</p>\n<p>is missing (e.g., an older build), search falls back to indexing in the browser on</p>\n<p>first use. Rebuild with `npm run build:tenants` to regenerate the static index.</p>\n<h2 id=\"resources\">Resources</h2>\n<ul>\n<li><a href=\"#tenant-config\">Tenant Configuration</a> - All config options</li>\n<li><a href=\"#architecture\">Architecture</a> - How it works</li>\n<li><a href=\"#api\">API Reference</a> - Module documentation</li>\n<li><a href=\"#deployment\">Deployment</a> - Hosting guide</li>\n</ul>\n </div>\n</section>" };
3
3
  }
@@ -1,3 +1,3 @@
1
1
  export async function load() {
2
- return { html: "<section class=\"section doc markdown\">\n <div class=\"doc-content\">\n<h1 id=\"seo-strategy\">SEO Strategy</h1>\n<p>Pagenary publishes a hash-routed single-page app, so it generates **crawler-facing</p>\n<p>static artifacts** alongside the SPA. These are produced automatically at build</p>\n<p>time by `scripts/lib/seo-generator.js` and configured via the tenant</p>\n<p><a href=\"#tenant-config#seo-seo\">`seo` block</a>.</p>\n<h2 id=\"what-the-build-generates\">What the build generates</h2>\n<table><thead><tr><th style=\"text-align: left\">Artifact</th><th style=\"text-align: left\">Purpose</th></tr></thead><tbody><tr><td style=\"text-align: left\">`sitemap.xml`</td><td style=\"text-align: left\">Absolute `&lt;loc&gt;` for the home page and every section&#39;s static snapshot</td></tr><tr><td style=\"text-align: left\">`robots.txt`</td><td style=\"text-align: left\">Allows `/` and `/pages/`, points at the sitemap</td></tr><tr><td style=\"text-align: left\">`llms.txt`</td><td style=\"text-align: left\">LLM-friendly site index (<a href=\"https://llmstxt.org/\" target=\"_blank\" rel=\"noopener noreferrer\">llmstxt.org</a>)</td></tr><tr><td style=\"text-align: left\">`/pages/&lt;id&gt;.html`</td><td style=\"text-align: left\">Per-section static snapshots with full metadata + JSON-LD, for crawlers</td></tr><tr><td style=\"text-align: left\">JSON-LD</td><td style=\"text-align: left\">`TechArticle` + `BreadcrumbList` per page; `WebSite` + optional `Organization` site-wide</td></tr><tr><td style=\"text-align: left\">Runtime meta</td><td style=\"text-align: left\">`src/seo.js` keeps `&lt;title&gt;`, description, canonical, OG, and Twitter tags in sync as the SPA navigates</td></tr></tbody></table>\n<h2 id=\"make-urls-absolute\">Make URLs absolute</h2>\n<p>Declare a `domain` (or `seo.siteUrl`) on the tenant. This is what turns the</p>\n<p>sitemap `&lt;loc&gt;`, canonical, `og:url`, and `robots` `Sitemap:` into fully-qualified</p>\n<p>URLs. The <a href=\"https://www.sitemaps.org/protocol.html\" target=\"_blank\" rel=\"noopener noreferrer\">sitemap protocol</a> requires</p>\n<p>absolute URLs, so a tenant with neither set emits a non-compliant sitemap — the</p>\n<p>build prints a warning when that happens.</p>\n<p>Precedence: `seo.siteUrl` → `domain` (https-prefixed) → relative (warned).</p>\n<h2 id=\"canonical-strategy\">Canonical strategy</h2>\n<p>Static snapshots and the runtime SPA canonicalize to the <strong>crawlable static URL</strong></p>\n<p>(`/pages/&lt;id&gt;.html`), not the SPA `#hash` route. Search engines ignore URL</p>\n<p>fragments, so hash canonicals (`/#section`) would collapse every page onto the</p>\n<p>homepage. The `#hash` route is still used for the human-facing &quot;interactive</p>\n<p>version&quot; link and the JavaScript redirect on the static page.</p>\n<h2 id=\"social-cards\">Social cards</h2>\n<p>Set `seo.ogImage` (absolute or site-relative) to emit `og:image` /</p>\n<p>`twitter:image` and upgrade `twitter:card` to `summary_large_image`. Individual</p>\n<p>pages can override it with an `ogImage` field on the manifest entry.</p>\n<h2 id=\"authoring-practices\">Authoring practices</h2>\n<ul>\n<li>Keep manifest `summary` values concise — they power the meta description, search</li>\n</ul>\n<p>results, link previews, and the export document.</p>\n<ul>\n<li>Use human-readable, hyphenated, lowercase section `id`s — they become both the</li>\n</ul>\n<p>hash route and the static page filename (`/` becomes `--`).</p>\n<ul>\n<li>Gate broken links in CI with `strictLinks: true` (see</li>\n</ul>\n<p><a href=\"#tenant-config\">Tenant Configuration</a>).</p>\n </div>\n</section>" };
2
+ return { html: "<section class=\"section doc markdown\">\n <div class=\"doc-content\">\n<h1 id=\"seo-strategy\">SEO Strategy</h1>\n<p>Pagenary publishes a hash-routed single-page app, so it generates **crawler-facing</p>\n<p>static artifacts** alongside the SPA. These are produced automatically at build</p>\n<p>time by `scripts/lib/seo-generator.js` and configured via the tenant</p>\n<p><a href=\"#tenant-config#seo-seo\">`seo` block</a>.</p>\n<h2 id=\"what-the-build-generates\">What the build generates</h2>\n<table><thead><tr><th style=\"text-align: left\">Artifact</th><th style=\"text-align: left\">Purpose</th></tr></thead><tbody><tr><td style=\"text-align: left\">`sitemap.xml`</td><td style=\"text-align: left\">Absolute `&lt;loc&gt;` for the home page and every section&#39;s static snapshot</td></tr><tr><td style=\"text-align: left\">`robots.txt`</td><td style=\"text-align: left\">Allows `/` and `/pages/`, points at the sitemap</td></tr><tr><td style=\"text-align: left\">`llms.txt`</td><td style=\"text-align: left\">LLM-friendly site index (<a href=\"https://llmstxt.org/\" target=\"_blank\" rel=\"noopener noreferrer\">llmstxt.org</a>)</td></tr><tr><td style=\"text-align: left\">`/pages/&lt;id&gt;.html`</td><td style=\"text-align: left\">Per-section static snapshots with full metadata + JSON-LD, for crawlers</td></tr><tr><td style=\"text-align: left\">JSON-LD</td><td style=\"text-align: left\">`TechArticle` + `BreadcrumbList` per page; `WebSite` + optional `Organization` site-wide</td></tr><tr><td style=\"text-align: left\">Shell `&lt;title&gt;`</td><td style=\"text-align: left\">The build sets the static shell title from the <strong>default page&#39;s metadata title</strong> (`&quot;&lt;page title&gt; · &lt;brand&gt;&quot;`), so the crawler-visible root URL is specific, not generic. The brand alone is only a fallback</td></tr><tr><td style=\"text-align: left\">Runtime meta</td><td style=\"text-align: left\">`src/seo.js` keeps `&lt;title&gt;`, description, canonical, OG, and Twitter tags in sync as the SPA navigates</td></tr></tbody></table>\n<h2 id=\"make-urls-absolute\">Make URLs absolute</h2>\n<p>Declare a `domain` (or `seo.siteUrl`) on the tenant. This is what turns the</p>\n<p>sitemap `&lt;loc&gt;`, canonical, `og:url`, and `robots` `Sitemap:` into fully-qualified</p>\n<p>URLs. The <a href=\"https://www.sitemaps.org/protocol.html\" target=\"_blank\" rel=\"noopener noreferrer\">sitemap protocol</a> requires</p>\n<p>absolute URLs, so a tenant with neither set emits a non-compliant sitemap — the</p>\n<p>build prints a warning when that happens.</p>\n<p>Precedence: `seo.siteUrl` → `domain` (https-prefixed) → relative (warned).</p>\n<h2 id=\"canonical-strategy\">Canonical strategy</h2>\n<p>Static snapshots and the runtime SPA canonicalize to the <strong>crawlable static URL</strong></p>\n<p>(`/pages/&lt;id&gt;.html`), not the SPA `#hash` route. Search engines ignore URL</p>\n<p>fragments, so hash canonicals (`/#section`) would collapse every page onto the</p>\n<p>homepage. The `#hash` route is still used for the human-facing &quot;interactive</p>\n<p>version&quot; link and the JavaScript redirect on the static page.</p>\n<h2 id=\"social-cards\">Social cards</h2>\n<p>Set `seo.ogImage` (absolute or site-relative) to emit `og:image` /</p>\n<p>`twitter:image` and upgrade `twitter:card` to `summary_large_image`. Individual</p>\n<p>pages can override it with an `ogImage` field on the manifest entry.</p>\n<h2 id=\"authoring-practices\">Authoring practices</h2>\n<ul>\n<li>Keep manifest `summary` values concise — they power the meta description, search</li>\n</ul>\n<p>results, link previews, and the export document.</p>\n<ul>\n<li>Use human-readable, hyphenated, lowercase section `id`s — they become both the</li>\n</ul>\n<p>hash route and the static page filename (`/` becomes `--`).</p>\n<ul>\n<li>Gate broken links in CI with `strictLinks: true` (see</li>\n</ul>\n<p><a href=\"#tenant-config\">Tenant Configuration</a>).</p>\n </div>\n</section>" };
3
3
  }
@@ -1,3 +1,3 @@
1
1
  export async function load() {
2
- return { html: "<section class=\"section doc markdown\">\n <div class=\"doc-content\">\n<h1 id=\"where-documentation-takes-shape\">Where documentation takes shape</h1>\n<p>Pagenary is a multi-tenant documentation publishing platform that turns one</p>\n<p>shared set of templates into many branded, tenant-specific static sites. Zero</p>\n<p>runtime dependencies, hash-based routing, and a Git-aware build pipeline make</p>\n<p>it suited to white-label documentation portalsone source of truth, any</p>\n<p>number of published sites.</p>\n<p><strong>This portal is built by Pagenary, from Pagenary&#39;s own documentation.</strong> Every</p>\n<p>page you see here is the same publisher pipeline applied to the developer docs</p>\n<p>in the repository, served as a static single-page app.</p>\n<h2 id=\"start-here\">Start here</h2>\n<ul>\n<li><strong><a href=\"#quickstart\">Quickstart</a></strong> — install, build the default bundle, and serve it locally.</li>\n<li><strong><a href=\"#architecture\">Architecture</a></strong> — the static SPA pattern, build pipeline, and tenant content model.</li>\n<li><strong><a href=\"#tenant-config\">Tenant Configuration</a></strong> — every `config.json` option for branding, theming, and export.</li>\n</ul>\n<h2 id=\"how-a-tenant-works\">How a tenant works</h2>\n<p>A tenant is a thin layer — content, a `config.json`, and a `manifest.json` —</p>\n<p>over the shared template catalog. Branding, theming, and navigation are **data,</p>\n<p>not code**: a tenant changes its look through configuration, never by forking</p>\n<p>the generator. The build produces a self-contained bundle under</p>\n<p>`dist/&lt;tenant-id&gt;/` that you can host anywhere that serves files.</p>\n </div>\n</section>" };
2
+ return { html: "<section class=\"section doc markdown\">\n <div class=\"doc-content\">\n<h1 id=\"where-documentation-takes-shape\">Where documentation takes shape</h1>\n<p>Pagenary turns a folder of Markdown in a git repo into a fast, searchable,</p>\n<p>SEO-ready documentation site you host yourself — for next to nothing. Write your</p>\n<p>docs, run one tool, and deploy the static output to any free static host</p>\n<p>(GitHub/Gitea Pages, Netlify, Cloudflare Pages, S3, a CDN, or your own box). No</p>\n<p>server, no database, no monthly SaaS bill just the things you&#39;d expect from a</p>\n<p>paid docs platform: command-palette search with ranking, theming and branding,</p>\n<p>Mermaid diagrams, syntax highlighting, SEO, and one-click export.</p>\n<p><strong>This portal is built by Pagenary, from Pagenary&#39;s own documentation.</strong> Every</p>\n<p>page you see here is the same publisher pipeline applied to the developer docs in</p>\n<p>the repository, served as a static single-page app.</p>\n<h2 id=\"start-here\">Start here</h2>\n<ul>\n<li><strong><a href=\"#quickstart\">Quickstart</a></strong> — install, build your first site, and serve it locally.</li>\n<li><strong><a href=\"#architecture\">Architecture</a></strong> — the static SPA pattern, build pipeline, and content model.</li>\n<li><strong><a href=\"#tenant-config\">Tenant Configuration</a></strong> — every `config.json` option for branding, theming, SEO, and export.</li>\n</ul>\n<h2 id=\"scale-to-many-sites-when-you-need-to\">Scale to many sites when you need to</h2>\n<p>One site is just content, a `config.json`, and a `manifest.json`. Need more than</p>\n<p>one? The same tool publishes many sites from a shared template catalog branding,</p>\n<p>theming, and navigation are <strong>data, not code</strong>, so standing up another branded</p>\n<p>site is cheap, and a Git-aware build rebuilds only what changed. Pagenary scales</p>\n<p>from a weekend project to a multi-product portal without changing tools.</p>\n </div>\n</section>" };
3
3
  }
@@ -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