@taurgis/bonsai 1.0.0

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 (159) hide show
  1. package/README.md +239 -0
  2. package/agents/agents/official-docs-researcher.agent.md +98 -0
  3. package/agents/instructions/web-research.instructions.md +29 -0
  4. package/agents/skills/web-research/SKILL.md +126 -0
  5. package/bin/cli.mjs +22 -0
  6. package/dist/base-command.d.ts +14 -0
  7. package/dist/base-command.js +56 -0
  8. package/dist/commands/config/base.d.ts +23 -0
  9. package/dist/commands/config/base.js +46 -0
  10. package/dist/commands/config/get.d.ts +20 -0
  11. package/dist/commands/config/get.js +48 -0
  12. package/dist/commands/config/list.d.ts +17 -0
  13. package/dist/commands/config/list.js +48 -0
  14. package/dist/commands/config/set.d.ts +31 -0
  15. package/dist/commands/config/set.js +94 -0
  16. package/dist/commands/config/unset.d.ts +21 -0
  17. package/dist/commands/config/unset.js +65 -0
  18. package/dist/commands/fetch.d.ts +38 -0
  19. package/dist/commands/fetch.js +311 -0
  20. package/dist/commands/import.d.ts +38 -0
  21. package/dist/commands/import.js +313 -0
  22. package/dist/commands/index.d.ts +35 -0
  23. package/dist/commands/index.js +306 -0
  24. package/dist/commands/inspect.d.ts +13 -0
  25. package/dist/commands/inspect.js +82 -0
  26. package/dist/commands/list.d.ts +25 -0
  27. package/dist/commands/list.js +143 -0
  28. package/dist/commands/prune.d.ts +24 -0
  29. package/dist/commands/prune.js +141 -0
  30. package/dist/commands/research/config/base.d.ts +23 -0
  31. package/dist/commands/research/config/base.js +46 -0
  32. package/dist/commands/research/config/get.d.ts +20 -0
  33. package/dist/commands/research/config/get.js +45 -0
  34. package/dist/commands/research/config/list.d.ts +17 -0
  35. package/dist/commands/research/config/list.js +42 -0
  36. package/dist/commands/research/config/set.d.ts +31 -0
  37. package/dist/commands/research/config/set.js +94 -0
  38. package/dist/commands/research/config/unset.d.ts +21 -0
  39. package/dist/commands/research/config/unset.js +59 -0
  40. package/dist/commands/research/import.d.ts +38 -0
  41. package/dist/commands/research/import.js +313 -0
  42. package/dist/commands/research/inspect.d.ts +13 -0
  43. package/dist/commands/research/inspect.js +86 -0
  44. package/dist/commands/research/list.d.ts +25 -0
  45. package/dist/commands/research/list.js +143 -0
  46. package/dist/commands/research/prune.d.ts +24 -0
  47. package/dist/commands/research/prune.js +141 -0
  48. package/dist/commands/research/search.d.ts +32 -0
  49. package/dist/commands/research/search.js +334 -0
  50. package/dist/commands/research/status.d.ts +17 -0
  51. package/dist/commands/research/status.js +108 -0
  52. package/dist/commands/research.d.ts +36 -0
  53. package/dist/commands/research.js +304 -0
  54. package/dist/commands/search.d.ts +32 -0
  55. package/dist/commands/search.js +334 -0
  56. package/dist/commands/status.d.ts +17 -0
  57. package/dist/commands/status.js +95 -0
  58. package/dist/commands.d.ts +24 -0
  59. package/dist/commands.js +24 -0
  60. package/dist/lib/config/index.d.ts +18 -0
  61. package/dist/lib/config/index.js +33 -0
  62. package/dist/lib/config/io.d.ts +8 -0
  63. package/dist/lib/config/io.js +75 -0
  64. package/dist/lib/config/resolve.d.ts +15 -0
  65. package/dist/lib/config/resolve.js +20 -0
  66. package/dist/lib/config/schema.d.ts +28 -0
  67. package/dist/lib/config/schema.js +32 -0
  68. package/dist/lib/research/artifact.d.ts +21 -0
  69. package/dist/lib/research/artifact.js +217 -0
  70. package/dist/lib/research/browser.d.ts +74 -0
  71. package/dist/lib/research/browser.js +359 -0
  72. package/dist/lib/research/cache-key.d.ts +5 -0
  73. package/dist/lib/research/cache-key.js +10 -0
  74. package/dist/lib/research/capture.d.ts +29 -0
  75. package/dist/lib/research/capture.js +181 -0
  76. package/dist/lib/research/compress.d.ts +5 -0
  77. package/dist/lib/research/compress.js +17 -0
  78. package/dist/lib/research/docs/__fixtures__/sphinx-searchindex.d.ts +1 -0
  79. package/dist/lib/research/docs/__fixtures__/sphinx-searchindex.js +2 -0
  80. package/dist/lib/research/docs/capabilities.d.ts +43 -0
  81. package/dist/lib/research/docs/capabilities.js +9 -0
  82. package/dist/lib/research/docs/clean-dom.d.ts +4 -0
  83. package/dist/lib/research/docs/clean-dom.js +41 -0
  84. package/dist/lib/research/docs/code-blocks.d.ts +5 -0
  85. package/dist/lib/research/docs/code-blocks.js +69 -0
  86. package/dist/lib/research/docs/detect.d.ts +6 -0
  87. package/dist/lib/research/docs/detect.js +239 -0
  88. package/dist/lib/research/docs/machine-readable.d.ts +21 -0
  89. package/dist/lib/research/docs/machine-readable.js +73 -0
  90. package/dist/lib/research/docs/markdown-source.d.ts +17 -0
  91. package/dist/lib/research/docs/markdown-source.js +194 -0
  92. package/dist/lib/research/docs/quality-gates.d.ts +9 -0
  93. package/dist/lib/research/docs/quality-gates.js +65 -0
  94. package/dist/lib/research/docs/remote-search-runner.d.ts +21 -0
  95. package/dist/lib/research/docs/remote-search-runner.js +54 -0
  96. package/dist/lib/research/docs/remote-search.d.ts +14 -0
  97. package/dist/lib/research/docs/remote-search.js +101 -0
  98. package/dist/lib/research/docs/rsc.d.ts +10 -0
  99. package/dist/lib/research/docs/rsc.js +30 -0
  100. package/dist/lib/research/docs/search-index.d.ts +13 -0
  101. package/dist/lib/research/docs/search-index.js +176 -0
  102. package/dist/lib/research/docs/section-artifacts.d.ts +12 -0
  103. package/dist/lib/research/docs/section-artifacts.js +75 -0
  104. package/dist/lib/research/docs/sections.d.ts +12 -0
  105. package/dist/lib/research/docs/sections.js +67 -0
  106. package/dist/lib/research/docs/source-map.d.ts +19 -0
  107. package/dist/lib/research/docs/source-map.js +87 -0
  108. package/dist/lib/research/docs/validate.d.ts +15 -0
  109. package/dist/lib/research/docs/validate.js +63 -0
  110. package/dist/lib/research/extract.d.ts +11 -0
  111. package/dist/lib/research/extract.js +83 -0
  112. package/dist/lib/research/fetcher.d.ts +29 -0
  113. package/dist/lib/research/fetcher.js +192 -0
  114. package/dist/lib/research/freshness.d.ts +21 -0
  115. package/dist/lib/research/freshness.js +82 -0
  116. package/dist/lib/research/markdown.d.ts +6 -0
  117. package/dist/lib/research/markdown.js +66 -0
  118. package/dist/lib/research/regression.d.ts +23 -0
  119. package/dist/lib/research/regression.js +93 -0
  120. package/dist/lib/research/resolve-target.d.ts +18 -0
  121. package/dist/lib/research/resolve-target.js +20 -0
  122. package/dist/lib/research/revalidate.d.ts +32 -0
  123. package/dist/lib/research/revalidate.js +152 -0
  124. package/dist/lib/research/schema.d.ts +47 -0
  125. package/dist/lib/research/schema.js +1 -0
  126. package/dist/lib/research/secret-scan.d.ts +5 -0
  127. package/dist/lib/research/secret-scan.js +37 -0
  128. package/dist/lib/research/secure-write.d.ts +16 -0
  129. package/dist/lib/research/secure-write.js +17 -0
  130. package/dist/lib/research/storage.d.ts +52 -0
  131. package/dist/lib/research/storage.js +182 -0
  132. package/dist/lib/research/store-roots.d.ts +29 -0
  133. package/dist/lib/research/store-roots.js +28 -0
  134. package/dist/lib/research/token-estimate.d.ts +5 -0
  135. package/dist/lib/research/token-estimate.js +7 -0
  136. package/dist/lib/research/url.d.ts +24 -0
  137. package/dist/lib/research/url.js +121 -0
  138. package/dist/sites/index.d.ts +4 -0
  139. package/dist/sites/index.js +28 -0
  140. package/dist/sites/salesforce/browser-search.d.ts +7 -0
  141. package/dist/sites/salesforce/browser-search.js +82 -0
  142. package/dist/sites/salesforce/coveo.d.ts +23 -0
  143. package/dist/sites/salesforce/coveo.js +161 -0
  144. package/dist/sites/salesforce/fetch-page.d.ts +6 -0
  145. package/dist/sites/salesforce/fetch-page.js +42 -0
  146. package/dist/sites/salesforce/index.d.ts +2 -0
  147. package/dist/sites/salesforce/index.js +16 -0
  148. package/dist/sites/salesforce/token.d.ts +26 -0
  149. package/dist/sites/salesforce/token.js +154 -0
  150. package/dist/sites/salesforce-developer/fetch-page.d.ts +3 -0
  151. package/dist/sites/salesforce-developer/fetch-page.js +18 -0
  152. package/dist/sites/salesforce-developer/index.d.ts +2 -0
  153. package/dist/sites/salesforce-developer/index.js +12 -0
  154. package/dist/sites/salesforce-doc-fetch.d.ts +14 -0
  155. package/dist/sites/salesforce-doc-fetch.js +321 -0
  156. package/dist/sites/types.d.ts +27 -0
  157. package/dist/sites/types.js +1 -0
  158. package/oclif.manifest.json +845 -0
  159. package/package.json +78 -0
