@shoalrun/cli 0.1.0-alpha.15 → 0.1.0-alpha.17

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 (113) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/README.md +41 -3
  3. package/agent-docs/SNAPSHOT.json +5 -5
  4. package/agent-docs/author/frontmatter-reference.md +15 -11
  5. package/agent-docs/author/frontmatter-reference.sections/choose-a-publication-and-visibility.md +13 -11
  6. package/agent-docs/author/frontmatter-reference.sections/control-output-details.md +1 -1
  7. package/agent-docs/author/frontmatter-reference.sections/global-defaults-and-enforced-values.md +1 -1
  8. package/agent-docs/author/frontmatter-reference.sections/html-markdown-and-agents.md +1 -1
  9. package/agent-docs/author/frontmatter-reference.sections/identify-and-route-a-page.md +1 -1
  10. package/agent-docs/author/frontmatter-reference.sections/point-agents-to-it.md +1 -1
  11. package/agent-docs/author/frontmatter-reference.sections/project-defined-fields.md +1 -1
  12. package/agent-docs/author/frontmatter-reference.sections/related-reading.md +1 -1
  13. package/agent-docs/author/frontmatter-reference.sections/use-the-local-files-while-authoring.md +1 -1
  14. package/agent-docs/author/frontmatter-reference.sections/when-the-files-update.md +1 -1
  15. package/agent-docs/author/recipe-drafts.md +2 -2
  16. package/agent-docs/author/recipe-drafts.sections/1-write-one-reusable-step.md +1 -1
  17. package/agent-docs/author/recipe-drafts.sections/2-list-the-steps-for-one-complete-goal.md +1 -1
  18. package/agent-docs/author/recipe-drafts.sections/3-generate-a-draft-and-review-it.md +1 -1
  19. package/agent-docs/author/recipe-drafts.sections/4-maintain-shoalrun-s-published-recipe-snapshots.md +1 -1
  20. package/agent-docs/author/recipe-drafts.sections/related-reading.md +1 -1
  21. package/agent-docs/author/recipe-drafts.sections/start-an-ingredient-from-an-api-example.md +2 -2
  22. package/agent-docs/author/recipes.md +3 -3
  23. package/agent-docs/author/recipes.sections/how-agents-use-this.md +2 -2
  24. package/agent-docs/author/recipes.sections/one-example-from-shoalrun.md +2 -2
  25. package/agent-docs/author/recipes.sections/related-reading.md +2 -2
  26. package/agent-docs/author/recipes.sections/try-a-complete-workflow.md +3 -3
  27. package/agent-docs/author/recipes.sections/use-recipes-for-task-oriented-how-tos.md +2 -2
  28. package/agent-docs/author/recipes.sections/what-changing-an-ingredient-updates.md +2 -2
  29. package/agent-docs/author/recipes.sections/what-each-file-does.md +2 -2
  30. package/agent-docs/author/recipes.sections/what-happens-and-when.md +2 -2
  31. package/agent-docs/author/search.md +55 -6
  32. package/agent-docs/author/search.sections/agent-and-mcp-search-scope.md +48 -0
  33. package/agent-docs/author/search.sections/choose-a-provider.md +7 -6
  34. package/agent-docs/author/search.sections/cli-and-mcp-ranking-details.md +1 -1
  35. package/agent-docs/author/search.sections/enable-included-semantic-search.md +66 -0
  36. package/agent-docs/author/search.sections/hosted-semantic-search-limits.md +1 -1
  37. package/agent-docs/author/search.sections/html-markdown-and-agents.md +1 -1
  38. package/agent-docs/author/search.sections/lexical-search.md +1 -1
  39. package/agent-docs/author/search.sections/local-semantic-preview.md +1 -1
  40. package/agent-docs/author/search.sections/read-a-search-result-with-an-agent.md +1 -1
  41. package/agent-docs/author/search.sections/related-reading.md +1 -1
  42. package/agent-docs/author/search.sections/reuse-the-lexical-index.md +1 -1
  43. package/agent-docs/author/search.sections/turn-search-off.md +1 -1
  44. package/agent-docs/author/search.sections/use-the-search-box.md +1 -1
  45. package/agent-docs/author/search.sections/write-a-custom-provider.md +1 -1
  46. package/agent-docs/chat/passage-index.md +16 -12
  47. package/agent-docs/chat/passage-index.sections/build-the-index.md +3 -3
  48. package/agent-docs/chat/passage-index.sections/embed-passages.md +3 -3
  49. package/agent-docs/chat/passage-index.sections/how-the-default-chunker-splits-a-page.md +3 -3
  50. package/agent-docs/chat/passage-index.sections/limits.md +3 -3
  51. package/agent-docs/chat/passage-index.sections/passage-ids-and-links.md +14 -10
  52. package/agent-docs/chat/passage-index.sections/related-reading.md +3 -3
  53. package/agent-docs/chat/passage-index.sections/what-a-passage-contains.md +5 -5
  54. package/agent-docs/cookbooks/welcome-email.md +12 -9
  55. package/agent-docs/documents.json +207 -183
  56. package/agent-docs/provenance.json +4 -4
  57. package/agent-docs/recipes/queue-email.md +1 -1
  58. package/agent-docs/recipes/send-welcome-email.md +3 -3
  59. package/agent-docs/search-index.json +1 -1
  60. package/agent-docs/start.md +2 -2
  61. package/agent-docs/start.sections/add-the-page-to-navigation.md +1 -1
  62. package/agent-docs/start.sections/build-and-check-both-outputs.md +1 -1
  63. package/agent-docs/start.sections/build-this-site.md +1 -1
  64. package/agent-docs/start.sections/check-writing-style.md +1 -1
  65. package/agent-docs/start.sections/create-a-page.md +1 -1
  66. package/agent-docs/start.sections/make-it-yours.md +1 -1
  67. package/agent-docs/start.sections/migrating-from-github-packages.md +1 -1
  68. package/agent-docs/start.sections/prerequisites.md +1 -1
  69. package/agent-docs/start.sections/related-reading.md +1 -1
  70. package/agent-docs/start.sections/start-your-own-project.md +2 -2
  71. package/assets/search-semantic.js +34 -0
  72. package/dist/ai-directory.d.ts +1 -0
  73. package/dist/ai-directory.js +12 -0
  74. package/dist/answer-cards.js +1 -1
  75. package/dist/artifact-bundle.d.ts +1 -1
  76. package/dist/artifact-bundle.js +1 -1
  77. package/dist/ask-function.d.ts +4 -18
  78. package/dist/ask-function.js +12 -71
  79. package/dist/ask-runtime.d.ts +45 -0
  80. package/dist/ask-runtime.js +96 -0
  81. package/dist/ask-setup.d.ts +3 -42
  82. package/dist/ask-setup.js +5 -93
  83. package/dist/cli.js +6 -2
  84. package/dist/compiler.js +2 -0
  85. package/dist/embeddings.d.ts +1 -1
  86. package/dist/embeddings.js +1 -1
  87. package/dist/function-package.js +5 -0
  88. package/dist/function-runtime.d.ts +23 -0
  89. package/dist/function-runtime.js +45 -0
  90. package/dist/index-build.d.ts +1 -2
  91. package/dist/index-build.js +10 -14
  92. package/dist/publication-render.d.ts +1 -1
  93. package/dist/retrieval-runtime.d.ts +5 -0
  94. package/dist/retrieval-runtime.js +21 -0
  95. package/dist/retrieval.d.ts +4 -0
  96. package/dist/retrieval.js +28 -8
  97. package/dist/search-function.d.ts +2 -0
  98. package/dist/search-function.js +30 -0
  99. package/dist/search-handler.js +12 -12
  100. package/dist/search-identifier-core.d.ts +4 -0
  101. package/dist/search-identifier-core.js +30 -0
  102. package/dist/search-identifier.d.ts +3 -2
  103. package/dist/search-identifier.js +7 -24
  104. package/dist/search-literal.js +1 -1
  105. package/dist/search-results.d.ts +1 -1
  106. package/dist/search-results.js +39 -25
  107. package/dist/search-snippet.d.ts +1 -1
  108. package/dist/search-snippet.js +2 -14
  109. package/dist/search-text.d.ts +1 -0
  110. package/dist/search-text.js +14 -0
  111. package/dist/vercel-function.js +10 -2
  112. package/package.json +1 -1
  113. package/skills/shoalrun-setup/SKILL.md +2 -0
