@readme/cli 0.0.27 → 0.0.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +5 -5
- package/src/cli.js +3 -3
- package/src/commands/import-oas.test.js +277 -0
- package/src/commands/import.classify.test.js +172 -0
- package/src/commands/import.js +2848 -443
- package/src/commands/import.landing.test.js +217 -0
- package/src/commands/import.slug-planner.test.js +86 -0
- package/src/commands/import.staging.test.js +158 -0
- package/src/commands/import.test.js +382 -0
- package/src/commands/oas-sync.js +362 -92
- package/src/prompts/index.js +101 -13
- package/src/utils/lint.js +2 -2
- package/src/utils/llms.js +224 -0
- package/src/utils/llms.test.js +302 -0
- package/src/utils/reporter.js +2 -1
- package/src/utils/slug-for-entry.js +16 -0
- package/src/utils/url-segs.js +124 -0
- package/src/utils/url-segs.test.js +239 -0
- package/src/validators/components.js +11 -7
- package/src/validators/duplicates.js +8 -8
- package/src/validators/frontmatter.js +11 -0
- package/src/validators/links.js +2 -8
- package/src/validators/oas-reference.js +13 -38
- package/src/validators/ordering.js +169 -83
- package/vendor/TOOLS.md +1 -1
- package/src/dev/.next/app-build-manifest.json +0 -20
- package/src/dev/.next/build-manifest.json +0 -31
- package/src/dev/.next/cache/.rscinfo +0 -1
- package/src/dev/.next/cache/next-devtools-config.json +0 -1
- package/src/dev/.next/cache/webpack/client-development/0.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/client-development/1.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/client-development/10.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/client-development/11.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/client-development/2.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/client-development/3.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/client-development/3.pack.gz_ +0 -0
- package/src/dev/.next/cache/webpack/client-development/4.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/client-development/5.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/client-development/5.pack.gz_ +0 -0
- package/src/dev/.next/cache/webpack/client-development/6.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/client-development/7.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/client-development/7.pack.gz_ +0 -0
- package/src/dev/.next/cache/webpack/client-development/8.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/client-development/9.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/client-development/index.pack.gz.old +0 -0
- package/src/dev/.next/cache/webpack/client-development-fallback/0.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/client-development-fallback/1.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/client-development-fallback/index.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/client-development-fallback/index.pack.gz.old +0 -0
- package/src/dev/.next/cache/webpack/edge-server-development/0.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/edge-server-development/1.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/edge-server-development/index.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/edge-server-development/index.pack.gz.old +0 -0
- package/src/dev/.next/cache/webpack/server-development/0.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/server-development/1.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/server-development/10.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/server-development/11.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/server-development/12.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/server-development/13.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/server-development/14.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/server-development/15.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/server-development/2.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/server-development/2.pack.gz_ +0 -0
- package/src/dev/.next/cache/webpack/server-development/3.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/server-development/3.pack.gz_ +0 -0
- package/src/dev/.next/cache/webpack/server-development/4.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/server-development/5.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/server-development/6.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/server-development/6.pack.gz_ +0 -0
- package/src/dev/.next/cache/webpack/server-development/7.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/server-development/7.pack.gz_ +0 -0
- package/src/dev/.next/cache/webpack/server-development/8.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/server-development/9.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/server-development/9.pack.gz_ +0 -0
- package/src/dev/.next/cache/webpack/server-development/index.pack.gz +0 -0
- package/src/dev/.next/cache/webpack/server-development/index.pack.gz.old +0 -0
- package/src/dev/.next/package.json +0 -1
- package/src/dev/.next/prerender-manifest.json +0 -11
- package/src/dev/.next/react-loadable-manifest.json +0 -1
- package/src/dev/.next/routes-manifest.json +0 -1
- package/src/dev/.next/server/app/[...slug]/page.js +0 -360
- package/src/dev/.next/server/app/[...slug]/page_client-reference-manifest.js +0 -1
- package/src/dev/.next/server/app/page.js +0 -349
- package/src/dev/.next/server/app/page_client-reference-manifest.js +0 -1
- package/src/dev/.next/server/app-paths-manifest.json +0 -3
- package/src/dev/.next/server/edge-runtime-webpack.js +0 -1151
- package/src/dev/.next/server/interception-route-rewrite-manifest.js +0 -1
- package/src/dev/.next/server/middleware-build-manifest.js +0 -33
- package/src/dev/.next/server/middleware-manifest.json +0 -32
- package/src/dev/.next/server/middleware-react-loadable-manifest.js +0 -1
- package/src/dev/.next/server/middleware.js +0 -1113
- package/src/dev/.next/server/next-font-manifest.js +0 -1
- package/src/dev/.next/server/next-font-manifest.json +0 -1
- package/src/dev/.next/server/pages-manifest.json +0 -5
- package/src/dev/.next/server/server-reference-manifest.js +0 -1
- package/src/dev/.next/server/server-reference-manifest.json +0 -5
- package/src/dev/.next/server/static/webpack/633457081244afec._.hot-update.json +0 -1
- package/src/dev/.next/server/vendor-chunks/@readme.js +0 -25
- package/src/dev/.next/server/vendor-chunks/@swc.js +0 -55
- package/src/dev/.next/server/vendor-chunks/next.js +0 -3659
- package/src/dev/.next/server/webpack-runtime.js +0 -209
- package/src/dev/.next/static/chunks/app/[...slug]/loading.js +0 -28
- package/src/dev/.next/static/chunks/app/[...slug]/page.js +0 -28
- package/src/dev/.next/static/chunks/app/layout.js +0 -171
- package/src/dev/.next/static/chunks/app/page.js +0 -28
- package/src/dev/.next/static/chunks/app-pages-internals.js +0 -182
- package/src/dev/.next/static/chunks/main-app.js +0 -1882
- package/src/dev/.next/static/chunks/polyfills.js +0 -1
- package/src/dev/.next/static/chunks/webpack.js +0 -1393
- package/src/dev/.next/static/css/app/layout.css +0 -559
- package/src/dev/.next/static/development/_buildManifest.js +0 -1
- package/src/dev/.next/static/development/_ssgManifest.js +0 -1
- package/src/dev/.next/static/webpack/633457081244afec._.hot-update.json +0 -1
- package/src/dev/.next/static/webpack/ec52a3fce0f78db0.webpack.hot-update.json +0 -1
- package/src/dev/.next/static/webpack/webpack.ec52a3fce0f78db0.hot-update.js +0 -12
- package/src/dev/.next/trace +0 -21
- package/src/dev/.next/types/app/[...slug]/page.ts +0 -84
- package/src/dev/.next/types/app/layout.ts +0 -84
- package/src/dev/.next/types/app/page.ts +0 -84
- package/src/dev/.next/types/cache-life.d.ts +0 -141
- package/src/dev/.next/types/package.json +0 -1
- package/src/dev/.next/types/routes.d.ts +0 -55
- package/src/validators/numbering.js +0 -134
package/src/prompts/index.js
CHANGED
|
@@ -19,16 +19,33 @@
|
|
|
19
19
|
|
|
20
20
|
export const slotOrphansSystemPrompt = [
|
|
21
21
|
'You assign orphan documentation pages to existing categories.',
|
|
22
|
-
'
|
|
22
|
+
'Respond with a JSON object of the form {"assignments": [...]} — one integer per orphan page, in order.',
|
|
23
23
|
'Each integer is the category index (0-based) the orphan belongs in, or -1 if none fit.',
|
|
24
24
|
'',
|
|
25
|
-
'Example output: [0, 2, 0, 1, -1, 3]',
|
|
25
|
+
'Example output: {"assignments": [0, 2, 0, 1, -1, 3]}',
|
|
26
26
|
'',
|
|
27
27
|
'Guidance:',
|
|
28
28
|
'- Choose the best semantic fit based on the orphan\'s title and URL path.',
|
|
29
29
|
'- Only use -1 when no existing category is plausible.',
|
|
30
30
|
].join('\n');
|
|
31
31
|
|
|
32
|
+
/**
|
|
33
|
+
* Structured-output schema for slotOrphans. Root must be an object (top-level
|
|
34
|
+
* arrays aren't allowed), so the index array is wrapped in `assignments`.
|
|
35
|
+
*/
|
|
36
|
+
export const slotOrphansOutputSchema = {
|
|
37
|
+
type: 'object',
|
|
38
|
+
additionalProperties: false,
|
|
39
|
+
required: ['assignments'],
|
|
40
|
+
properties: {
|
|
41
|
+
assignments: {
|
|
42
|
+
type: 'array',
|
|
43
|
+
items: { type: 'integer' },
|
|
44
|
+
description: 'One entry per orphan page, in input order: the 0-based category index, or -1 if none fit.',
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
|
|
32
49
|
/**
|
|
33
50
|
* @param {object} input
|
|
34
51
|
* @param {Array<{ title: string, pages?: Array<{ title: string }> }>} input.categories
|
|
@@ -54,7 +71,7 @@ export function buildSlotOrphansUserPrompt({ categories, orphans }) {
|
|
|
54
71
|
`${orphans.length} orphan pages to slot:`,
|
|
55
72
|
...orphanList,
|
|
56
73
|
'',
|
|
57
|
-
`Output
|
|
74
|
+
`Output {"assignments": [...]} with ${orphans.length} integers, one per orphan in order (category index 0..${categories.length - 1}, or -1 for none).`,
|
|
58
75
|
].join('\n');
|
|
59
76
|
}
|
|
60
77
|
|
|
@@ -69,12 +86,29 @@ export function slotOrphansPrompt(input) {
|
|
|
69
86
|
|
|
70
87
|
export const iconizeNavSystemPrompt = [
|
|
71
88
|
'You assign one FontAwesome Free Solid icon to each documentation category.',
|
|
72
|
-
'
|
|
73
|
-
'Use the icon name only (no "fa-" prefix
|
|
89
|
+
'Respond with a JSON object of the form {"icons": [...]} — one icon name string per input category, in order.',
|
|
90
|
+
'Use the icon name only (no "fa-" prefix).',
|
|
74
91
|
'',
|
|
75
|
-
'Example output: ["rocket", "book", "code", "gear"]',
|
|
92
|
+
'Example output: {"icons": ["rocket", "book", "code", "gear"]}',
|
|
76
93
|
].join('\n');
|
|
77
94
|
|
|
95
|
+
/**
|
|
96
|
+
* Structured-output schema for iconizeNav. Root must be an object, so the
|
|
97
|
+
* icon-name array is wrapped in `icons`.
|
|
98
|
+
*/
|
|
99
|
+
export const iconizeNavOutputSchema = {
|
|
100
|
+
type: 'object',
|
|
101
|
+
additionalProperties: false,
|
|
102
|
+
required: ['icons'],
|
|
103
|
+
properties: {
|
|
104
|
+
icons: {
|
|
105
|
+
type: 'array',
|
|
106
|
+
items: { type: 'string' },
|
|
107
|
+
description: 'One FontAwesome Free Solid icon name per input category, in order (no "fa-" prefix).',
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
|
|
78
112
|
/**
|
|
79
113
|
* @param {object} input
|
|
80
114
|
* @param {Array<{ title: string }>} input.categories
|
|
@@ -86,7 +120,7 @@ export function buildIconizeNavUserPrompt({ categories }) {
|
|
|
86
120
|
'',
|
|
87
121
|
...categories.map((c, i) => `${i}. ${c.title}`),
|
|
88
122
|
'',
|
|
89
|
-
'Return
|
|
123
|
+
'Return {"icons": [...]} with one FontAwesome icon name per category, in order.',
|
|
90
124
|
].join('\n');
|
|
91
125
|
}
|
|
92
126
|
|
|
@@ -101,13 +135,15 @@ export function iconizeNavPrompt(input) {
|
|
|
101
135
|
|
|
102
136
|
export const organizeFromSectionsSystemPrompt = [
|
|
103
137
|
'You assign a FontAwesome Free Solid icon to each documentation section, and lightly polish the section title.',
|
|
104
|
-
'
|
|
138
|
+
'Respond with a JSON object matching the schema below.',
|
|
105
139
|
'',
|
|
106
140
|
'Schema:',
|
|
107
|
-
'
|
|
108
|
-
'
|
|
109
|
-
'
|
|
110
|
-
'
|
|
141
|
+
'{',
|
|
142
|
+
' "sections": [',
|
|
143
|
+
' { "title": "<lightly polished Title Case, 1-4 words>", "icon": "<fontawesome icon name, no fa- prefix>" },',
|
|
144
|
+
' ...',
|
|
145
|
+
' ]',
|
|
146
|
+
'}',
|
|
111
147
|
'',
|
|
112
148
|
'Rules:',
|
|
113
149
|
'- Return exactly one entry per input section, in the same order.',
|
|
@@ -115,6 +151,31 @@ export const organizeFromSectionsSystemPrompt = [
|
|
|
115
151
|
'- Pick an icon that semantically fits the section (e.g. rocket for Getting Started, code for API, plug for Integrations).',
|
|
116
152
|
].join('\n');
|
|
117
153
|
|
|
154
|
+
/**
|
|
155
|
+
* Structured-output schema for organizeFromSections. Root must be an object,
|
|
156
|
+
* so the per-section entries are wrapped in `sections`.
|
|
157
|
+
*/
|
|
158
|
+
export const organizeFromSectionsOutputSchema = {
|
|
159
|
+
type: 'object',
|
|
160
|
+
additionalProperties: false,
|
|
161
|
+
required: ['sections'],
|
|
162
|
+
properties: {
|
|
163
|
+
sections: {
|
|
164
|
+
type: 'array',
|
|
165
|
+
description: 'Exactly one entry per input section, in the same order.',
|
|
166
|
+
items: {
|
|
167
|
+
type: 'object',
|
|
168
|
+
additionalProperties: false,
|
|
169
|
+
required: ['title', 'icon'],
|
|
170
|
+
properties: {
|
|
171
|
+
title: { type: 'string', description: 'Lightly polished Title Case section title, 1-4 words.' },
|
|
172
|
+
icon: { type: 'string', description: 'FontAwesome Free Solid icon name, no "fa-" prefix.' },
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
},
|
|
177
|
+
};
|
|
178
|
+
|
|
118
179
|
/**
|
|
119
180
|
* @param {object} input
|
|
120
181
|
* @param {string|null|undefined} input.siteTitle
|
|
@@ -128,7 +189,7 @@ export function buildOrganizeFromSectionsUserPrompt({ siteTitle, sections }) {
|
|
|
128
189
|
'',
|
|
129
190
|
...sections.map((s, i) => `${i}. ${s.title} (${s.items.length} pages)`),
|
|
130
191
|
'',
|
|
131
|
-
'Output the JSON
|
|
192
|
+
'Output the JSON object now.',
|
|
132
193
|
].join('\n');
|
|
133
194
|
}
|
|
134
195
|
|
|
@@ -166,6 +227,33 @@ export const organizeFromScratchSystemPrompt = [
|
|
|
166
227
|
'- Keep category titles human-readable, Title Case, 1-4 words.',
|
|
167
228
|
].join('\n');
|
|
168
229
|
|
|
230
|
+
/** Structured-output schema for organizeFromScratch. */
|
|
231
|
+
export const organizeFromScratchOutputSchema = {
|
|
232
|
+
type: 'object',
|
|
233
|
+
additionalProperties: false,
|
|
234
|
+
required: ['title', 'categories'],
|
|
235
|
+
properties: {
|
|
236
|
+
title: { type: 'string', description: 'Short site or doc title.' },
|
|
237
|
+
categories: {
|
|
238
|
+
type: 'array',
|
|
239
|
+
items: {
|
|
240
|
+
type: 'object',
|
|
241
|
+
additionalProperties: false,
|
|
242
|
+
required: ['title', 'icon', 'pageIds'],
|
|
243
|
+
properties: {
|
|
244
|
+
title: { type: 'string', description: 'Title Case category name, 1-4 words.' },
|
|
245
|
+
icon: { type: 'string', description: 'FontAwesome Free Solid icon name, no "fa-" prefix.' },
|
|
246
|
+
pageIds: {
|
|
247
|
+
type: 'array',
|
|
248
|
+
items: { type: 'integer' },
|
|
249
|
+
description: 'Integer ids from the input page list. Every input id appears in exactly one category.',
|
|
250
|
+
},
|
|
251
|
+
},
|
|
252
|
+
},
|
|
253
|
+
},
|
|
254
|
+
},
|
|
255
|
+
};
|
|
256
|
+
|
|
169
257
|
/**
|
|
170
258
|
* @param {object} input
|
|
171
259
|
* @param {string|null|undefined} input.siteTitle
|
package/src/utils/lint.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import fs from 'node:fs'
|
|
2
2
|
import path from 'node:path'
|
|
3
|
-
import { pathToFileURL } from 'node:url'
|
|
3
|
+
import { pathToFileURL, fileURLToPath } from 'node:url'
|
|
4
4
|
|
|
5
5
|
const TARGET_PATTERNS = [
|
|
6
6
|
{ dir: 'custom_blocks', ext: ['.mdx', '.md'] },
|
|
@@ -38,7 +38,7 @@ export function collectFiles(gitRoot) {
|
|
|
38
38
|
* `validateAll()` for cross-file checks (like ordering).
|
|
39
39
|
*/
|
|
40
40
|
async function loadValidators() {
|
|
41
|
-
const validatorsDir = path.join(path.dirname(
|
|
41
|
+
const validatorsDir = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', 'validators')
|
|
42
42
|
const files = fs.readdirSync(validatorsDir).filter((f) => f.endsWith('.js'))
|
|
43
43
|
const validators = []
|
|
44
44
|
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
const H1_RE = /^#\s+(.+)$/
|
|
2
|
+
const H2_RE = /^##\s+(.+)$/
|
|
3
|
+
const H3_RE = /^###\s+(.+)$/
|
|
4
|
+
|
|
5
|
+
// llms.txt might have a literal " (section)" marker to every
|
|
6
|
+
// grouping heading (e.g. `## getting-started (section)`). It's internal
|
|
7
|
+
// metadata, not part of the category name — strip it before it reaches the sidebar.
|
|
8
|
+
const SECTION_MARKER_RE = /\s*\(section\)\s*$/i
|
|
9
|
+
|
|
10
|
+
// Max items per section before we consider it "oversized" for usability purposes.
|
|
11
|
+
// Mirrors the cap in usableSections() in import.js.
|
|
12
|
+
const MAX_SECTION_ITEMS = 200
|
|
13
|
+
// Any line whose meaningful content is a markdown link. Accepts:
|
|
14
|
+
// - Standard list rows: `- [text](url)`, `* [text](url) — desc`
|
|
15
|
+
// - Bare link lines: `[text](url)`
|
|
16
|
+
// - Breadcrumb-prefixed rows used by Fern/Mintlify-style indices:
|
|
17
|
+
// `ElevenAgents [Agent WebSockets](url)`
|
|
18
|
+
// `API Reference > Agents > Branches [List branches](url)`
|
|
19
|
+
// - Trailing-parenthetical-breadcrumb rows (AssemblyAI/Fern variant) — the
|
|
20
|
+
// breadcrumb sits between the url and the description and is matched-and-discarded:
|
|
21
|
+
// `[text](url) (Breadcrumb > path): description`
|
|
22
|
+
// Captures: [prefix, text, url, description] (prefix and description may be empty).
|
|
23
|
+
const LINK_LINE_RE = /^\s*(?:[-*+]\s+)?([^\[\n]*?)\s*\[([^\]]+)\] ?\(([^)\s]+)\)(?:\s+\([^)]*\))?(?:\s*[:—–-]\s*(.+?))?\s*$/
|
|
24
|
+
const BLOCKQUOTE_RE = /^\s*>/
|
|
25
|
+
const FENCE_RE = /^\s*(?:```|~~~)/
|
|
26
|
+
|
|
27
|
+
// A llms.txt is considered usable as long as it has at least one link row.
|
|
28
|
+
// Beyond that we enforce a structural ratio (share of "spec-shaped" lines —
|
|
29
|
+
// H1/H2/blockquote/link row/blank — over total lines), but only once the
|
|
30
|
+
// file has enough links for the ratio to be statistically meaningful. A
|
|
31
|
+
// 3-link file with one prose sentence would compute a misleading ratio; we
|
|
32
|
+
// can't tell signal from noise at that size, so we trust it and let the
|
|
33
|
+
// BFS-merge dedupe handle any junk that slips through.
|
|
34
|
+
const RATIO_CHECK_MIN_LINKS = 10
|
|
35
|
+
const MIN_CONFORMING_RATIO = 0.7
|
|
36
|
+
|
|
37
|
+
export function analyzeLlmsTxt(body, llmsUrl, options = {}) {
|
|
38
|
+
const parsed = parseLlmsTxt(body, llmsUrl, options)
|
|
39
|
+
const lineStats = classifyLines(body)
|
|
40
|
+
const linkItems = parsed.sections.reduce((sum, s) => sum + s.items.length, 0)
|
|
41
|
+
const reason = getSkipReason(body, linkItems, lineStats)
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
parsed,
|
|
45
|
+
usable: !reason,
|
|
46
|
+
reason,
|
|
47
|
+
stats: { ...lineStats, linkItems },
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function parseLlmsTxt(body, llmsUrl, options = {}) {
|
|
52
|
+
const h2Result = parseSections(body, llmsUrl, H2_RE, options)
|
|
53
|
+
|
|
54
|
+
// If every H2 section is oversized (or there are none), and the file has H3
|
|
55
|
+
// headings, re-parse treating H3 (###) as section boundaries.
|
|
56
|
+
// For strange LLMS.txt formatting
|
|
57
|
+
const allOversized = h2Result.sections.length === 0 ||
|
|
58
|
+
h2Result.sections.every((s) => s.items.length > MAX_SECTION_ITEMS)
|
|
59
|
+
if (allOversized && /^###\s/m.test(body)) {
|
|
60
|
+
const h3Result = parseSections(body, llmsUrl, H3_RE, options)
|
|
61
|
+
if (h3Result.sections.length > 1) {
|
|
62
|
+
return { ...h3Result, h3Fallback: true }
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return h2Result
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function parseSections(body, llmsUrl, sectionRe, options = {}) {
|
|
70
|
+
const lines = body.split(/\r?\n/)
|
|
71
|
+
let title = null
|
|
72
|
+
const sections = []
|
|
73
|
+
let current = null
|
|
74
|
+
|
|
75
|
+
for (const line of lines) {
|
|
76
|
+
const h1 = line.match(H1_RE)
|
|
77
|
+
if (h1 && !title) {
|
|
78
|
+
title = h1[1].trim()
|
|
79
|
+
continue
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const sectionMatch = line.match(sectionRe)
|
|
83
|
+
if (sectionMatch) {
|
|
84
|
+
current = { title: sectionMatch[1].trim().replace(SECTION_MARKER_RE, '').trim(), items: [] }
|
|
85
|
+
sections.push(current)
|
|
86
|
+
continue
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const item = parseListLink(line, llmsUrl, options)
|
|
90
|
+
if (!item) continue
|
|
91
|
+
|
|
92
|
+
if (!current) {
|
|
93
|
+
current = { title: 'Resources', items: [] }
|
|
94
|
+
sections.push(current)
|
|
95
|
+
}
|
|
96
|
+
current.items.push(item)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return { title, sections }
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function getSkipReason(body, linkItems, lineStats) {
|
|
103
|
+
if (/^---\r?\n/.test(body)) return 'starts with YAML frontmatter'
|
|
104
|
+
if (linkItems === 0) return 'no link items'
|
|
105
|
+
if (linkItems >= RATIO_CHECK_MIN_LINKS && lineStats.ratio < MIN_CONFORMING_RATIO) {
|
|
106
|
+
return `conforming-line ratio ${lineStats.ratio.toFixed(2)} below ${MIN_CONFORMING_RATIO} (${lineStats.conforming}/${lineStats.total} lines)`
|
|
107
|
+
}
|
|
108
|
+
return null
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Walk the body line-by-line and bucket each line as conforming or not.
|
|
113
|
+
*
|
|
114
|
+
* conforming blank, H1, H2, blockquote, standard link-row
|
|
115
|
+
* non-conforming everything else, plus every line inside (and the delimiters
|
|
116
|
+
* of) a fenced code block
|
|
117
|
+
*/
|
|
118
|
+
function classifyLines(body) {
|
|
119
|
+
const lines = body.split(/\r?\n/)
|
|
120
|
+
let inFence = false
|
|
121
|
+
let conforming = 0
|
|
122
|
+
let nonConforming = 0
|
|
123
|
+
|
|
124
|
+
for (const line of lines) {
|
|
125
|
+
if (FENCE_RE.test(line)) {
|
|
126
|
+
nonConforming++
|
|
127
|
+
inFence = !inFence
|
|
128
|
+
continue
|
|
129
|
+
}
|
|
130
|
+
if (inFence) {
|
|
131
|
+
nonConforming++
|
|
132
|
+
continue
|
|
133
|
+
}
|
|
134
|
+
if (line.trim() === '') {
|
|
135
|
+
conforming++
|
|
136
|
+
continue
|
|
137
|
+
}
|
|
138
|
+
if (H1_RE.test(line) || H2_RE.test(line) || LINK_LINE_RE.test(line) || BLOCKQUOTE_RE.test(line)) {
|
|
139
|
+
conforming++
|
|
140
|
+
continue
|
|
141
|
+
}
|
|
142
|
+
nonConforming++
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const total = conforming + nonConforming
|
|
146
|
+
const ratio = total === 0 ? 0 : conforming / total
|
|
147
|
+
return { conforming, nonConforming, total, ratio }
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function parseListLink(line, llmsUrl, options = {}) {
|
|
151
|
+
const match = line.match(LINK_LINE_RE)
|
|
152
|
+
if (!match) return null
|
|
153
|
+
|
|
154
|
+
const [, prefix, text, rawUrl, trailingDesc] = match
|
|
155
|
+
const url = normalizeUrl(rawUrl, llmsUrl, options)
|
|
156
|
+
if (!url) return null
|
|
157
|
+
|
|
158
|
+
// Prefer an explicit trailing description (`[text](url) — desc`); fall back
|
|
159
|
+
// to the breadcrumb prefix (`API Reference > Agents [text](url)`) when no
|
|
160
|
+
// explicit description is present. Either gives downstream extra context.
|
|
161
|
+
const description = trailingDesc?.trim() || prefix?.trim() || null
|
|
162
|
+
|
|
163
|
+
return {
|
|
164
|
+
text: text.trim(),
|
|
165
|
+
url,
|
|
166
|
+
description: description || null,
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function normalizeUrl(rawUrl, llmsUrl, options = {}) {
|
|
171
|
+
const trimmed = String(rawUrl || '').trim().replace(/[.,;]+$/, '')
|
|
172
|
+
if (!trimmed || /^#/.test(trimmed) || /^(mailto|javascript):/i.test(trimmed)) return null
|
|
173
|
+
|
|
174
|
+
try {
|
|
175
|
+
const url = llmsUrl ? new URL(resolveLlmsRelativeUrl(trimmed, llmsUrl, options), llmsUrl) : new URL(trimmed)
|
|
176
|
+
if (url.protocol !== 'http:' && url.protocol !== 'https:') return null
|
|
177
|
+
return url.toString()
|
|
178
|
+
} catch {
|
|
179
|
+
return null
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function resolveLlmsRelativeUrl(trimmed, llmsUrl, options) {
|
|
184
|
+
if (options.rootRelativeResolution !== 'llms-dir' || !isRootRelativeUrl(trimmed)) return trimmed
|
|
185
|
+
|
|
186
|
+
const nestedPrefix = getLlmsNestedPrefix(llmsUrl)
|
|
187
|
+
if (!nestedPrefix) return trimmed
|
|
188
|
+
|
|
189
|
+
const normallyResolved = new URL(trimmed, llmsUrl)
|
|
190
|
+
if (!shouldResolveRootRelativeFromLlmsDir(trimmed, normallyResolved.pathname, nestedPrefix)) return trimmed
|
|
191
|
+
|
|
192
|
+
return `.${trimmed}`
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function shouldResolveRootRelativeFromLlmsDir(rawUrl, pathname, nestedPrefix) {
|
|
196
|
+
if (isPathWithinPrefix(pathname, nestedPrefix)) return false
|
|
197
|
+
if (/\/llms\.txt$/i.test(pathname)) return false
|
|
198
|
+
|
|
199
|
+
const lastSegment = pathname.split('/').pop() || ''
|
|
200
|
+
const ext = lastSegment.match(/\.([a-z0-9]+)$/i)?.[1]?.toLowerCase()
|
|
201
|
+
if (ext && !['htm', 'html', 'md', 'mdx'].includes(ext)) return false
|
|
202
|
+
|
|
203
|
+
return isRootRelativeUrl(rawUrl)
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function getLlmsNestedPrefix(llmsUrl) {
|
|
207
|
+
if (!llmsUrl) return null
|
|
208
|
+
|
|
209
|
+
try {
|
|
210
|
+
const pathname = new URL(llmsUrl).pathname
|
|
211
|
+
const dir = pathname.replace(/llms\.txt$/i, '').replace(/\/+$/, '/')
|
|
212
|
+
return dir === '/' ? null : dir
|
|
213
|
+
} catch {
|
|
214
|
+
return null
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function isRootRelativeUrl(rawUrl) {
|
|
219
|
+
return /^\/(?!\/)/.test(rawUrl)
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function isPathWithinPrefix(pathname, prefix) {
|
|
223
|
+
return pathname === prefix.replace(/\/$/, '') || pathname.startsWith(prefix)
|
|
224
|
+
}
|