@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.
Files changed (99) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/DEPLOYMENT-PROFILES.md +287 -0
  3. package/README.md +43 -2
  4. package/agent-docs/.well-known/shoalrun/navigation/project-updates-50f26fe22670.txt +5 -1
  5. package/agent-docs/SNAPSHOT.json +5 -5
  6. package/agent-docs/changelog/2026-10-30.md +43 -0
  7. package/agent-docs/changelog/2026-11-03.md +56 -0
  8. package/agent-docs/changelog.md +17 -1
  9. package/agent-docs/documents.json +419 -61
  10. package/agent-docs/index.md +73 -103
  11. package/agent-docs/provenance.json +4 -4
  12. package/agent-docs/publish/content-export.md +1 -1
  13. package/agent-docs/publish/content-export.sections/prerequisites.md +11 -0
  14. package/agent-docs/publish/deployment-profiles.md +273 -0
  15. package/agent-docs/publish/deployment-profiles.sections/before-you-begin.md +44 -0
  16. package/agent-docs/publish/deployment-profiles.sections/build-and-inspect-the-edition.md +69 -0
  17. package/agent-docs/publish/deployment-profiles.sections/check-the-result.md +46 -0
  18. package/agent-docs/publish/deployment-profiles.sections/choose-navigation-labels.md +53 -0
  19. package/agent-docs/publish/deployment-profiles.sections/current-limits.md +46 -0
  20. package/agent-docs/publish/deployment-profiles.sections/define-the-shared-sku-manifest.md +70 -0
  21. package/agent-docs/publish/deployment-profiles.sections/html-markdown-and-agents.md +44 -0
  22. package/agent-docs/publish/deployment-profiles.sections/mark-pages-and-shared-sections.md +135 -0
  23. package/agent-docs/publish/deployment-profiles.sections/related-reading.md +42 -0
  24. package/agent-docs/publish/deployment-profiles.sections/supply-the-profile-from-the-deployment.md +68 -0
  25. package/agent-docs/search-index.json +1 -1
  26. package/agent-docs/start.md +2 -2
  27. package/agent-docs/start.sections/add-the-page-to-navigation.md +1 -1
  28. package/agent-docs/start.sections/build-and-check-both-outputs.md +1 -1
  29. package/agent-docs/start.sections/build-this-site.md +1 -1
  30. package/agent-docs/start.sections/check-writing-style.md +1 -1
  31. package/agent-docs/start.sections/create-a-page.md +1 -1
  32. package/agent-docs/start.sections/make-it-yours.md +1 -1
  33. package/agent-docs/start.sections/migrating-from-github-packages.md +1 -1
  34. package/agent-docs/start.sections/prerequisites.md +1 -1
  35. package/agent-docs/start.sections/related-reading.md +1 -1
  36. package/agent-docs/start.sections/start-your-own-project.md +2 -2
  37. package/assets/search-semantic.js +34 -0
  38. package/dist/api-publication-plan.d.ts +3 -0
  39. package/dist/api-publication-plan.js +30 -3
  40. package/dist/artifact-bundle.d.ts +2 -1
  41. package/dist/artifact-bundle.js +2 -2
  42. package/dist/authoring-api.d.ts +1 -1
  43. package/dist/authoring-api.js +1 -1
  44. package/dist/authoring-project-command.js +4 -2
  45. package/dist/authoring-project.d.ts +2 -0
  46. package/dist/authoring-project.js +4 -4
  47. package/dist/authoring-snapshot.d.ts +23 -4
  48. package/dist/authoring-snapshot.js +84 -12
  49. package/dist/authoring-validation-command.js +5 -3
  50. package/dist/authoring-validation.d.ts +2 -0
  51. package/dist/authoring-validation.js +38 -11
  52. package/dist/cli-discovery.d.ts +2 -2
  53. package/dist/cli-discovery.js +9 -9
  54. package/dist/cli.js +14 -8
  55. package/dist/compiler.d.ts +13 -0
  56. package/dist/compiler.js +27 -74
  57. package/dist/component-names.js +1 -1
  58. package/dist/component-transform.js +7 -1
  59. package/dist/config.d.ts +4 -1
  60. package/dist/config.js +19 -5
  61. package/dist/content-export-command.d.ts +13 -1
  62. package/dist/content-export-command.js +3 -2
  63. package/dist/content-export.d.ts +13 -0
  64. package/dist/content-export.js +5 -4
  65. package/dist/content-source.d.ts +1 -0
  66. package/dist/content-workspace.d.ts +13 -0
  67. package/dist/content-workspace.js +18 -1
  68. package/dist/deployment-profile.d.ts +52 -0
  69. package/dist/deployment-profile.js +277 -0
  70. package/dist/frontmatter.js +11 -4
  71. package/dist/index-build.js +7 -3
  72. package/dist/page-materialization.js +2 -0
  73. package/dist/page-renderer.d.ts +1 -0
  74. package/dist/page-renderer.js +3 -1
  75. package/dist/palettes.d.ts +1 -1
  76. package/dist/project-navigation.d.ts +5 -0
  77. package/dist/project-navigation.js +19 -0
  78. package/dist/publication-plan.d.ts +3 -1
  79. package/dist/publication-plan.js +23 -4
  80. package/dist/publication-render.d.ts +3 -2
  81. package/dist/publication-render.js +2 -2
  82. package/dist/retrieval.d.ts +4 -0
  83. package/dist/retrieval.js +28 -8
  84. package/dist/search-handler.js +11 -11
  85. package/dist/search-identifier.d.ts +2 -1
  86. package/dist/search-identifier.js +10 -4
  87. package/dist/search-results.d.ts +1 -1
  88. package/dist/search-results.js +39 -25
  89. package/dist/site-identity.d.ts +3 -0
  90. package/dist/site-identity.js +69 -0
  91. package/dist/vercel.d.ts +1 -0
  92. package/dist/vercel.js +3 -1
  93. package/package.json +2 -1
  94. package/skills/shoalrun-author/SKILL.md +5 -1
  95. package/skills/shoalrun-author/references/customer-editions.md +164 -0
  96. package/skills/shoalrun-recipes/SKILL.md +2 -0
  97. package/skills/shoalrun-setup/SKILL.md +4 -0
  98. package/styles/site.css +2 -0
  99. package/agent-docs/publish/content-export.sections/upgrade-an-existing-integration.md +0 -34