@@ -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-core.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
  }
@@ -1,3 +1,4 @@
1
+ export { searchSnippetFromParagraphs } from './search-text.js';
1
2
  export declare function searchParagraphs(markdown: string): string[];
2
3
  export declare function searchPassageParagraphs(markdown: string): string[];
3
4
  export declare function searchLiteralLines(markdown: string): string[];
@@ -8,4 +9,3 @@ export type PreparedSearchPassage = {
8
9
  };
9
10
  export declare function prepareSearchPassage(markdown: string): PreparedSearchPassage;
10
11
  export declare function searchSnippet(markdown: string, query: string, pageSummary?: string): string;
11
- export declare function searchSnippetFromParagraphs(choices: string[], query: string, pageSummary?: string): string;
@@ -1,3 +1,5 @@
1
+ import { searchSnippetFromParagraphs } from './search-text.js';
2
+ export { searchSnippetFromParagraphs } from './search-text.js';
1
3
  import { unified } from 'unified';
2
4
  import remarkParse from 'remark-parse';
3
5
  import remarkGfm from 'remark-gfm';
@@ -71,23 +73,9 @@ export function prepareSearchPassage(markdown) {
71
73
  const tree = parser.parse(markdown);
72
74
  return { schemaVersion: 1, paragraphs: passageParagraphs(tree), literalLines: cleanLines(literalLines(tree)) };
73
75
  }
