@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 +3 -1
- package/package.json +1 -1
- package/scripts/build-tenants.js +41 -1
- package/site/index.html +3 -3
- package/site/llms.txt +1 -1
- package/site/pages/quickstart.html +3 -1
- package/site/pages/seo-strategy.html +1 -1
- package/site/pages/welcome.html +18 -16
- package/site/robots.txt +1 -1
- package/site/search-index/manifest.json +2 -2
- package/site/search-index/part-0000.json +12 -12
- package/site/sections/quickstart.js +1 -1
- package/site/sections/seo-strategy.js +1 -1
- package/site/sections/welcome.js +1 -1
- package/site/vendor/fortemi-aiwg-index.d.ts +31 -1
- package/site/vendor/fortemi-aiwg-index.js +1 -1
- package/src/vendor/fortemi-aiwg-index.d.ts +31 -1
- package/src/vendor/fortemi-aiwg-index.js +180 -12
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
|
|
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
package/scripts/build-tenants.js
CHANGED
|
@@ -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="
|
|
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'd expect from a paid docs platform. Pagenary'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-
|
|
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
|
-
>
|
|
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)
|
|
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 `<loc>` for the home page and every section'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/<id>.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 `<title>`, 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 `<loc>` for the home page and every section'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/<id>.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 `<title>`</td><td style="text-align: left">The build sets the static shell title from the <strong>default page's metadata title</strong> (`"<page title> · <brand>"`), 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 `<title>`, 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 `<loc>`, canonical, `og:url`, and `robots` `Sitemap:` into fully-qualified</p>
|
package/site/pages/welcome.html
CHANGED
|
@@ -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
|
|
92
|
-
<p>
|
|
93
|
-
<p>
|
|
94
|
-
<p>
|
|
95
|
-
<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'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'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>
|
|
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
|
|
102
|
-
<li><strong><a href="#architecture">Architecture</a></strong> — the static SPA pattern, build pipeline, and
|
|
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="
|
|
106
|
-
<p>
|
|
107
|
-
<p>
|
|
108
|
-
<p>
|
|
109
|
-
<p>
|
|
110
|
-
<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,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schema_version": "aiwg.fortemi.index.chunk-manifest.v1",
|
|
3
|
-
"generated_at": "
|
|
3
|
+
"generated_at": "1989-11-27T18:58:05.000Z",
|
|
4
4
|
"source": {
|
|
5
5
|
"repo": "pagenary",
|
|
6
6
|
"privacy": "public",
|
|
7
|
-
"build_hash": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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)
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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
|
|
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": "
|
|
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 && npm run bootstrap</p>\n<p>npm run publisher:build && 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)
|
|
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 && npm run bootstrap</p>\n<p>npm run publisher:build && 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 "title": "My Product Documentation",\n "description": "Complete guide to using My Product",\n "brandMark": "MY",\n "brandSub": "PRODUCT",\n "tagline": "Documentation that works",\n "copyright": "My Company",\n "accentColor": "#3B82F6",\n "surfaceColor": "#F8FAFC"\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'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 'my-product';\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] --> B[Frontend]\n B --> C[API]\n C --> 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 "id": "welcome",\n "title": "Welcome",\n "summary": "Introduction to My Product",\n "file": "welcome.md"\n },\n {\n "id": "installation",\n "title": "Installation",\n "summary": "How to install My Product",\n "file": "installation.md"\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 "tenants": [\n {\n "id": "my-docs",\n "source": { "type": "local", "path": "./my-docs" },\n "strictLinks": 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 "title": "Guides",\n "sections": [\n { "id": "getting-started", "title": "Getting Started", "file": "getting-started.md" },\n { "id": "advanced", "title": "Advanced Usage", "file": "advanced.md" }\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->>API: Request\n API->>DB: Query\n DB-->>API: Results\n API-->>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/<tenant>/search-index/`)</p>\n<p>on first open — wait a moment for "Indexing content…" 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 `<loc>` for the home page and every section'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/<id>.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 `<title>`, 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 `<loc>`, 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/<id>.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 "interactive</p>\n<p>version" 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 `<loc>` for the home page and every section'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/<id>.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 `<title>`</td><td style=\"text-align: left\">The build sets the static shell title from the <strong>default page's metadata title</strong> (`"<page title> · <brand>"`), 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 `<title>`, 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 `<loc>`, 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/<id>.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 "interactive</p>\n<p>version" 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
|
}
|
package/site/sections/welcome.js
CHANGED
|
@@ -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
|
|
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'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'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.
|
|
5
|
-
* SHA-256:
|
|
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
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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
|