@@ -0,0 +1,2 @@
1
+ Search.setIndex({ "docnames": ["index", "library/functions", "library/string"], "filenames": ["index.rst", "library/functions.rst", "library/string.rst"], "titles": ["Python Documentation", "Built-in Functions", "String Methods"], "terms": { "function": [1], "string": [2], "print": [1], "format": [2] }, "titleterms": { "function": [1], "string": [2], "python": [0], "built": [1], "methods": [2] }, "objects": {}, "envversion": {} });
2
+ export {};
@@ -0,0 +1,43 @@
1
+ export type Evidence = 'verified' | 'signal';
2
+ export type DocsEngine = 'vitepress' | 'docusaurus' | 'starlight' | 'next' | 'generated-static' | 'spa';
3
+ export type DocsFramework = 'mkdocs' | 'material-mkdocs' | 'sphinx' | 'nextra' | 'fumadocs' | 'gitbook' | 'mintlify' | 'readme' | 'redocly' | 'docsify' | 'rspress' | 'vuepress' | 'docsy' | 'just-the-docs';
4
+ export type MachineReadableType = 'llms.txt' | 'llms-full.txt' | 'route-markdown' | 'search-index' | 'sitemap' | 'source-edit-link';
5
+ export interface MachineReadableArtifact {
6
+ type: MachineReadableType;
7
+ url: string;
8
+ evidence: Evidence;
9
+ }
10
+ export interface SourceHint {
11
+ type: 'markdown' | 'mdx' | 'html';
12
+ url?: string;
13
+ repository?: string;
14
+ branch?: string;
15
+ path?: string;
16
+ evidence: Evidence;
17
+ }
18
+ export type SearchProvider = 'algolia-docsearch' | 'vitepress-local' | 'pagefind' | 'docusaurus-local' | 'mkdocs-local' | 'sphinx-searchindex' | 'jekyll-json' | 'readme-algolia' | 'custom';
19
+ export interface SearchCapability {
20
+ provider: SearchProvider;
21
+ evidence: Evidence;
22
+ indexUrl?: string;
23
+ indexName?: string;
24
+ appId?: string;
25
+ apiKey?: string;
26
+ publicEndpoint?: string;
27
+ }
28
+ export interface PageMapEntry {
29
+ title: string;
30
+ url: string;
31
+ sourcePath?: string;
32
+ }
33
+ export interface SiteCapabilities {
34
+ docsEngine?: DocsEngine;
35
+ framework?: DocsFramework;
36
+ recommendedCapture: 'static' | 'rendered' | 'source';
37
+ machineReadable: MachineReadableArtifact[];
38
+ source?: SourceHint;
39
+ search?: SearchCapability;
40
+ pageMap?: PageMapEntry[];
41
+ notes: string[];
42
+ }
43
+ export declare function emptyCapabilities(): SiteCapabilities;
@@ -0,0 +1,9 @@
1
+ // Capability model for documentation-aware research. A docs-engine detector (and, later,
2
+ // machine-readable/source/search probes) fill this in so the generic pipeline can prefer the
3
+ // best available artifact before falling back to HTML extraction. Evidence is deliberately
4
+ // two-valued: `verified` means observed in the supplied bytes (markup/data/HTTP), `signal` means
5
+ // the page hints at the capability but the endpoint/index/source is not proven. A visible search
6
+ // button is at most a `signal` — never `verified` (see T-16/T-20 acceptance criteria).
7
+ export function emptyCapabilities() {
8
+ return { recommendedCapture: 'static', machineReadable: [], notes: [] };
9
+ }
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Strips decorative docs chrome and base64 images from the document in place before extraction.
3
+ */
4
+ export declare function cleanDocsChrome(document: any): void;
@@ -0,0 +1,41 @@
1
+ // Targeted pre-Readability cleanup for documentation chrome that Readability keeps but agents do
2
+ // not want (T-21): Pagefind-ignored regions, contributor/avatar/sponsor galleries, theme
3
+ // customizer widgets, and base64 `data:` images (e.g. the TypeScript logo, Astro contributor grid).
4
+ // Deliberately narrow — Readability already drops generic nav/footer; over-stripping risks content.
5
+ const CHROME_SELECTORS = [
6
+ '[data-pagefind-ignore]',
7
+ '[class*="contributor" i]',
8
+ '[class*="avatar" i]',
9
+ '[class*="sponsor" i]',
10
+ '[class*="customizer" i]',
11
+ '[class*="theme-toggle" i]',
12
+ '[class*="edit-link" i]',
13
+ '[aria-label*="theme" i]',
14
+ ];
15
+ function removeMatching(document, selector) {
16
+ let nodes = [];
17
+ try {
18
+ nodes = Array.from(document.querySelectorAll(selector));
19
+ }
20
+ catch {
21
+ return; // ignore selectors linkedom can't parse
22
+ }
23
+ for (const node of nodes)
24
+ node.remove();
25
+ }
26
+ // Drops <img> with a data: URI and any anchor reduced to an empty icon link after that removal.
27
+ function dropDataImages(document) {
28
+ for (const img of Array.from(document.querySelectorAll('img'))) {
29
+ const src = img.getAttribute('src') || '';
30
+ if (src.trim().toLowerCase().startsWith('data:'))
31
+ img.remove();
32
+ }
33
+ }
34
+ /**
35
+ * Strips decorative docs chrome and base64 images from the document in place before extraction.
36
+ */
37
+ export function cleanDocsChrome(document) {
38
+ for (const selector of CHROME_SELECTORS)
39
+ removeMatching(document, selector);
40
+ dropDataImages(document);
41
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Reconstructs newline-delimited text for every highlighted code block in the document and tags it
3
+ * with a `language-x` class when one is detectable. Idempotent and a no-op on plain `<pre>` blocks.
4
+ */
5
+ export declare function normalizeCodeBlocks(document: any): void;
@@ -0,0 +1,69 @@
1
+ // Normalizes highlighted code DOM before Readability/Turndown so multi-line examples survive
2
+ // (T-17). Syntax highlighters (Shiki, Expressive Code, Prism/Docusaurus) wrap each source line in
3
+ // a <span class="line"> with no newline between spans; serialized to text they collapse onto one
4
+ // line — exactly the Tailwind/Redux defects from Phase 2. We rebuild newline-delimited text and
5
+ // preserve the language label as a `language-x` class Turndown understands. Operates in place.
6
+ const LANGUAGE_HINTS = /\b(bash|shell|sh|zsh|console|ts|tsx|typescript|js|jsx|javascript|json|jsonc|html|xml|css|scss|less|python|py|go|golang|rust|rs|java|kotlin|ruby|rb|php|sql|yaml|yml|toml|diff|graphql|vue|svelte|astro|md|mdx|dockerfile|c|cpp|csharp|cs)\b/i;
7
+ function detectLanguage(pre, code) {
8
+ const sources = [
9
+ code?.getAttribute?.('class'),
10
+ pre.getAttribute('class'),
11
+ pre.getAttribute('data-language'),
12
+ code?.getAttribute?.('data-language'),
13
+ pre.getAttribute('data-lang'),
14
+ ];
15
+ for (const src of sources) {
16
+ if (!src)
17
+ continue;
18
+ const explicit = src.match(/language-([\w-]+)/i);
19
+ if (explicit)
20
+ return explicit[1].toLowerCase();
21
+ }
22
+ // data-language attributes often hold a bare token (e.g. "bash").
23
+ for (const attr of ['data-language', 'data-lang']) {
24
+ const raw = pre.getAttribute(attr) || code?.getAttribute?.(attr);
25
+ if (raw && LANGUAGE_HINTS.test(raw))
26
+ return raw.toLowerCase();
27
+ }
28
+ return null;
29
+ }
30
+ function reconstructLines(code) {
31
+ const lineEls = code.querySelectorAll('.line, .token-line, [data-line]');
32
+ if (!lineEls.length)
33
+ return false;
34
+ const text = Array.from(lineEls)
35
+ .map((el) => el.textContent ?? '')
36
+ .join('\n')
37
+ .replace(/\n+$/, '');
38
+ code.textContent = text;
39
+ return true;
40
+ }
41
+ // Copy buttons render inside or beside code blocks and otherwise leak a stray "Copy" into Markdown.
42
+ function removeCopyButtons(document) {
43
+ const candidates = document.querySelectorAll('button, [class*="copy" i], [class*="Copy"], [aria-label*="copy" i]');
44
+ for (const el of candidates) {
45
+ const cls = (el.getAttribute('class') || '').toString();
46
+ const aria = el.getAttribute('aria-label') || '';
47
+ const text = (el.textContent || '').trim();
48
+ if (/copy/i.test(cls) || /copy/i.test(aria) || /^copy$/i.test(text)) {
49
+ el.remove();
50
+ }
51
+ }
52
+ }
53
+ /**
54
+ * Reconstructs newline-delimited text for every highlighted code block in the document and tags it
55
+ * with a `language-x` class when one is detectable. Idempotent and a no-op on plain `<pre>` blocks.
56
+ */
57
+ export function normalizeCodeBlocks(document) {
58
+ removeCopyButtons(document);
59
+ const pres = document.querySelectorAll('pre');
60
+ for (const pre of pres) {
61
+ const code = pre.querySelector('code') ?? pre;
62
+ const language = detectLanguage(pre, code === pre ? null : code);
63
+ reconstructLines(code);
64
+ if (language) {
65
+ const existing = (code.getAttribute('class') || '').replace(/language-[\w-]+/gi, '').trim();
66
+ code.setAttribute('class', `${existing} language-${language}`.trim());
67
+ }
68
+ }
69
+ }
@@ -0,0 +1,6 @@
1
+ import type { SiteCapabilities } from './capabilities.js';
2
+ /**
3
+ * Inspects page HTML and returns the detected docs engine/framework plus any capability signals.
4
+ * Always returns a result; an unrecognized page yields a best-effort static/SPA shape classification.
5
+ */
6
+ export declare function detectDocsEngine(html: string, url: string): SiteCapabilities;
@@ -0,0 +1,239 @@
1
+ import { parseHTML } from 'linkedom';
2
+ import { emptyCapabilities } from './capabilities.js';
3
+ import { parseGithubSourceLink } from './source-map.js';
4
+ import { extractNextRscData } from './rsc.js';
5
+ function getMetaGenerator(doc) {
6
+ const meta = doc.querySelector('meta[name="generator"]');
7
+ return (meta?.getAttribute('content') || '').toLowerCase();
8
+ }
9
+ // True when a link is genuinely an "edit this page" link, not just any GitHub link on the page.
10
+ // Footer "Code of Conduct"/"License" links are `/blob/` links that must NOT be mistaken for the
11
+ // page source — that produced a wrong tiny artifact in live testing. Require the `/edit/` route, or
12
+ // an anchor whose class/text explicitly says edit.
13
+ function isPageEditLink(link, href) {
14
+ if (href.includes('/edit/'))
15
+ return true;
16
+ const cls = (link.getAttribute('class') || '').toString();
17
+ const text = (link.textContent || '').trim();
18
+ return (/edit[-_ ]?(?:this|on|page)|edit-link/i.test(cls) ||
19
+ /edit (?:this )?page|edit on github/i.test(text));
20
+ }
21
+ // Finds the page's edit/source link and turns it into a source hint. Only a `signal` until the raw
22
+ // URL is fetched/verified by the source-mapping layer (T-19).
23
+ function findEditLinkSource(doc) {
24
+ const links = doc.querySelectorAll('a[href]');
25
+ for (const link of links) {
26
+ const href = link.getAttribute('href') || '';
27
+ if (!isPageEditLink(link, href))
28
+ continue;
29
+ const parsed = parseGithubSourceLink(href);
30
+ if (parsed) {
31
+ return {
32
+ type: parsed.path.endsWith('.mdx') ? 'mdx' : 'markdown',
33
+ repository: parsed.repository,
34
+ branch: parsed.branch,
35
+ path: parsed.path,
36
+ url: parsed.rawUrl,
37
+ evidence: 'signal',
38
+ };
39
+ }
40
+ }
41
+ return undefined;
42
+ }
43
+ // An advertised llms.txt link/hint in the markup (managed platforms embed these). Conventional
44
+ // probing of llms.txt happens later (T-24); this only records what the page itself points to.
45
+ function findLlmsTxtSignal(ctx) {
46
+ const match = ctx.html.match(/href=["']([^"']*llms(?:-full)?\.txt)["']/i);
47
+ if (!match)
48
+ return undefined;
49
+ try {
50
+ const resolved = new URL(match[1], ctx.url).toString();
51
+ const type = /llms-full\.txt$/i.test(resolved) ? 'llms-full.txt' : 'llms.txt';
52
+ return { type, url: resolved, evidence: 'signal' };
53
+ }
54
+ catch {
55
+ return undefined;
56
+ }
57
+ }
58
+ // Records search shape as a SIGNAL only. A visible button or a config blob is a lead, never proof
59
+ // (T-16/T-20). Connectors promote these to `verified` from fixtures.
60
+ function detectSearchSignal(ctx) {
61
+ const h = ctx.html;
62
+ // VitePress site data is embedded via JSON.parse("…") so quotes may be backslash-escaped.
63
+ if (/__VP_SITE_DATA__/.test(h) && /\\?"provider\\?"\s*:\s*\\?"local\\?"/.test(h)) {
64
+ return { provider: 'vitepress-local', evidence: 'signal' };
65
+ }
66
+ if (/docsearch/i.test(h) || /algolia/i.test(h)) {
67
+ return { provider: 'algolia-docsearch', evidence: 'signal' };
68
+ }
69
+ if (/data-pagefind-body/.test(h)) {
70
+ return { provider: 'pagefind', evidence: 'signal' };
71
+ }
72
+ return undefined;
73
+ }
74
+ function finalize(ctx, engine, framework, recommendedCapture, notes) {
75
+ const caps = emptyCapabilities();
76
+ caps.docsEngine = engine;
77
+ caps.framework = framework;
78
+ caps.recommendedCapture = recommendedCapture;
79
+ caps.notes = notes;
80
+ const source = findEditLinkSource(ctx.doc);
81
+ if (source) {
82
+ caps.source = source;
83
+ caps.machineReadable.push({ type: 'source-edit-link', url: source.url, evidence: 'signal' });
84
+ }
85
+ const llms = findLlmsTxtSignal(ctx);
86
+ if (llms)
87
+ caps.machineReadable.push(llms);
88
+ const search = detectSearchSignal(ctx);
89
+ if (search)
90
+ caps.search = search;
91
+ return caps;
92
+ }
93
+ const detectVitePress = (ctx) => {
94
+ if (!/__VP_HASH_MAP__|__VP_SITE_DATA__|class="[^"]*\bVPDoc\b/.test(ctx.html))
95
+ return null;
96
+ return finalize(ctx, 'vitepress', undefined, 'static', ['detected VitePress globals (verified)']);
97
+ };
98
+ const detectStarlight = (ctx) => {
99
+ // Starlight = Astro docs theme. Require its content markers; the Astro generator only strengthens
100
+ // the note (a bare Astro site without Starlight markers is not a docs page we special-case).
101
+ const starlightMarkers = /data-pagefind-body|sl-(?:doc-search|markdown-content|container)|starlight/i;
102
+ if (!starlightMarkers.test(ctx.html))
103
+ return null;
104
+ const note = ctx.generator.includes('astro')
105
+ ? 'detected Astro Starlight (generator + markers, verified)'
106
+ : 'detected Starlight markers (verified)';
107
+ return finalize(ctx, 'starlight', undefined, 'static', [note]);
108
+ };
109
+ const detectDocusaurus = (ctx) => {
110
+ if (!ctx.generator.includes('docusaurus') &&
111
+ !/__docusaurus|theme-doc-|docusaurus/i.test(ctx.html))
112
+ return null;
113
+ return finalize(ctx, 'docusaurus', undefined, 'static', [
114
+ 'detected Docusaurus markers (verified)',
115
+ ]);
116
+ };
117
+ // Next/RSC frameworks: classify the framework first, then fall back to bare Next.
118
+ const detectNextFamily = (ctx) => {
119
+ const isNext = /__NEXT_DATA__|\/_next\/static\/|self\.__next_f/.test(ctx.html);
120
+ const gen = ctx.generator;
121
+ let framework;
122
+ if (gen.includes('mintlify') || /mintlify/i.test(ctx.html))
123
+ framework = 'mintlify';
124
+ else if (/nextra/i.test(ctx.html))
125
+ framework = 'nextra';
126
+ else if (/fumadocs|fd-/i.test(ctx.html))
127
+ framework = 'fumadocs';
128
+ if (!isNext && !framework)
129
+ return null;
130
+ const note = framework
131
+ ? `detected Next.js + ${framework} (verified)`
132
+ : 'detected Next.js docs/app (verified)';
133
+ const caps = finalize(ctx, 'next', framework, 'static', [note]);
134
+ // Recover the RSC/App Router page map and source-path hints (inert; never executed).
135
+ const rsc = extractNextRscData(ctx.html);
136
+ if (rsc.pageMap.length)
137
+ caps.pageMap = rsc.pageMap;
138
+ if (rsc.sourcePathHints.length) {
139
+ caps.notes.push(`source-path hints (unverified): ${rsc.sourcePathHints.slice(0, 5).join(', ')}`);
140
+ }
141
+ return caps;
142
+ };
143
+ const detectMkDocs = (ctx) => {
144
+ const gen = ctx.generator;
145
+ if (!gen.includes('mkdocs') && !/mkdocs/i.test(ctx.html))
146
+ return null;
147
+ const framework = gen.includes('material') ? 'material-mkdocs' : 'mkdocs';
148
+ return finalize(ctx, 'generated-static', framework, 'static', [
149
+ `detected ${framework} (verified)`,
150
+ ]);
151
+ };
152
+ const detectSphinx = (ctx) => {
153
+ if (!ctx.generator.includes('sphinx') &&
154
+ !/DOCUMENTATION_OPTIONS|searchindex\.js|documentation_options\.js|class="sphinxsidebar"/.test(ctx.html))
155
+ return null;
156
+ return finalize(ctx, 'generated-static', 'sphinx', 'static', ['detected Sphinx (verified)']);
157
+ };
158
+ // Per-framework capability notes beyond the base "detected X" line. Kept out of the detector loop
159
+ // so the loop stays a flat match-and-return.
160
+ function managedFrameworkNotes(ctx, framework) {
161
+ const notes = [`detected ${framework} (verified)`];
162
+ if (framework === 'readme') {
163
+ if (/algolia/i.test(ctx.html))
164
+ notes.push('ReadMe Algolia search metadata present (signal)');
165
+ if (/openapi|oas/i.test(ctx.html))
166
+ notes.push('OpenAPI registry metadata present (signal)');
167
+ }
168
+ // Redocly did not expose llms.txt at the conventional path in research; do not assume one.
169
+ if (framework === 'redocly')
170
+ notes.push('llms.txt unverified at conventional path');
171
+ return notes;
172
+ }
173
+ const detectManagedAndThemes = (ctx) => {
174
+ const checks = [
175
+ [/gitbook/i, 'gitbook', undefined],
176
+ [/readme(?:\.io|\.com)|data-rm-|\brm-Guides\b/i, 'readme', undefined],
177
+ [/redocly|redoc/i, 'redocly', undefined],
178
+ [/rspress/i, 'rspress', 'generated-static'],
179
+ [/vuepress/i, 'vuepress', 'generated-static'],
180
+ [/just-the-docs|search-data\.json/i, 'just-the-docs', 'generated-static'],
181
+ [/td-(?:sidebar|content)|docsy/i, 'docsy', 'generated-static'],
182
+ ];
183
+ for (const [re, framework, engine] of checks) {
184
+ if (re.test(ctx.html) || ctx.generator.includes(framework)) {
185
+ return finalize(ctx, engine, framework, 'static', managedFrameworkNotes(ctx, framework));
186
+ }
187
+ }
188
+ return null;
189
+ };
190
+ const detectDocsify = (ctx) => {
191
+ if (!/\$docsify|docsify(?:\.min)?\.js|window\.\$docsify/i.test(ctx.html))
192
+ return null;
193
+ // Docsify renders Markdown client-side; static HTML is an empty app shell, so prefer rendered
194
+ // unless a source-Markdown mapping is found later (T-28).
195
+ return finalize(ctx, 'spa', 'docsify', 'rendered', [
196
+ 'detected Docsify client-side Markdown renderer (verified)',
197
+ ]);
198
+ };
199
+ // Last resort: tell a script-heavy empty SPA shell apart from a real static/generated doc page.
200
+ function detectGenericShape(ctx) {
201
+ const bodyText = (ctx.doc.body?.textContent || '').trim();
202
+ const scriptCount = ctx.doc.querySelectorAll('script').length;
203
+ if (bodyText.length < 400 && scriptCount >= 3) {
204
+ return finalize(ctx, 'spa', undefined, 'rendered', [
205
+ 'thin body with heavy script payload; SPA rendered candidate (signal)',
206
+ ]);
207
+ }
208
+ const headings = ctx.doc.querySelectorAll('h1, h2, h3').length;
209
+ if (headings >= 8) {
210
+ return finalize(ctx, 'generated-static', undefined, 'static', [
211
+ 'long static reference shape (many headings) (signal)',
212
+ ]);
213
+ }
214
+ return finalize(ctx, undefined, undefined, 'static', ['no docs-engine markers detected']);
215
+ }
216
+ const FAMILY_DETECTORS = [
217
+ detectVitePress,
218
+ detectStarlight,
219
+ detectDocusaurus,
220
+ detectMkDocs,
221
+ detectSphinx,
222
+ detectDocsify,
223
+ detectNextFamily,
224
+ detectManagedAndThemes,
225
+ ];
226
+ /**
227
+ * Inspects page HTML and returns the detected docs engine/framework plus any capability signals.
228
+ * Always returns a result; an unrecognized page yields a best-effort static/SPA shape classification.
229
+ */
230
+ export function detectDocsEngine(html, url) {
231
+ const { document } = parseHTML(html);
232
+ const ctx = { html, url, doc: document, generator: getMetaGenerator(document) };
233
+ for (const detector of FAMILY_DETECTORS) {
234
+ const result = detector(ctx);
235
+ if (result)
236
+ return result;
237
+ }
238
+ return detectGenericShape(ctx);
239
+ }
@@ -0,0 +1,21 @@
1
+ import type { MachineReadableArtifact, SiteCapabilities } from './capabilities.js';
2
+ export interface MachineReadableResult {
3
+ artifact: MachineReadableArtifact;
4
+ body: string;
5
+ }
6
+ export type TextFetcher = (url: string) => Promise<{
7
+ content: string;
8
+ finalUrl: string;
9
+ status: number;
10
+ contentType: string | null;
11
+ }>;
12
+ /**
13
+ * Probes conventional and advertised `llms.txt` URLs. Returns the first that validates as same-origin
14
+ * text, or null. The result is a site-INDEX artifact for discovery — not a replacement for page content.
15
+ */
16
+ export declare function probeLlmsTxt(pageUrl: string, caps: SiteCapabilities, fetcher?: TextFetcher): Promise<MachineReadableResult | null>;
17
+ /**
18
+ * Probes a page-level `.md` route, but only when the engine/framework is known to serve one or the
19
+ * page advertises one. Returns validated Markdown source for the page, or null.
20
+ */
21
+ export declare function probeRouteMarkdown(pageUrl: string, caps: SiteCapabilities, fetcher?: TextFetcher): Promise<MachineReadableResult | null>;
@@ -0,0 +1,73 @@
1
+ import { fetchText } from '../fetcher.js';
2
+ import { isSameDocsOrigin, validateTextArtifact } from './validate.js';
3
+ import { vitepressRouteMarkdown } from './source-map.js';
4
+ const defaultFetcher = (url) => fetchText(url);
5
+ // Frameworks/engines that the research observed serving page-level `.md` routes (T-24 evidence).
6
+ function advertisesRouteMarkdown(caps) {
7
+ return (caps.docsEngine === 'vitepress' || caps.framework === 'rspress' || caps.framework === 'gitbook');
8
+ }
9
+ function llmsTxtCandidates(pageUrl, caps) {
10
+ const advertised = caps.machineReadable
11
+ .filter((m) => m.type === 'llms.txt' || m.type === 'llms-full.txt')
12
+ .map((m) => m.url);
13
+ const candidates = [...advertised];
14
+ try {
15
+ const u = new URL(pageUrl);
16
+ candidates.push(`${u.origin}/llms.txt`);
17
+ const firstSegment = u.pathname.split('/').filter(Boolean)[0];
18
+ if (firstSegment)
19
+ candidates.push(`${u.origin}/${firstSegment}/llms.txt`);
20
+ }
21
+ catch {
22
+ // pageUrl already validated upstream; ignore
23
+ }
24
+ // De-duplicate while preserving order (advertised first).
25
+ return [...new Set(candidates)];
26
+ }
27
+ async function tryFetchValidText(url, fetcher) {
28
+ try {
29
+ const res = await fetcher(url);
30
+ if (res.status === 304 || !res.content)
31
+ return null;
32
+ return validateTextArtifact(res.content).ok ? res.content : null;
33
+ }
34
+ catch {
35
+ return null;
36
+ }
37
+ }
38
+ /**
39
+ * Probes conventional and advertised `llms.txt` URLs. Returns the first that validates as same-origin
40
+ * text, or null. The result is a site-INDEX artifact for discovery — not a replacement for page content.
41
+ */
42
+ export async function probeLlmsTxt(pageUrl, caps, fetcher = defaultFetcher) {
43
+ for (const candidate of llmsTxtCandidates(pageUrl, caps)) {
44
+ if (!isSameDocsOrigin(candidate, pageUrl))
45
+ continue;
46
+ const body = await tryFetchValidText(candidate, fetcher);
47
+ if (body) {
48
+ const type = /llms-full\.txt$/i.test(candidate) ? 'llms-full.txt' : 'llms.txt';
49
+ return { artifact: { type, url: candidate, evidence: 'verified' }, body };
50
+ }
51
+ }
52
+ return null;
53
+ }
54
+ /**
55
+ * Probes a page-level `.md` route, but only when the engine/framework is known to serve one or the
56
+ * page advertises one. Returns validated Markdown source for the page, or null.
57
+ */
58
+ export async function probeRouteMarkdown(pageUrl, caps, fetcher = defaultFetcher) {
59
+ const advertised = caps.machineReadable
60
+ .filter((m) => m.type === 'route-markdown')
61
+ .map((m) => m.url);
62
+ const derived = advertisesRouteMarkdown(caps) ? [vitepressRouteMarkdown(pageUrl)] : [];
63
+ const candidates = [...new Set([...advertised, ...derived].filter((u) => !!u))];
64
+ for (const candidate of candidates) {
65
+ if (!isSameDocsOrigin(candidate, pageUrl))
66
+ continue;
67
+ const body = await tryFetchValidText(candidate, fetcher);
68
+ if (body) {
69
+ return { artifact: { type: 'route-markdown', url: candidate, evidence: 'verified' }, body };
70
+ }
71
+ }
72
+ return null;
73
+ }
@@ -0,0 +1,17 @@
1
+ import type { ExtractionResult } from '../extract.js';
2
+ export interface ParsedFrontmatter {
3
+ frontmatter: Record<string, string>;
4
+ body: string;
5
+ }
6
+ export declare function parseFrontmatter(md: string): ParsedFrontmatter;
7
+ /**
8
+ * Removes dangerous embedded HTML from untrusted Markdown: <script>/<style> blocks, inline event
9
+ * handler attributes, and javascript: URLs. Leaves ordinary Markdown and benign inline HTML intact.
10
+ */
11
+ export declare function sanitizeSourceMarkdown(md: string): string;
12
+ /**
13
+ * Builds an ExtractionResult from public Markdown/MDX source. Frontmatter is parsed for the title
14
+ * and the body is sanitized. The detailed Markdown is the source itself, preserving code fences,
15
+ * tabs, and admonitions better than HTML conversion.
16
+ */
17
+ export declare function extractFromSource(md: string, sourceUrl: string): ExtractionResult;