@@ -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
- const base = `${document.id}#${slug(headingPath.join(' ')) || 'intro'}${part ? `~${part + 1}` : ''}`;
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
- const source = manifest.documents.map(document => `${document.id}:${document.digest}`).sort().join('\n');
223
- const index = { schemaVersion: 1, sourceDigest: sha(source + (synonyms.length ? `\n${JSON.stringify(synonyms)}` : '')), chunks, ...(synonyms.length ? { synonyms } : {}) };
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}`);
@@ -42,6 +42,7 @@ export type PageShell = {
42
42
  generated: boolean;
43
43
  };
44
44
  favicon?: string;
45
+ logo?: string;
45
46
  touchIcon?: boolean;
46
47
  origin?: string;
47
48
  indexing?: boolean;
@@ -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="brand" href="/">${esc(siteTitle)}</a>` : `<span class="brand">${esc(siteTitle)}</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>`;
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
  }
@@ -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-warning-line" | "callout-note-line" | "callout-tip-line" | "callout-important-line" | "callout-caution-line" | "callout-warning-bg" | "callout-note-bg" | "callout-tip-bg" | "callout-important-bg" | "callout-caution-bg" | "callout-warning-text" | "callout-note-text" | "callout-tip-text" | "callout-important-text" | "callout-caution-text")[], "strong-surface", "strong-text"];
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
  };
@@ -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
- if (frontmatter.access?.policy !== 'public' && !authoringFiles?.has(file))
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
- docs.set(key, { file, frontmatter: publishedFrontmatter, body, tree: null, variant, route, markdownRoute: markdownRoute(route, basePath), readerFilters: marks });
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
  }
@@ -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
- hits.push({ chunk: entry.chunk, score });
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
- hits.push({ chunk: row.chunk, score });
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
- groups.set(chunk.variant, [...(groups.get(chunk.variant) ?? []), chunk]);
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)
@@ -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 { identifierRescue, searchIdentifiers } from './search-identifier.js';
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
- // Rank pages after inspecting their passages. A large gallery can otherwise
95
- // consume the entire candidate window before page deduplication happens.
96
- const retrievalOptions = { ...searchOptions, limit: Number.MAX_SAFE_INTEGER };
97
- let hits = vector
98
- ? options.retriever.vector(vector, retrievalOptions)
99
- : options.retriever.lexical(query.trim(), retrievalOptions);
100
- if (vector && limit >= 3 && searchIdentifiers(query).length) {
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 results = selectResults(hits, query.trim(), searchOptions).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}`)) }));
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: Hit[], lexical: Hit[]): Hit[];
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 identifiers = searchIdentifiers(query);
11
- if (!identifiers.length)
10
+ const matches = identifierMatch(query);
11
+ if (!matches)
12
12
  return semantic;
13
- const matches = identifiers.map(id => new RegExp(`(?<![\\w$.-])${id.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(?![\\w$-]|\\.[\\w$])`, 'i'));
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
+ }
@@ -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[], query: string, options: LexicalOptions) => SearchHit[];
5
+ export declare function createSearchResultSelector(retriever: Retriever): (hits: Iterable<Hit>, query: string, options: LexicalOptions, rescue?: Hit) => SearchHit[];
6
6
  export {};
@@ -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
- const literalPages = new Map();
12
- if (literal) {
13
- for (const hit of hits) {
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
- if (pages.has(hit.chunk.docId))
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
- // Keep the page's retrieved position, but land on the passage that actually
25
- // contains the literal. A nearby token table must not hide the correct row.
26
- const exactPassage = literalPages.get(hit.chunk.docId);
27
- if (exactPassage)
28
- pages.set(hit.chunk.docId, { ...exactPassage, score: hit.score });
29
- else if (hit.chunk.search.paragraphs.length)
30
- pages.set(hit.chunk.docId, hit);
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 landing = introduction ? { chunk: introduction, score: exact.score } : pages.get(exact.chunk.docId);
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
- pages.delete(exact.chunk.docId);
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 [...pages.values()].slice(0, options.limit ?? 8);
57
+ return selected.slice(0, limit);
44
58
  };
45
59
  }
@@ -0,0 +1,3 @@
1
+ import type { HeaderConfig, FooterConfig } from './content.js';
2
+ export declare function validateHeader(value: unknown): HeaderConfig;
3
+ export declare function validateFooter(value: unknown): FooterConfig | undefined;