74
- const stopWords = new Set('a an the i me my we our you your how do does can could would should will is are was were be to of in on for with please what where when why which and or it this that have has after before'.split(' '));
75
76
  // Prefer substantive query matches; Markdown boilerplate and common question words
76
77
  // must not hide a useful paragraph. Keep a matching term visible in long paragraphs.
77
78
  export function searchSnippet(markdown, query, pageSummary = '') {
78
79
  const choices = searchParagraphs(markdown).filter(p => !discovery.test(p));
79
80
  return searchSnippetFromParagraphs(choices, query, pageSummary);
80
81
  }
81
- export function searchSnippetFromParagraphs(choices, query, pageSummary = '') {
82
- const terms = [...new Set((query.toLocaleLowerCase().match(/[\p{L}\p{N}_$.-]+/gu) ?? []).map(t => t.replace(/^[.-]+|[.-]+$/g, '')).filter(t => t && !stopWords.has(t)))];
83
- const matches = (text) => terms.filter(term => new RegExp(`(?<![\\p{L}\\p{N}_])${term.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(?![\\p{L}\\p{N}_])`, 'iu').test(text));
84
- const ranked = choices.map((text, at) => ({ text, at, terms: matches(text) })).sort((a, b) => b.terms.length - a.terms.length || a.at - b.at);
85
- const selected = ranked[0];
86
- const text = (selected?.text ?? pageSummary).replace(/\s+/g, ' ').trim();
87
- if (text.length <= 180)
88
- return text;
89
- const first = selected?.terms.length ? Math.min(...selected.terms.map(t => text.toLocaleLowerCase().indexOf(t))) : 0;
90
- const start = Math.max(0, first - 50);
91
- const length = 180 - (start ? 1 : 0) - 1;
92
- return `${start ? '…' : ''}${text.slice(start, start + length).trim()}…`;
93
- }
@@ -0,0 +1 @@
1
+ export declare function searchSnippetFromParagraphs(choices: string[], query: string, pageSummary?: string): string;
@@ -0,0 +1,14 @@
1
+ const stopWords = new Set('a an the i me my we our you your how do does can could would should will is are was were be to of in on for with please what where when why which and or it this that have has after before'.split(' '));
2
+ export function searchSnippetFromParagraphs(choices, query, pageSummary = '') {
3
+ const terms = [...new Set((query.toLocaleLowerCase().match(/[\p{L}\p{N}_$.-]+/gu) ?? []).map(t => t.replace(/^[.-]+|[.-]+$/g, '')).filter(t => t && !stopWords.has(t)))];
4
+ const matches = (text) => terms.filter(term => new RegExp(`(?<![\\p{L}\\p{N}_])${term.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(?![\\p{L}\\p{N}_])`, 'iu').test(text));
5
+ const ranked = choices.map((text, at) => ({ text, at, terms: matches(text) })).sort((a, b) => b.terms.length - a.terms.length || a.at - b.at);
6
+ const selected = ranked[0];
7
+ const text = (selected?.text ?? pageSummary).replace(/\s+/g, ' ').trim();
8
+ if (text.length <= 180)
9
+ return text;
10
+ const first = selected?.terms.length ? Math.min(...selected.terms.map(t => text.toLocaleLowerCase().indexOf(t))) : 0;
11
+ const start = Math.max(0, first - 50);
12
+ const length = 180 - (start ? 1 : 0) - 1;
13
+ return `${start ? '…' : ''}${text.slice(start, start + length).trim()}…`;
14
+ }
@@ -100,8 +100,16 @@ export async function addAskFunction(input) {
100
100
  await writeFile(path.join(func, 'index.js'), `import { createAskFunction, nodeAdapter } from './lib/ask-function.js';\n\nconst directory = new URL('.', import.meta.url).pathname;\nexport default nodeAdapter(() => createAskFunction(directory));\n`);
101
101
  // The answer can take about a minute. Hobby allows up to 300 seconds with Fluid compute, so 120 leaves room.
102
102
  await writeFile(path.join(func, '.vc-config.json'), JSON.stringify({ runtime: 'nodejs22.x', handler: 'index.js', launcherType: 'Nodejs', shouldAddHelpers: false, maxDuration: 120, memory: 1024 }, null, 2) + '\n');
103
- await cp(func, searchFunc, { recursive: true });
104
- await writeFile(path.join(searchFunc, 'index.js'), `import { createHostedSearchFunction, nodeAdapter } from './lib/ask-function.js';\n\nconst directory = new URL('.', import.meta.url).pathname;\nexport default nodeAdapter(() => createHostedSearchFunction(directory));\n`);
103
+ // Build Search from its own runtime closure and private retrieval data. It
104
+ // must not inherit chat generation, cards, logging, or compiler dependencies.
105
+ await mkdir(path.join(searchFunc, 'site', 'ai'), { recursive: true });
106
+ for (const name of ['chunks.json', 'embeddings.json', 'embeddings.bin'])
107
+ await cp(path.join(site, 'ai', name), path.join(searchFunc, 'site', 'ai', name));
108
+ const { chat: _chat, ...searchSettings } = runtime.settings;
109
+ await writeFile(path.join(searchFunc, 'runtime.json'), JSON.stringify({ ...runtime, settings: searchSettings, searchStore: chat.budget.store }, null, 2) + '\n');
110
+ await writeFile(path.join(searchFunc, 'package.json'), JSON.stringify({ type: 'module' }) + '\n');
111
+ await copyFunctionRuntime(dist, 'search-function.js', searchFunc);
112
+ await writeFile(path.join(searchFunc, 'index.js'), `import { createHostedSearchFunction } from './lib/search-function.js';\nimport { nodeAdapter } from './lib/function-runtime.js';\n\nconst directory = new URL('.', import.meta.url).pathname;\nexport default nodeAdapter(() => createHostedSearchFunction(directory));\n`);
105
113
  await writeFile(path.join(searchFunc, '.vc-config.json'), JSON.stringify({ runtime: 'nodejs22.x', handler: 'index.js', launcherType: 'Nodejs', shouldAddHelpers: false, maxDuration: 30, memory: 1024 }, null, 2) + '\n');
106
114
  await readdir(func);
107
115
  return { added: true, directory: func, chunks: built.index.chunks.length, embedded: true, embeddingCounts: { embedded: embedded.embedded, reused: embedded.reused } };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shoalrun/cli",
3
- "version": "0.1.0-alpha.15",
3
+ "version": "0.1.0-alpha.17",
4
4
  "publishConfig": {
5
5
  "registry": "https://registry.npmjs.org",
6
6
  "access": "public"
@@ -33,4 +33,6 @@ For a new OpenAPI source, read `guide.api-references` and complete its API-sourc
33
33
 
34
34
  Check the generated HTML, Markdown twins, and `documents.json`. Confirm navigation and API links resolve, and inspect assets inside the components that contain them. Run `shoalrun audit docs.config.yaml` when project checks or API audits are configured; documentation behavior tests can use Doc Detective through `audit.tests` or another external CLI.
35
35
 
36
+ For optional semantic search, use `outputs.html.search.provider: semantic` (available since `0.1.0-alpha.16`) with `retrieval.embeddings`; do not generate a custom browser adapter for Shoalrun's own endpoint. Build the site, run `shoalrun index <output> --embed --config docs.config.yaml`, then `shoalrun serve <output> --search --config docs.config.yaml` with the provider key only in the environment. Confirm the endpoint returns `mode: vector`; a visible result or keyword fallback does not prove semantic retrieval. Selecting the browser provider does not provision production hosting, and profiled Vercel chat-function builds remain unsupported.
37
+
36
38
  Report the files created, commands run, and anything still needing an owner decision. Do not deploy, publish a release, or claim factual accuracy from a successful build alone.