@shoalrun/cli 0.1.0-alpha.14 → 0.1.0-alpha.16
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/CHANGELOG.md +49 -0
- package/DEPLOYMENT-PROFILES.md +287 -0
- package/README.md +43 -2
- package/agent-docs/.well-known/shoalrun/navigation/project-updates-50f26fe22670.txt +5 -1
- package/agent-docs/SNAPSHOT.json +5 -5
- package/agent-docs/changelog/2026-10-30.md +43 -0
- package/agent-docs/changelog/2026-11-03.md +56 -0
- package/agent-docs/changelog.md +17 -1
- package/agent-docs/documents.json +419 -61
- package/agent-docs/index.md +73 -103
- package/agent-docs/provenance.json +4 -4
- package/agent-docs/publish/content-export.md +1 -1
- package/agent-docs/publish/content-export.sections/prerequisites.md +11 -0
- package/agent-docs/publish/deployment-profiles.md +273 -0
- package/agent-docs/publish/deployment-profiles.sections/before-you-begin.md +44 -0
- package/agent-docs/publish/deployment-profiles.sections/build-and-inspect-the-edition.md +69 -0
- package/agent-docs/publish/deployment-profiles.sections/check-the-result.md +46 -0
- package/agent-docs/publish/deployment-profiles.sections/choose-navigation-labels.md +53 -0
- package/agent-docs/publish/deployment-profiles.sections/current-limits.md +46 -0
- package/agent-docs/publish/deployment-profiles.sections/define-the-shared-sku-manifest.md +70 -0
- package/agent-docs/publish/deployment-profiles.sections/html-markdown-and-agents.md +44 -0
- package/agent-docs/publish/deployment-profiles.sections/mark-pages-and-shared-sections.md +135 -0
- package/agent-docs/publish/deployment-profiles.sections/related-reading.md +42 -0
- package/agent-docs/publish/deployment-profiles.sections/supply-the-profile-from-the-deployment.md +68 -0
- package/agent-docs/search-index.json +1 -1
- package/agent-docs/start.md +2 -2
- package/agent-docs/start.sections/add-the-page-to-navigation.md +1 -1
- package/agent-docs/start.sections/build-and-check-both-outputs.md +1 -1
- package/agent-docs/start.sections/build-this-site.md +1 -1
- package/agent-docs/start.sections/check-writing-style.md +1 -1
- package/agent-docs/start.sections/create-a-page.md +1 -1
- package/agent-docs/start.sections/make-it-yours.md +1 -1
- package/agent-docs/start.sections/migrating-from-github-packages.md +1 -1
- package/agent-docs/start.sections/prerequisites.md +1 -1
- package/agent-docs/start.sections/related-reading.md +1 -1
- package/agent-docs/start.sections/start-your-own-project.md +2 -2
- package/assets/search-semantic.js +34 -0
- package/dist/api-publication-plan.d.ts +3 -0
- package/dist/api-publication-plan.js +30 -3
- package/dist/artifact-bundle.d.ts +2 -1
- package/dist/artifact-bundle.js +2 -2
- package/dist/authoring-api.d.ts +1 -1
- package/dist/authoring-api.js +1 -1
- package/dist/authoring-project-command.js +4 -2
- package/dist/authoring-project.d.ts +2 -0
- package/dist/authoring-project.js +4 -4
- package/dist/authoring-snapshot.d.ts +23 -4
- package/dist/authoring-snapshot.js +84 -12
- package/dist/authoring-validation-command.js +5 -3
- package/dist/authoring-validation.d.ts +2 -0
- package/dist/authoring-validation.js +38 -11
- package/dist/cli-discovery.d.ts +2 -2
- package/dist/cli-discovery.js +9 -9
- package/dist/cli.js +14 -8
- package/dist/compiler.d.ts +13 -0
- package/dist/compiler.js +27 -74
- package/dist/component-names.js +1 -1
- package/dist/component-transform.js +7 -1
- package/dist/config.d.ts +4 -1
- package/dist/config.js +19 -5
- package/dist/content-export-command.d.ts +13 -1
- package/dist/content-export-command.js +3 -2
- package/dist/content-export.d.ts +13 -0
- package/dist/content-export.js +5 -4
- package/dist/content-source.d.ts +1 -0
- package/dist/content-workspace.d.ts +13 -0
- package/dist/content-workspace.js +18 -1
- package/dist/deployment-profile.d.ts +52 -0
- package/dist/deployment-profile.js +277 -0
- package/dist/frontmatter.js +11 -4
- package/dist/index-build.js +7 -3
- package/dist/page-materialization.js +2 -0
- package/dist/page-renderer.d.ts +1 -0
- package/dist/page-renderer.js +3 -1
- package/dist/palettes.d.ts +1 -1
- package/dist/project-navigation.d.ts +5 -0
- package/dist/project-navigation.js +19 -0
- package/dist/publication-plan.d.ts +3 -1
- package/dist/publication-plan.js +23 -4
- package/dist/publication-render.d.ts +3 -2
- package/dist/publication-render.js +2 -2
- package/dist/retrieval.d.ts +4 -0
- package/dist/retrieval.js +28 -8
- package/dist/search-handler.js +11 -11
- package/dist/search-identifier.d.ts +2 -1
- package/dist/search-identifier.js +10 -4
- package/dist/search-results.d.ts +1 -1
- package/dist/search-results.js +39 -25
- package/dist/site-identity.d.ts +3 -0
- package/dist/site-identity.js +69 -0
- package/dist/vercel.d.ts +1 -0
- package/dist/vercel.js +3 -1
- package/package.json +2 -1
- package/skills/shoalrun-author/SKILL.md +5 -1
- package/skills/shoalrun-author/references/customer-editions.md +164 -0
- package/skills/shoalrun-recipes/SKILL.md +2 -0
- package/skills/shoalrun-setup/SKILL.md +4 -0
- package/styles/site.css +2 -0
- package/agent-docs/publish/content-export.sections/upgrade-an-existing-integration.md +0 -34
package/dist/index-build.js
CHANGED
|
@@ -164,7 +164,9 @@ export function chunkMarkdown(markdown, document, anchors = [], options = {}) {
|
|
|
164
164
|
return chunks;
|
|
165
165
|
}
|
|
166
166
|
function makeChunk(document, headingPath, anchor, text, used, part = 0) {
|
|
167
|
-
|
|
167
|
+
// Logical page IDs repeat across releases. Stored passage IDs must not: vector
|
|
168
|
+
// rows and answer citations are keyed globally within this site's index.
|
|
169
|
+
const base = `${encodeURIComponent(document.variant)}:${encodeURIComponent(document.id)}#${slug(headingPath.join(' ')) || 'intro'}${part ? `~${part + 1}` : ''}`;
|
|
168
170
|
const seen = used.get(base) ?? 0;
|
|
169
171
|
used.set(base, seen + 1);
|
|
170
172
|
const chunk = { id: seen ? `${base}-${seen + 1}` : base, docId: document.id, title: document.title, type: document.type, variant: document.variant, headingPath, url: anchor ? `${document.url}#${anchor}` : document.url, markdownUrl: document.markdownUrl, text };
|
|
@@ -219,8 +221,10 @@ export async function buildChunkIndex(directory, options = {}) {
|
|
|
219
221
|
const { channel, version, language } = document.variant;
|
|
220
222
|
chunks.push(...chunkMarkdown(markdown, { ...document, variant: `${channel}|${version}|${language}` }, anchors.get(document.url), options));
|
|
221
223
|
}
|
|
222
|
-
|
|
223
|
-
|
|
224
|
+
// Generation identity includes publication membership, routes, reader marks,
|
|
225
|
+
// and prepared search evidence. Embedding digests remain content-only.
|
|
226
|
+
const source = chunks.map(({ id, variant, digest, url, markdownUrl, readerFilters, search }) => ({ id, variant, digest, url, markdownUrl, readerFilters: Object.fromEntries(Object.entries(readerFilters ?? {}).sort(([a], [b]) => a.localeCompare(b)).map(([facet, values]) => [facet, [...values].sort()])), search })).sort((a, b) => a.id.localeCompare(b.id));
|
|
227
|
+
const index = { schemaVersion: 1, sourceDigest: sha(JSON.stringify({ identityVersion: 2, source, synonyms })), chunks, ...(synonyms.length ? { synonyms } : {}) };
|
|
224
228
|
const output = path.join(await aiDirectory(root), 'chunks.json');
|
|
225
229
|
await writeFile(output, JSON.stringify(index) + '\n');
|
|
226
230
|
return { index, output, documents: manifest.documents.length };
|
|
@@ -29,6 +29,8 @@ export async function materializePages({ variables, frontmatterConfig, publicati
|
|
|
29
29
|
local.set(other.frontmatter.id, other);
|
|
30
30
|
const parsed = parse.parse(doc.body);
|
|
31
31
|
const tree = await resolveApiEmbedTree(transformMdx(parsed, doc, local, snippets, variables, snippetUses, calloutTypes, base, readerFilters, frontmatterConfig, publications, badgeVariants, componentPlugins), api, operationId => sourceForOperation(operationId, doc.variant), apiId => {
|
|
32
|
+
if (doc.featureContext?.grants)
|
|
33
|
+
throw new Error('Standalone API schema embeds require an explicitly scoped source and are not supported in profiled builds; select an operation instead');
|
|
32
34
|
const definition = apiPlan.apiDefinitions[apiId];
|
|
33
35
|
if (!definition)
|
|
34
36
|
throw new Error(`unknown API ${apiId}`);
|
package/dist/page-renderer.d.ts
CHANGED
package/dist/page-renderer.js
CHANGED
|
@@ -62,6 +62,8 @@ export function buildHtmlPage(doc, tree, markdownUrl, siteTitle, navigation, exa
|
|
|
62
62
|
const audienceLabels = doc.frontmatter.audiences?.filter(label => label.trim()) ?? [];
|
|
63
63
|
const audienceBadges = audienceLabels.length ? `<div class="audience-labels" role="group" aria-label="Intended audience"><span class="audience-label-heading">For</span>${audienceLabels.map(label => `<span class="audience-badge">${esc(readerConfig?.facets.audience?.options[label] ?? label)}</span>`).join('')}</div>` : '';
|
|
64
64
|
const title = doc.frontmatter.seo?.title ?? doc.frontmatter.title;
|
|
65
|
+
const brand = `${shell.logo ? `<img class="brand-logo" src="${esc(shell.logo)}" alt="">` : ''}${esc(siteTitle)}`;
|
|
66
|
+
const brandClass = shell.logo ? 'brand brand-custom' : 'brand';
|
|
65
67
|
const pageTitle = String(title).trim();
|
|
66
68
|
const brandedTitle = `${pageTitle} | ${siteTitle.trim()}`;
|
|
67
69
|
const socialTitle = !doc.frontmatter.seo?.title && pageTitle.length < 30 && brandedTitle.length <= 70 ? brandedTitle : pageTitle;
|
|
@@ -174,6 +176,6 @@ export function buildHtmlPage(doc, tree, markdownUrl, siteTitle, navigation, exa
|
|
|
174
176
|
const menu = side || sectionNavigation ? `<a class="mobile-menu" href="#${side ? 'docs-navigation' : 'docs-sections'}">Menu</a>` : '';
|
|
175
177
|
const rail = toc || panel ? `<aside class="page-rail">${doc.endpoint ? `${panel}${toc}` : `${toc}${panel}`}</aside>` : '';
|
|
176
178
|
const breadcrumbs = (shell.breadcrumbs?.length ?? 0) > 1 && !hero ? `<nav class="doc-breadcrumbs" aria-label="Breadcrumb"><ol>${shell.breadcrumbs.map((item, index) => `<li>${index === shell.breadcrumbs.length - 1 ? `<span aria-current="page">${esc(item.label)}</span>` : item.url ? `<a href="${esc(item.url)}">${esc(item.label)}</a>` : `<span>${esc(item.label)}</span>`}</li>`).join('')}</ol></nav>` : '';
|
|
177
|
-
const markup = `<!doctype html><html lang="${esc(doc.variant.language)}"${shell.themeCss || shell.paletteCss ? ` data-theme="${esc(shell.preset ?? 'shoal')}" data-palette="${esc(shell.palette ?? '')}"` : ''}${shell.masthead ? ` data-masthead="${esc(shell.masthead)}"` : ''}${layout === 'docs' ? '' : ` data-layout="${layout}"`}><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><meta name="color-scheme" content="${shell.mode === 'system' ? 'light dark' : shell.mode ?? 'light'}">${shell.indexing === false || doc.frontmatter.seo?.noindex === true ? '<meta name="robots" content="noindex">' : ''}${shell.origin ? `<link rel="canonical" href="${esc(absolute(doc.route))}">` : ''}${shell.toggle ? '<script src="/assets/mode.js"></script>' : ''}<title>${esc(String(title))}</title>${description ? `<meta name="description" content="${esc(String(description))}">` : ''}${socialDescription ? `<meta property="og:description" content="${esc(socialDescription)}">` : ''}${social}<link rel="alternate" type="text/markdown" href="${esc(markdownUrl)}"><link rel="describedby" href="/llms.min.txt" title="Documentation index">${icons}${shell.preloadFont ? `<link rel="preload" href="/assets/fonts/${esc(shell.preloadFont)}" as="font" type="font/woff2" crossorigin>` : ''}<link rel="stylesheet" href="/assets/site.css">${article.includes('data-theme-preview') ? '<link rel="stylesheet" href="/assets/theme-preview.css">' : ''}${shell.themeCss ? '<link rel="stylesheet" href="/assets/theme.css" data-theme-link="theme">' : ''}${shell.paletteCss ? '<link rel="stylesheet" href="/assets/palette.css" data-theme-link="palette">' : ''}${shell.badgesCss ? '<link rel="stylesheet" href="/assets/badges.css">' : ''}${shell.themeSwitcher ? `<script src="/assets/theme-switcher.js" data-themes="${esc(shell.themeSwitcher)}"></script>` : ''}${shell.fontsCss ? '<link rel="stylesheet" href="/assets/fonts.css">' : ''}${customCss ? '<link rel="stylesheet" href="/assets/project.css">' : ''}</head><body><a class="skip" href="#main">Skip to content</a>${agent.mode === 'disclosure' ? '' : agentBlock}<header class="mast"><div class="mast-inner">${shell.home ? `<a class="
|
|
179
|
+
const markup = `<!doctype html><html lang="${esc(doc.variant.language)}"${shell.themeCss || shell.paletteCss ? ` data-theme="${esc(shell.preset ?? 'shoal')}" data-palette="${esc(shell.palette ?? '')}"` : ''}${shell.masthead ? ` data-masthead="${esc(shell.masthead)}"` : ''}${layout === 'docs' ? '' : ` data-layout="${layout}"`}><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><meta name="color-scheme" content="${shell.mode === 'system' ? 'light dark' : shell.mode ?? 'light'}">${shell.indexing === false || doc.frontmatter.seo?.noindex === true ? '<meta name="robots" content="noindex">' : ''}${shell.origin ? `<link rel="canonical" href="${esc(absolute(doc.route))}">` : ''}${shell.toggle ? '<script src="/assets/mode.js"></script>' : ''}<title>${esc(String(title))}</title>${description ? `<meta name="description" content="${esc(String(description))}">` : ''}${socialDescription ? `<meta property="og:description" content="${esc(socialDescription)}">` : ''}${social}<link rel="alternate" type="text/markdown" href="${esc(markdownUrl)}"><link rel="describedby" href="/llms.min.txt" title="Documentation index">${icons}${shell.preloadFont ? `<link rel="preload" href="/assets/fonts/${esc(shell.preloadFont)}" as="font" type="font/woff2" crossorigin>` : ''}<link rel="stylesheet" href="/assets/site.css">${article.includes('data-theme-preview') ? '<link rel="stylesheet" href="/assets/theme-preview.css">' : ''}${shell.themeCss ? '<link rel="stylesheet" href="/assets/theme.css" data-theme-link="theme">' : ''}${shell.paletteCss ? '<link rel="stylesheet" href="/assets/palette.css" data-theme-link="palette">' : ''}${shell.badgesCss ? '<link rel="stylesheet" href="/assets/badges.css">' : ''}${shell.themeSwitcher ? `<script src="/assets/theme-switcher.js" data-themes="${esc(shell.themeSwitcher)}"></script>` : ''}${shell.fontsCss ? '<link rel="stylesheet" href="/assets/fonts.css">' : ''}${customCss ? '<link rel="stylesheet" href="/assets/project.css">' : ''}</head><body><a class="skip" href="#main">Skip to content</a>${agent.mode === 'disclosure' ? '' : agentBlock}<header class="mast"><div class="mast-inner">${shell.home ? `<a class="${brandClass}" href="/">${brand}</a>` : `<span class="${brandClass}">${brand}</span>`}${editionLabel && (shell.publicationAlternates?.length ?? 0) < 2 ? `<span class="edition">${esc(editionLabel)}</span>` : ''}${menu}${headerTools(shell, doc.variant, esc, readerConfig)}${agent.mode === 'disclosure' ? agentBlock : ''}</div></header>${sectionNavigation}${shortcuts}<div class="layout${layout === 'docs' ? '' : ` layout-${layout}`}${rail ? ' has-rail' : ''}${panel ? ' has-examples' : ''}${doc.endpoint ? ' api-reference' : ''}"><main id="main" class="content" tabindex="-1" data-document-type="${esc(doc.frontmatter.type)}"${readerConfig ? ` data-reader-page="${esc(readerFilterAttribute(doc.readerFilters ?? {}))}"` : ''}>${breadcrumbs}${hero ? '' : `<header class="page-head"><span class="eyebrow">${esc(typeLabel(doc.frontmatter.type))}</span>${doc.endpoint ? '' : badge(doc.frontmatter.subjectLifecycle)}<h1>${esc(doc.frontmatter.title)}${doc.endpoint ? badge(doc.frontmatter.subjectLifecycle) : ''}</h1>${pageHeader && !routeSummary ? `<p class="summary">${esc(pageHeader)}</p>` : ''}${pageDates}${endpoint}${audienceBadges}</header>`}${hero ? '' : `<div class="page-formats"><a class="formats" href="${esc(markdownUrl)}">Read as Markdown</a><button type="button" class="formats" data-copy-markdown hidden>Copy as Markdown</button><span role="status" data-copy-markdown-status></span></div>`}${toc ? '<div class="mobile-outline-slot"></div>' : ''}${jumps}${readerBanner}<article>${article}</article></main>${rail}${side}</div>${footer}${analyticsBootstrap}${readerBootstrap}<script type="module" src="/assets/copy-markdown.js"></script><script type="module" src="/assets/navigation.js"></script><script type="module" src="/assets/reader.js"></script><script type="module" src="/assets/tabs.js"></script>${shell.search ? '<script type="module" src="/assets/search.js"></script>' : ''}${article.includes('data-tooltip') ? '<script type="module" src="/assets/tooltips.js"></script>' : ''}${article.includes('data-component-module') ? '<script type="module" src="/assets/components.js"></script>' : ''}${article.includes('data-file-tree') ? '<script type="module" src="/assets/file-tree.js"></script>' : ''}${article.includes('data-image-expand') ? '<script type="module" src="/assets/image-viewer.js"></script>' : ''}${article.includes('data-table-filter') || article.includes('data-definition-filter') ? '<script type="module" src="/assets/table-filter.js"></script>' : ''}${article.includes('data-walkthrough') ? '<script type="module" src="/assets/walkthrough.js"></script>' : ''}${article.includes('data-cookbook-chooser') ? '<script type="module" src="/assets/cookbook-chooser.js"></script>' : ''}${article.includes('data-theme-preview') ? '<script type="module" src="/assets/theme-preview.js"></script>' : ''}${panel ? '<script type="module" src="/assets/example-panel.js"></script>' : ''}${panel.includes('data-code-samples') ? '<script type="module" src="/assets/code-samples.js"></script>' : ''}</body></html>`;
|
|
178
180
|
return basePath ? markup.replace(/((?:href|src)=")\/(assets\/|llms(?:\.min)?\.txt|apple-touch-icon\.png)/g, `$1${basePath}/$2`).replace('href="/"', `href="${basePath}/"`) : markup;
|
|
179
181
|
}
|
package/dist/palettes.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export declare const paletteTokens: readonly ["text", "page", "surface", "link", "link-hover", "focus", "ink", "accent", "accent-contrast", "header-muted", "header-border", "border", "border-soft", "border-nested", "field-surface", "field-border", "field-accent", "presence-bg", "presence-text", "optional-bg", "optional-text", "muted", "nav-heading", "section-heading", "nav-text", "nav-hover", "nav-active", "nav-active-text", "deprecated-bg", "deprecated-text", "beta-bg", "beta-text", "development-bg", "development-text", "experimental-bg", "experimental-text", "emphasis", "summary", "code-bg", "code-text", "code-muted", "code-border", "code-block", "callout-line", "callout-bg", ...("callout-
|
|
1
|
+
export declare const paletteTokens: readonly ["text", "page", "surface", "link", "link-hover", "focus", "ink", "accent", "accent-contrast", "header-muted", "header-border", "border", "border-soft", "border-nested", "field-surface", "field-border", "field-accent", "presence-bg", "presence-text", "optional-bg", "optional-text", "muted", "nav-heading", "section-heading", "nav-text", "nav-hover", "nav-active", "nav-active-text", "deprecated-bg", "deprecated-text", "beta-bg", "beta-text", "development-bg", "development-text", "experimental-bg", "experimental-text", "emphasis", "summary", "code-bg", "code-text", "code-muted", "code-border", "code-block", "callout-line", "callout-bg", ...("callout-note-line" | "callout-tip-line" | "callout-important-line" | "callout-warning-line" | "callout-caution-line" | "callout-note-bg" | "callout-tip-bg" | "callout-important-bg" | "callout-warning-bg" | "callout-caution-bg" | "callout-note-text" | "callout-tip-text" | "callout-important-text" | "callout-warning-text" | "callout-caution-text")[], "strong-surface", "strong-text"];
|
|
2
2
|
export type Token = typeof paletteTokens[number];
|
|
3
3
|
export type Mode = 'light' | 'dark';
|
|
4
4
|
export type Palette = {
|
|
@@ -38,4 +38,9 @@ export declare function navigationPath(nodes: NavigationNode[], route: string):
|
|
|
38
38
|
label: string;
|
|
39
39
|
url?: string;
|
|
40
40
|
}>;
|
|
41
|
+
/** Prune only after validating every authored node against known source identities. */
|
|
42
|
+
export declare function profileNavigation(layout: unknown, docs: Map<string, Document>, knownIds: Set<string>, operations: Map<string, {
|
|
43
|
+
apiId: string;
|
|
44
|
+
reference: string;
|
|
45
|
+
}>, apiIds: Set<string>): NavSpec[];
|
|
41
46
|
export {};
|
|
@@ -188,3 +188,22 @@ export function navigationPath(nodes, route) {
|
|
|
188
188
|
}
|
|
189
189
|
return [];
|
|
190
190
|
}
|
|
191
|
+
/** Prune only after validating every authored node against known source identities. */
|
|
192
|
+
export function profileNavigation(layout, docs, knownIds, operations, apiIds) {
|
|
193
|
+
const known = new Map(docs);
|
|
194
|
+
for (const id of knownIds)
|
|
195
|
+
if (![...known.values()].some(doc => doc.frontmatter.id === id))
|
|
196
|
+
known.set(id, { frontmatter: { id } });
|
|
197
|
+
const validated = validateProjectNavigation(layout, known, operations, apiIds);
|
|
198
|
+
const available = new Set([...docs.values()].map(doc => doc.frontmatter.id));
|
|
199
|
+
const prune = (nodes) => nodes.flatMap(node => {
|
|
200
|
+
if (node.doc && !available.has(node.doc) || node.operation && !available.has(operations.get(node.operation).reference) || node.tab && !available.has(node.landing))
|
|
201
|
+
return [];
|
|
202
|
+
if (node.items) {
|
|
203
|
+
const items = prune(node.items);
|
|
204
|
+
return items.length ? [{ ...node, items }] : [];
|
|
205
|
+
}
|
|
206
|
+
return [node];
|
|
207
|
+
});
|
|
208
|
+
return prune(validated);
|
|
209
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type FeatureContext } from './deployment-profile.js';
|
|
1
2
|
import { type Document, type Variant } from './content.js';
|
|
2
3
|
import type { PublicationRoutingConfig } from './content-source.js';
|
|
3
4
|
import { type MetadataProvider } from './metadata-provider.js';
|
|
@@ -16,7 +17,7 @@ export type AuthoredPublicationPlan = {
|
|
|
16
17
|
authoredOperationIds: Set<string>;
|
|
17
18
|
routeOwners: Map<string, string>;
|
|
18
19
|
};
|
|
19
|
-
export declare function planAuthoredDocuments({ base, basePath, config, variants, language, readerFilters, contentFilter, metadataProvider, metadataDependencies, sourceOverrides, authoringFiles, allowRouteCollisions }: {
|
|
20
|
+
export declare function planAuthoredDocuments({ base, basePath, config, variants, language, readerFilters, contentFilter, featureContext, metadataProvider, metadataDependencies, sourceOverrides, authoringFiles, allowRouteCollisions }: {
|
|
20
21
|
base: string;
|
|
21
22
|
basePath: string;
|
|
22
23
|
config: AuthoredPublicationConfig;
|
|
@@ -24,6 +25,7 @@ export declare function planAuthoredDocuments({ base, basePath, config, variants
|
|
|
24
25
|
language: string;
|
|
25
26
|
readerFilters?: ReaderFiltersConfig;
|
|
26
27
|
contentFilter?: ReaderSelection;
|
|
28
|
+
featureContext?: FeatureContext;
|
|
27
29
|
metadataProvider?: {
|
|
28
30
|
provide: MetadataProvider;
|
|
29
31
|
};
|
package/dist/publication-plan.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { hasScaffoldPlaceholders } from './scaffold-placeholders.js';
|
|
2
|
+
import { validateFeatureClassification, matchesFeatures } from './deployment-profile.js';
|
|
2
3
|
import { matchesContentFilter } from './content-filter.js';
|
|
3
4
|
import { readdir } from 'node:fs/promises';
|
|
4
5
|
import path from 'node:path';
|
|
@@ -13,7 +14,7 @@ async function files(dir) {
|
|
|
13
14
|
const nested = await Promise.all(entries.map(async (entry) => entry.isDirectory() ? files(path.join(dir, entry.name)) : /\.mdx?$/.test(entry.name) ? [path.join(dir, entry.name)] : []));
|
|
14
15
|
return nested.flat().sort();
|
|
15
16
|
}
|
|
16
|
-
export async function planAuthoredDocuments({ base, basePath, config, variants, language, readerFilters, contentFilter, metadataProvider, metadataDependencies, sourceOverrides, authoringFiles, allowRouteCollisions }) {
|
|
17
|
+
export async function planAuthoredDocuments({ base, basePath, config, variants, language, readerFilters, contentFilter, featureContext, metadataProvider, metadataDependencies, sourceOverrides, authoringFiles, allowRouteCollisions }) {
|
|
17
18
|
const docs = new Map();
|
|
18
19
|
const knownDocumentIds = new Set();
|
|
19
20
|
const knownDocumentTypes = new Map();
|
|
@@ -56,13 +57,28 @@ export async function planAuthoredDocuments({ base, basePath, config, variants,
|
|
|
56
57
|
if (frontmatter.type === 'api-reference')
|
|
57
58
|
for (const operation of frontmatter.operations ?? [])
|
|
58
59
|
authoredOperationIds.add(operation);
|
|
59
|
-
|
|
60
|
+
const classificationVariants = featureContext?.classificationPublications ?? variants;
|
|
61
|
+
const classificationEligible = frontmatter.access?.policy === 'public' && classificationVariants.some(variant => (pagePublications ? pagePublications.some(item => item.channel === variant.channel && item.version === variant.version) : frontmatter.channel === variant.channel && frontmatter.version === variant.version) && frontmatter.language === variant.language);
|
|
62
|
+
if (classificationEligible)
|
|
63
|
+
validateFeatureClassification(sourceFrontmatter, featureContext, `${file}: ${frontmatter.id}`);
|
|
64
|
+
const included = matchesFeatures(frontmatter.requires, featureContext, `${file}: ${frontmatter.id}`);
|
|
65
|
+
const decision = featureContext?.decisions.at(-1);
|
|
66
|
+
if (!included)
|
|
60
67
|
continue;
|
|
68
|
+
if (frontmatter.access?.policy !== 'public' && !authoringFiles?.has(file)) {
|
|
69
|
+
if (decision)
|
|
70
|
+
decision.included = false;
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
61
73
|
const marks = pageReaderFilters(frontmatter, readerFilters, file);
|
|
62
|
-
if (!matchesContentFilter(marks, contentFilter))
|
|
74
|
+
if (!matchesContentFilter(marks, contentFilter)) {
|
|
75
|
+
if (decision)
|
|
76
|
+
decision.included = false;
|
|
63
77
|
continue;
|
|
78
|
+
}
|
|
64
79
|
if (hasScaffoldPlaceholders(body) || /\[\[TODO:[\s\S]*?\]\]/.test(JSON.stringify(frontmatter)) || frontmatter.summary?.startsWith('Draft guide for '))
|
|
65
80
|
throw new Error(`${file}: unresolved scaffold placeholders cannot be published`);
|
|
81
|
+
let selectedPublication = false;
|
|
66
82
|
for (const variant of variants) {
|
|
67
83
|
if (pagePublications ? !pagePublications.some(item => item.channel === variant.channel && item.version === variant.version) : (frontmatter.channel && frontmatter.channel !== variant.channel || frontmatter.version && frontmatter.version !== variant.version))
|
|
68
84
|
continue;
|
|
@@ -79,8 +95,11 @@ export async function planAuthoredDocuments({ base, basePath, config, variants,
|
|
|
79
95
|
if (collision && !allowRouteCollisions)
|
|
80
96
|
throw new Error(`Route collision ${route}: ${collision} and ${key}`);
|
|
81
97
|
routeOwners.set(route.toLowerCase(), key);
|
|
82
|
-
|
|
98
|
+
selectedPublication = true;
|
|
99
|
+
docs.set(key, { featureContext, file, frontmatter: publishedFrontmatter, body, tree: null, variant, route, markdownRoute: markdownRoute(route, basePath), readerFilters: marks });
|
|
83
100
|
}
|
|
101
|
+
if (!selectedPublication && decision)
|
|
102
|
+
decision.included = false;
|
|
84
103
|
}
|
|
85
104
|
return { contentDir, docs, knownDocumentIds, knownDocumentTypes, authoredOperationIds, routeOwners };
|
|
86
105
|
}
|
|
@@ -8,7 +8,7 @@ import type { ReaderFiltersConfig } from './reader-filters.js';
|
|
|
8
8
|
import type { CodeHighlighter } from './highlight.js';
|
|
9
9
|
import type { MaterializedPage } from './page-materialization.js';
|
|
10
10
|
import type { ResolvedCompilerConfig, CompilerSiteConfig, CompilerPublicationsConfig } from './compiler-config.js';
|
|
11
|
-
export declare function renderPublicationPages({ rendered, docs, relationships, knownOperations, cardUses, publishedRelationships, cookbooks, documentRoutes, basePath, siteOrigin, colorMode, config, highlighter, editionLabel, customStyleFile, themePreset, palette, themeStyleFile, generatedPaletteCss, generatedBadgeCss, themeSwitcherData, generatedFontsCss, fontsPreload, shareFor, faviconRoute, modeToggle, header, footer, search, readerFilters, variants, externalDocuments, externalSiteLinks, navigationSpec, navigationEntries, navigationTagOrder, freshness }: {
|
|
11
|
+
export declare function renderPublicationPages({ rendered, docs, relationships, knownOperations, cardUses, publishedRelationships, cookbooks, documentRoutes, basePath, siteOrigin, colorMode, config, highlighter, editionLabel, customStyleFile, themePreset, palette, themeStyleFile, generatedPaletteCss, generatedBadgeCss, themeSwitcherData, generatedFontsCss, fontsPreload, shareFor, faviconRoute, logoRoute, modeToggle, header, footer, search, readerFilters, variants, externalDocuments, externalSiteLinks, navigationSpec, navigationEntries, navigationTagOrder, freshness }: {
|
|
12
12
|
freshness?: Freshness;
|
|
13
13
|
rendered: MaterializedPage[];
|
|
14
14
|
docs: Map<string, Document>;
|
|
@@ -47,11 +47,12 @@ export declare function renderPublicationPages({ rendered, docs, relationships,
|
|
|
47
47
|
file?: string;
|
|
48
48
|
} | undefined;
|
|
49
49
|
faviconRoute: string;
|
|
50
|
+
logoRoute?: string;
|
|
50
51
|
modeToggle: boolean;
|
|
51
52
|
header: HeaderConfig;
|
|
52
53
|
footer?: FooterConfig;
|
|
53
54
|
search: {
|
|
54
|
-
provider: 'lexical' | 'custom' | 'none';
|
|
55
|
+
provider: 'lexical' | 'semantic' | 'custom' | 'none';
|
|
55
56
|
};
|
|
56
57
|
readerFilters?: ReaderFiltersConfig;
|
|
57
58
|
variants: Variant[];
|
|
@@ -11,7 +11,7 @@ import { navigationPath, navigationTab, navigationTabRecord, resolveProjectNavig
|
|
|
11
11
|
import { outputUrl, publicTree, resolveOutputLinks } from './representation-tree.js';
|
|
12
12
|
import { presetCatalog } from './palettes.js';
|
|
13
13
|
const keyFor = (id, variant) => `${id}|${variant.channel}|${variant.version}|${variant.language}`;
|
|
14
|
-
export function renderPublicationPages({ rendered, docs, relationships, knownOperations, cardUses, publishedRelationships, cookbooks, documentRoutes, basePath, siteOrigin, colorMode, config, highlighter, editionLabel, customStyleFile, themePreset, palette, themeStyleFile, generatedPaletteCss, generatedBadgeCss, themeSwitcherData, generatedFontsCss, fontsPreload, shareFor, faviconRoute, modeToggle, header, footer, search, readerFilters, variants, externalDocuments, externalSiteLinks, navigationSpec, navigationEntries, navigationTagOrder, freshness }) {
|
|
14
|
+
export function renderPublicationPages({ rendered, docs, relationships, knownOperations, cardUses, publishedRelationships, cookbooks, documentRoutes, basePath, siteOrigin, colorMode, config, highlighter, editionLabel, customStyleFile, themePreset, palette, themeStyleFile, generatedPaletteCss, generatedBadgeCss, themeSwitcherData, generatedFontsCss, fontsPreload, shareFor, faviconRoute, logoRoute, modeToggle, header, footer, search, readerFilters, variants, externalDocuments, externalSiteLinks, navigationSpec, navigationEntries, navigationTagOrder, freshness }) {
|
|
15
15
|
const resolvedNavigation = new Map();
|
|
16
16
|
const scopedRelationships = new Map();
|
|
17
17
|
const hasHome = rendered.some(page => page.doc.route === basePath || page.doc.route === '/');
|
|
@@ -86,7 +86,7 @@ export function renderPublicationPages({ rendered, docs, relationships, knownOpe
|
|
|
86
86
|
label: `${item.label}${item.label.toLowerCase() === item.variant.channel.toLowerCase() ? '' : ` · ${item.variant.channel}`} · ${item.variant.version}${languageSuffix(item.variant)}`,
|
|
87
87
|
url: item.url, current: false
|
|
88
88
|
}))];
|
|
89
|
-
page.html = buildHtmlPage(page.doc, page.htmlTree, page.doc.markdownRoute, config.site.title, navigation, page.pageExamples, page.pageShapes, highlighter, editionLabel(page.doc.variant), !!customStyleFile, { preset: themePreset, palette: palette.name, themeCss: !!themeStyleFile, paletteCss: !!generatedPaletteCss, badgesCss: !!generatedBadgeCss, themeSwitcher: themeSwitcherData, fontsCss: !!generatedFontsCss, preloadFont: fontsPreload, masthead: presetCatalog.find(preset => preset.name === themePreset)?.masthead, share: shareFor(page.doc), favicon: faviconRoute, touchIcon: !faviconRoute.endsWith('.png'), origin: siteOrigin, indexing: config.site.indexing !== false, home: hasHome, mode: colorMode, toggle: modeToggle, header, footer, search: search.provider !== 'none', publicationAlternates, breadcrumbs }, page.pageCurl, page.pageSamples, { ...config, readerFilters });
|
|
89
|
+
page.html = buildHtmlPage(page.doc, page.htmlTree, page.doc.markdownRoute, config.site.title, navigation, page.pageExamples, page.pageShapes, highlighter, editionLabel(page.doc.variant), !!customStyleFile, { preset: themePreset, palette: palette.name, themeCss: !!themeStyleFile, paletteCss: !!generatedPaletteCss, badgesCss: !!generatedBadgeCss, themeSwitcher: themeSwitcherData, fontsCss: !!generatedFontsCss, preloadFont: fontsPreload, masthead: presetCatalog.find(preset => preset.name === themePreset)?.masthead, share: shareFor(page.doc), favicon: faviconRoute, logo: logoRoute, touchIcon: !faviconRoute.endsWith('.png'), origin: siteOrigin, indexing: config.site.indexing !== false, home: hasHome, mode: colorMode, toggle: modeToggle, header, footer, search: search.provider !== 'none', publicationAlternates, breadcrumbs }, page.pageCurl, page.pageSamples, { ...config, readerFilters });
|
|
90
90
|
}
|
|
91
91
|
return resolvedNavigation;
|
|
92
92
|
}
|
package/dist/retrieval.d.ts
CHANGED
|
@@ -21,6 +21,7 @@ export declare class LexicalIndex {
|
|
|
21
21
|
private readonly average;
|
|
22
22
|
constructor(chunks: Chunk[], synonyms?: SynonymGroups);
|
|
23
23
|
search(query: string, limit?: number, keep?: Keep): Hit[];
|
|
24
|
+
scan(query: string, keep?: Keep): IterableIterator<Hit>;
|
|
24
25
|
}
|
|
25
26
|
type VectorRow = {
|
|
26
27
|
chunk: Chunk;
|
|
@@ -30,7 +31,9 @@ export declare class VectorIndex {
|
|
|
30
31
|
private readonly rows;
|
|
31
32
|
constructor(rows: VectorRow[]);
|
|
32
33
|
search(query: Float32Array, limit?: number, keep?: Keep): Hit[];
|
|
34
|
+
scan(query: Float32Array, keep?: Keep): IterableIterator<Hit>;
|
|
33
35
|
}
|
|
36
|
+
export declare const compareHits: (left: Hit, right: Hit) => number;
|
|
34
37
|
export declare function fuse(lists: Hit[][], limit?: number, k?: number): Hit[];
|
|
35
38
|
export type Coverage = {
|
|
36
39
|
total: number;
|
|
@@ -53,6 +56,7 @@ export declare class Retriever {
|
|
|
53
56
|
private variant;
|
|
54
57
|
pageIntroduction(docId: string, options?: LexicalOptions): Chunk | undefined;
|
|
55
58
|
lexical(query: string, options?: LexicalOptions): Hit[];
|
|
59
|
+
searchPassages(query: string, queryVector: Float32Array | undefined, options?: LexicalOptions): Iterable<Hit>;
|
|
56
60
|
vector(queryVector: Float32Array, options?: LexicalOptions): Hit[];
|
|
57
61
|
hasVectors(options?: LexicalOptions): boolean;
|
|
58
62
|
hybrid(query: string, queryVector: Float32Array | undefined, options?: LexicalOptions): Hit[];
|
package/dist/retrieval.js
CHANGED
|
@@ -67,10 +67,12 @@ export class LexicalIndex {
|
|
|
67
67
|
this.average = this.entries.reduce((sum, entry) => sum + entry.length, 0) / (this.entries.length || 1);
|
|
68
68
|
}
|
|
69
69
|
search(query, limit = 8, keep) {
|
|
70
|
+
return [...this.scan(query, keep)].sort(compareHits).slice(0, limit);
|
|
71
|
+
}
|
|
72
|
+
*scan(query, keep) {
|
|
70
73
|
const terms = [...new Set(tokenize(query))];
|
|
71
74
|
const units = queryUnits(query, this.synonyms).filter(unit => unit.alternatives.length);
|
|
72
75
|
const total = this.entries.length;
|
|
73
|
-
const hits = [];
|
|
74
76
|
for (const entry of this.entries) {
|
|
75
77
|
if (keep && !keep(entry.chunk))
|
|
76
78
|
continue;
|
|
@@ -93,9 +95,8 @@ export class LexicalIndex {
|
|
|
93
95
|
}
|
|
94
96
|
}
|
|
95
97
|
if (score > 0)
|
|
96
|
-
|
|
98
|
+
yield { chunk: entry.chunk, score };
|
|
97
99
|
}
|
|
98
|
-
return hits.sort((left, right) => right.score - left.score || left.chunk.id.localeCompare(right.chunk.id)).slice(0, limit);
|
|
99
100
|
}
|
|
100
101
|
}
|
|
101
102
|
function unit(vector) {
|
|
@@ -113,8 +114,10 @@ export class VectorIndex {
|
|
|
113
114
|
this.rows = rows;
|
|
114
115
|
}
|
|
115
116
|
search(query, limit = 8, keep) {
|
|
117
|
+
return [...this.scan(query, keep)].sort(compareHits).slice(0, limit);
|
|
118
|
+
}
|
|
119
|
+
*scan(query, keep) {
|
|
116
120
|
const q = unit(query);
|
|
117
|
-
const hits = [];
|
|
118
121
|
for (const row of this.rows) {
|
|
119
122
|
if (keep && !keep(row.chunk))
|
|
120
123
|
continue;
|
|
@@ -123,11 +126,11 @@ export class VectorIndex {
|
|
|
123
126
|
let score = 0;
|
|
124
127
|
for (let at = 0; at < q.length; at++)
|
|
125
128
|
score += q[at] * row.vector[at];
|
|
126
|
-
|
|
129
|
+
yield { chunk: row.chunk, score };
|
|
127
130
|
}
|
|
128
|
-
return hits.sort((left, right) => right.score - left.score || left.chunk.id.localeCompare(right.chunk.id)).slice(0, limit);
|
|
129
131
|
}
|
|
130
132
|
}
|
|
133
|
+
export const compareHits = (left, right) => right.score - left.score || left.chunk.id.localeCompare(right.chunk.id);
|
|
131
134
|
// Reciprocal rank fusion. Lexical and vector scores are not comparable, so only ranks are combined.
|
|
132
135
|
export function fuse(lists, limit = 8, k = 60) {
|
|
133
136
|
const scores = new Map();
|
|
@@ -149,10 +152,16 @@ export class Retriever {
|
|
|
149
152
|
constructor(index, embeddings) {
|
|
150
153
|
if (index?.schemaVersion !== 1 || !Array.isArray(index.chunks))
|
|
151
154
|
throw new Error('Unsupported or invalid chunks.json');
|
|
155
|
+
if (new Set(index.chunks.map(chunk => chunk.id)).size !== index.chunks.length)
|
|
156
|
+
throw new Error('Ambiguous passage IDs in chunks.json; run shoalrun index <site-directory> to rebuild publication-qualified IDs. Existing passage embeddings can be reused.');
|
|
152
157
|
const synonyms = validateSearchSynonyms(index.synonyms);
|
|
153
158
|
const groups = new Map();
|
|
154
|
-
for (const chunk of index.chunks)
|
|
155
|
-
|
|
159
|
+
for (const chunk of index.chunks) {
|
|
160
|
+
let passages = groups.get(chunk.variant);
|
|
161
|
+
if (!passages)
|
|
162
|
+
groups.set(chunk.variant, passages = []);
|
|
163
|
+
passages.push(chunk);
|
|
164
|
+
}
|
|
156
165
|
this.counts = { total: index.chunks.length, embedded: 0, stale: 0, ...(embeddings ? { model: embeddings.meta.model, dimension: embeddings.meta.dimension } : {}) };
|
|
157
166
|
const rowOf = new Map((embeddings?.meta.ids ?? []).map((id, row) => [id, row]));
|
|
158
167
|
for (const chunk of index.chunks)
|
|
@@ -209,6 +218,17 @@ export class Retriever {
|
|
|
209
218
|
lexical(query, options = {}) {
|
|
210
219
|
return this.byVariant.get(this.variant(options)).search(query, options.limit, keeping(options));
|
|
211
220
|
}
|
|
221
|
+
// File-backed search still scans its selected publication, but streams scores
|
|
222
|
+
// into page selection instead of sorting/materializing every passage first.
|
|
223
|
+
searchPassages(query, queryVector, options = {}) {
|
|
224
|
+
const variant = this.variant(options);
|
|
225
|
+
if (!queryVector)
|
|
226
|
+
return this.byVariant.get(variant).scan(query, keeping(options));
|
|
227
|
+
const index = this.vectors.get(variant);
|
|
228
|
+
if (!index)
|
|
229
|
+
throw new Error('No current embeddings for this publication; run shoalrun index --embed');
|
|
230
|
+
return index.scan(queryVector, keeping(options));
|
|
231
|
+
}
|
|
212
232
|
vector(queryVector, options = {}) {
|
|
213
233
|
const index = this.vectors.get(this.variant(options));
|
|
214
234
|
if (!index)
|
package/dist/search-handler.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import { compareHits } from './retrieval.js';
|
|
1
2
|
import { validateReaderSelection } from './reader-filters.js';
|
|
2
3
|
import { searchSnippetFromParagraphs } from './search-snippet.js';
|
|
3
|
-
import {
|
|
4
|
+
import { identifierMatch } from './search-identifier.js';
|
|
4
5
|
import { createSearchResultSelector } from './search-results.js';
|
|
5
6
|
export function createSearchHandler(options) {
|
|
6
7
|
const selectResults = createSearchResultSelector(options.retriever);
|
|
@@ -91,17 +92,16 @@ export function createSearchHandler(options) {
|
|
|
91
92
|
}
|
|
92
93
|
if (!vector && fallbackReason)
|
|
93
94
|
options.onFallback?.(fallbackReason);
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
const lexical = options.retriever.lexical(query.trim(), retrievalOptions);
|
|
102
|
-
hits = identifierRescue(query.trim(), hits, lexical);
|
|
95
|
+
let candidate;
|
|
96
|
+
const matchesIdentifier = identifierMatch(query);
|
|
97
|
+
if (vector && limit >= 3 && matchesIdentifier) {
|
|
98
|
+
for (const hit of options.retriever.searchPassages(query.trim(), undefined, searchOptions)) {
|
|
99
|
+
if (matchesIdentifier(hit) && (!candidate || compareHits(hit, candidate) < 0))
|
|
100
|
+
candidate = hit;
|
|
101
|
+
}
|
|
103
102
|
}
|
|
104
|
-
const
|
|
103
|
+
const hits = selectResults(options.retriever.searchPassages(query.trim(), vector, searchOptions), query.trim(), searchOptions, candidate);
|
|
104
|
+
const results = hits.map(({ chunk, literalSummary }) => ({ url: chunk.url, title: chunk.title, section: chunk.headingPath.at(-1), summary: literalSummary ?? searchSnippetFromParagraphs(chunk.search.paragraphs, query.trim(), options.pageSummaries?.get(`${chunk.variant}:${chunk.docId}`)) }));
|
|
105
105
|
return json(200, { results, mode: vector ? 'vector' : 'lexical', ...(!vector ? { fallbackReason } : {}) });
|
|
106
106
|
}
|
|
107
107
|
catch {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
import type { Hit } from './retrieval.js';
|
|
2
2
|
export declare function searchIdentifiers(query: string): string[];
|
|
3
|
-
export declare function identifierRescue(query: string, semantic:
|
|
3
|
+
export declare function identifierRescue<T extends Hit>(query: string, semantic: T[], lexical: T[]): T[];
|
|
4
|
+
export declare function identifierMatch(query: string): ((hit: Hit) => boolean) | undefined;
|
|
@@ -7,11 +7,10 @@ export function searchIdentifiers(query) {
|
|
|
7
7
|
return [...new Set(identifiers.filter(id => !/\.(?:txt|json|ya?ml|mdx?|[cm]?js|tsx?|html?|css|svg|png|jpe?g|com|org|net)$/i.test(id)))];
|
|
8
8
|
}
|
|
9
9
|
export function identifierRescue(query, semantic, lexical) {
|
|
10
|
-
const
|
|
11
|
-
if (!
|
|
10
|
+
const matches = identifierMatch(query);
|
|
11
|
+
if (!matches)
|
|
12
12
|
return semantic;
|
|
13
|
-
const
|
|
14
|
-
const candidate = lexical.find(hit => (hit.chunk.search?.paragraphs ?? searchParagraphs(hit.chunk.text)).some(p => matches.every(re => re.test(p))));
|
|
13
|
+
const candidate = lexical.find(matches);
|
|
15
14
|
if (!candidate)
|
|
16
15
|
return semantic;
|
|
17
16
|
const pages = [...new Set(semantic.map(h => h.chunk.docId))];
|
|
@@ -23,3 +22,10 @@ export function identifierRescue(query, semantic, lexical) {
|
|
|
23
22
|
const position = at < 0 ? remaining.length : at;
|
|
24
23
|
return [...remaining.slice(0, position), candidate, ...remaining.slice(position)];
|
|
25
24
|
}
|
|
25
|
+
export function identifierMatch(query) {
|
|
26
|
+
const identifiers = searchIdentifiers(query);
|
|
27
|
+
if (!identifiers.length)
|
|
28
|
+
return undefined;
|
|
29
|
+
const matches = identifiers.map(id => new RegExp(`(?<![\\w$.-])${id.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(?![\\w$-]|\\.[\\w$])`, 'i'));
|
|
30
|
+
return hit => (hit.chunk.search?.paragraphs ?? searchParagraphs(hit.chunk.text)).some(p => matches.every(re => re.test(p)));
|
|
31
|
+
}
|
package/dist/search-results.d.ts
CHANGED
|
@@ -2,5 +2,5 @@ import type { Hit, LexicalOptions, Retriever } from './retrieval.js';
|
|
|
2
2
|
type SearchHit = Hit & {
|
|
3
3
|
literalSummary?: string;
|
|
4
4
|
};
|
|
5
|
-
export declare function createSearchResultSelector(retriever: Retriever): (hits: Hit
|
|
5
|
+
export declare function createSearchResultSelector(retriever: Retriever): (hits: Iterable<Hit>, query: string, options: LexicalOptions, rescue?: Hit) => SearchHit[];
|
|
6
6
|
export {};
|
package/dist/search-results.js
CHANGED
|
@@ -1,45 +1,59 @@
|
|
|
1
|
-
import { tokenize } from './retrieval.js';
|
|
1
|
+
import { compareHits, tokenize } from './retrieval.js';
|
|
2
2
|
import { literalSearch } from './search-literal.js';
|
|
3
|
+
import { identifierRescue } from './search-identifier.js';
|
|
3
4
|
// Select landing passages for the search box, without changing passage retrieval
|
|
4
5
|
// used by Ask. A gallery's repeated examples must not crowd other pages out.
|
|
5
6
|
export function createSearchResultSelector(retriever) {
|
|
6
7
|
retriever.requirePreparedSearch();
|
|
7
8
|
const normalized = (text) => tokenize(text).join(' ');
|
|
8
|
-
return (hits, query, options) => {
|
|
9
|
+
return (hits, query, options, rescue) => {
|
|
9
10
|
const pages = new Map();
|
|
11
|
+
const normalizedQuery = normalized(query);
|
|
10
12
|
const literal = literalSearch(query);
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
if (literalPages.has(hit.chunk.docId))
|
|
15
|
-
continue;
|
|
16
|
-
const literalSummary = literal(hit.chunk.search.literalLines);
|
|
17
|
-
if (literalSummary)
|
|
18
|
-
literalPages.set(hit.chunk.docId, { ...hit, literalSummary });
|
|
19
|
-
}
|
|
20
|
-
}
|
|
13
|
+
let exact;
|
|
14
|
+
// Retain at most three candidates per page, independent of its passage
|
|
15
|
+
// count. Input scores need not be sorted or collected into an array.
|
|
21
16
|
for (const hit of hits) {
|
|
22
|
-
|
|
17
|
+
const page = pages.get(hit.chunk.docId) ?? { exactTitle: normalized(hit.chunk.title) === normalizedQuery };
|
|
18
|
+
if (!page.first || compareHits(hit, page.first) < 0)
|
|
19
|
+
page.first = hit;
|
|
20
|
+
if (hit.chunk.search.paragraphs.length && (!page.prose || compareHits(hit, page.prose) < 0))
|
|
21
|
+
page.prose = hit;
|
|
22
|
+
const literalSummary = literal?.(hit.chunk.search.literalLines);
|
|
23
|
+
if (literalSummary && (!page.literal || compareHits(hit, page.literal) < 0))
|
|
24
|
+
page.literal = { ...hit, literalSummary };
|
|
25
|
+
pages.set(hit.chunk.docId, page);
|
|
26
|
+
if (page.exactTitle && (!exact || compareHits(hit, exact) < 0))
|
|
27
|
+
exact = hit;
|
|
28
|
+
}
|
|
29
|
+
const limit = options.limit ?? 8;
|
|
30
|
+
const candidates = [];
|
|
31
|
+
for (const page of pages.values()) {
|
|
32
|
+
const candidate = page.literal ? { rank: page.first, hit: { ...page.literal, score: page.first.score } } : page.prose ? { rank: page.prose, hit: page.prose } : undefined;
|
|
33
|
+
if (!candidate)
|
|
23
34
|
continue;
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
35
|
+
const at = candidates.findIndex(other => compareHits(candidate.rank, other.rank) < 0);
|
|
36
|
+
candidates.splice(at < 0 ? candidates.length : at, 0, candidate);
|
|
37
|
+
if (candidates.length > limit)
|
|
38
|
+
candidates.pop();
|
|
39
|
+
}
|
|
40
|
+
let selected = candidates.map(item => item.hit);
|
|
41
|
+
if (rescue) {
|
|
42
|
+
selected = identifierRescue(query, selected, [rescue]);
|
|
43
|
+
const landing = pages.get(rescue.chunk.docId)?.literal;
|
|
44
|
+
if (landing)
|
|
45
|
+
selected = selected.map(hit => hit.chunk.docId === rescue.chunk.docId ? { ...landing, score: hit.score } : hit);
|
|
31
46
|
}
|
|
32
47
|
// An exact page-title query is a request for that topic's entry point.
|
|
33
48
|
// Ordinary semantic queries retain their ranking; incidental words do not qualify.
|
|
34
|
-
const exact = hits.find(hit => normalized(hit.chunk.title) === normalized(query));
|
|
35
49
|
if (exact) {
|
|
36
50
|
const introduction = retriever.pageIntroduction(exact.chunk.docId, options);
|
|
37
|
-
const
|
|
51
|
+
const page = pages.get(exact.chunk.docId);
|
|
52
|
+
const landing = introduction ? { chunk: introduction, score: exact.score } : page?.literal ?? page?.prose;
|
|
38
53
|
if (landing) {
|
|
39
|
-
|
|
40
|
-
return [landing, ...pages.values()].slice(0, options.limit ?? 8);
|
|
54
|
+
return [landing, ...selected.filter(hit => hit.chunk.docId !== exact.chunk.docId)].slice(0, options.limit ?? 8);
|
|
41
55
|
}
|
|
42
56
|
}
|
|
43
|
-
return
|
|
57
|
+
return selected.slice(0, limit);
|
|
44
58
|
};
|
|
45
59
|
}
|