-
Notifications
You must be signed in to change notification settings - Fork 72
Expand file tree
/
Copy pathgenerate-llms.cjs
More file actions
370 lines (320 loc) · 13.9 KB
/
Copy pathgenerate-llms.cjs
File metadata and controls
370 lines (320 loc) · 13.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
/*
Generates llms.txt files following the llms.txt standard (https://llmstxt.org/)
- llms.txt: thin routing index listing all frameworks with descriptions and page topics
- llms/{framework}.txt: framework index — overview content + links to all per-page files
- llms/{framework}/{page}.txt: one file per sidebar page with full stripped markdown content
- Page order follows the sidebar order defined in vocs.config.ts
- Runs post-build and writes to the dist directory
*/
const fs = require('fs');
const path = require('path');
const matter = require('gray-matter');
const PROD_URL = 'https://frameworks.securityalliance.org';
const BASE_URL = process.env.CF_PAGES_BRANCH === 'main'
? PROD_URL
: (process.env.CF_PAGES_URL || PROD_URL);
const workspaceRoot = process.cwd();
const PAGES_DIR = path.join(workspaceRoot, 'docs', 'pages');
const isMainBranch = process.env.CF_PAGES_BRANCH === 'main';
function findDistDir() {
const candidates = [
path.join(workspaceRoot, 'dist', 'public'),
path.join(workspaceRoot, 'docs', 'dist'),
path.join(workspaceRoot, 'dist'),
];
return candidates.find((dir) => fs.existsSync(dir));
}
// Returns all sidebar links in document order, filtered to a specific folder prefix.
// Tracks brace depth so a dev: true flag on a parent block is inherited by all child links.
function getSidebarLinksForFolder(folderName) {
const configPath = path.join(workspaceRoot, 'vocs.config.ts');
if (!fs.existsSync(configPath)) return [];
const lines = fs.readFileSync(configPath, 'utf8').split('\n');
const links = [];
let depth = 0;
const devDepths = new Set(); // brace depths at which a parent block carries dev: true
for (const line of lines) {
if (isMainBranch) {
const opens = (line.match(/\{/g) || []).length;
const closes = (line.match(/\}/g) || []).length;
const newDepth = depth + opens - closes;
// Pop dev markers for blocks we're closing
if (newDepth < depth) {
for (const d of devDepths) {
if (d > newDepth) devDepths.delete(d);
}
}
depth = newDepth;
// If this line carries dev: true but is not a link item, it's a container flag - inherit downward
if (line.includes('dev:') && line.includes('true') && !line.match(/link:/)) {
devDepths.add(depth);
}
}
const match = line.match(/link:\s*(['"])(\/[^'"]+)\1/);
if (!match) continue;
const link = match[2];
// Skip if the item itself has dev: true, or if any ancestor block does
if (isMainBranch && ((line.includes('dev:') && line.includes('true')) || devDepths.size > 0)) continue;
if (link.startsWith(`/${folderName}/`)) {
links.push(link);
}
}
return links;
}
function linkToFilePath(link) {
const base = path.join(PAGES_DIR, link);
for (const candidate of [`${base}.mdx`, `${base}.md`, path.join(base, 'index.mdx')]) {
if (fs.existsSync(candidate)) return candidate;
}
return null;
}
function getPageUrl(filePath) {
return filePath
.replace(PAGES_DIR, '')
.replace(/\.(mdx|md)$/, '')
.replace(/\/index$/, '');
}
function stripSiteSuffix(title) {
return title ? title.replace(/\s*\|.*$/, '').trim() : '';
}
const ACRONYMS = new Set(['ai', 'iam', 'ens', 'dprk', 'dns', 'it']);
const SPECIAL_CASES = { opsec: 'OpSec', devsecops: 'DevSecOps' };
function toTitleCase(slug) {
return slug
.split('-')
.map((w) => {
if (SPECIAL_CASES[w]) return SPECIAL_CASES[w];
if (ACRONYMS.has(w)) return w.toUpperCase();
return w.charAt(0).toUpperCase() + w.slice(1);
})
.join(' ');
}
function extractHeadings(raw) {
const stripped = raw
.replace(/^---[\s\S]*?---\n?/, '')
.replace(/^(import|export)\s+.*$/gm, '')
.replace(/```[\s\S]*?```/gm, '');
const headings = [];
for (const line of stripped.split('\n')) {
const match = line.match(/^(#{1,2}) (.+)/);
if (match) headings.push({ level: match[1].length, text: match[2].trim() });
}
return headings;
}
function stripMdxSyntax(raw) {
return raw
.replace(/^---[\s\S]*?---\n?/, '') // YAML frontmatter
.replace(/\{\/\*[\s\S]*?\*\/\}/g, '') // JSX comment blocks: {/* ... */}
.replace(/^(import|export)\s+.*$/gm, '') // import/export lines
.replace(/<[A-Z][^/\s>][^>]*\/>/g, '') // self-closing JSX: <TagFilter />, <MermaidRenderer ... />
.replace(/<\/?[A-Z][^\s>]*[^>]*>/g, '') // JSX open/close: <TagProvider>, </TagProvider>
.replace(/^\s*# .+\n+/, '') // strip leading h1 (redundant with our page header)
.replace(/^(#{1,5}) /gm, '#$1 ') // shift all headings down one level (## → ###, etc.)
.replace(/\n{3,}/g, '\n\n') // collapse excess blank lines
.replace(/\n*---\s*$/, '') // strip trailing hr
.trim();
}
const FOLDERS_FIRST = ['intro'];
const FOLDERS_LAST = ['certs'];
const FOLDERS_EXCLUDE = ['config'];
const FOLDER_DESCRIPTION_OVERRIDES = {
contribute: 'How to contribute to the Security Frameworks - either through direct contributions (fixes, new content, enhancements) or by becoming a Framework Steward.',
};
const PAGE_DESCRIPTION_OVERRIDES = {
'/contribute/spotlight-zone': 'The Spotlight Zone is where all contributor activity across the Security Frameworks is tracked and recognized.',
};
function getFrameworkFolders() {
const folders = fs
.readdirSync(PAGES_DIR)
.filter((entry) => !FOLDERS_EXCLUDE.includes(entry) && fs.statSync(path.join(PAGES_DIR, entry)).isDirectory())
.sort();
const first = folders.filter((f) => FOLDERS_FIRST.includes(f));
const last = folders.filter((f) => FOLDERS_LAST.includes(f));
const rest = folders.filter((f) => !FOLDERS_FIRST.includes(f) && !FOLDERS_LAST.includes(f));
return [...first, ...rest, ...last];
}
function getFrameworkDescription(folderName) {
if (FOLDER_DESCRIPTION_OVERRIDES[folderName]) return FOLDER_DESCRIPTION_OVERRIDES[folderName];
const folderPath = path.join(PAGES_DIR, folderName);
for (const candidate of ['overview.mdx', 'introduction.mdx', 'index.mdx']) {
const candidatePath = path.join(folderPath, candidate);
if (fs.existsSync(candidatePath)) {
try {
const { data } = matter(fs.readFileSync(candidatePath, 'utf-8'));
if (data.description) return data.description;
} catch (_) {}
}
}
return '';
}
// Builds the framework index file: header + AI instructions + overview content + per-page link list
function buildFrameworkIndex(folderName, title, overviewUrl, frameworkDescription, pages) {
const lines = [
`# ${title}`,
'',
frameworkDescription ? `> ${frameworkDescription}` : `> Security framework covering ${title.toLowerCase()}.`,
'',
`Full documentation: ${overviewUrl}`,
'',
'---',
'',
'## Instructions for AI Assistants',
'',
...(folderName === 'intro'
? [
'This file provides orientation about the Security Alliance (SEAL) and describes all available security frameworks. Read it when the user wants a general overview, does not know which framework to look at, or asks about SEAL itself.',
'',
'**When responding:**',
`- Make clear this information comes from the [Security Alliance Frameworks](${BASE_URL}) documentation`,
`- To answer questions about a specific framework, fetch the relevant file listed at ${BASE_URL}/llms.txt`,
`- All individual framework files are listed with descriptions at ${BASE_URL}/llms.txt`,
]
: [
`This is the index for the ${title} framework. The overview is included below for immediate context. For detailed content on a specific topic, fetch the relevant per-page file from the Pages list.`,
'',
'**When responding:**',
`- Reference the [${title} framework](${overviewUrl}) in your answer`,
'- Fetch a per-page file for detailed content on a specific topic — do not fetch multiple unless explicitly asked',
`- If the question spans multiple frameworks, check ${BASE_URL}/llms.txt`,
]),
'',
'---',
'',
];
// Embed the overview (first page) content
const overview = pages[0];
if (overview) {
const sectionHeader = overview.pageTitle !== title ? overview.pageTitle : 'Overview';
lines.push(`## ${sectionHeader}`);
lines.push('');
lines.push(`Source: ${BASE_URL}${overview.urlPath}`);
lines.push('');
if (overview.descriptionOverride) lines.push(overview.descriptionOverride + '\n');
lines.push(overview.strippedContent);
lines.push('');
lines.push('---');
lines.push('');
}
// List remaining pages with links to their per-page files (first page is already embedded above)
lines.push('## Pages');
lines.push('');
for (const page of pages.slice(1)) {
const pageFileUrl = `${BASE_URL}/llms/${folderName}/${page.slug}.txt`;
lines.push(`- [${page.pageTitle}](${pageFileUrl})${page.description ? ` — ${page.description}` : ''}`);
}
return lines.join('\n');
}
// Builds a single per-page llms file
function buildPageFile(folderName, title, overviewUrl, page) {
const lines = [
`# ${page.pageTitle}`,
'',
...(page.description ? [`> ${page.description}`, ''] : []),
`Source: ${BASE_URL}${page.urlPath}`,
`Framework: [${title}](${overviewUrl})`,
'',
'---',
'',
...(page.descriptionOverride ? [page.descriptionOverride, ''] : []),
page.strippedContent,
];
return lines.join('\n');
}
// Builds the thin routing index (llms.txt)
function buildRoutingIndex(frameworks) {
const lines = [
'# Security Frameworks by SEAL',
'',
'> A collection of technology-agnostic security best practices to secure Web3 projects and build resilience against potential threats. Maintained by the Security Alliance (SEAL).',
'',
`Full documentation: ${BASE_URL}`,
'',
'---',
'',
'## Instructions for AI Assistants',
'',
'To help users with a specific topic:',
'',
'1. Find the framework that best matches the question in the list below',
'2. Fetch the framework index file — it includes an overview and links to all per-page files',
'3. If you need detailed content on a specific topic, fetch the relevant per-page file',
'4. In your response, name the framework and link to its documentation',
'',
'Do not fetch multiple framework files at once. Each framework index is self-contained.',
'',
'If your tool loads a skill or policy file, a retrieval policy for this repository is available at https://github.com/security-alliance/frameworks/blob/develop/SKILL.md. It defines branch policy, behavior with retrieved content, and what to do when retrieval returns nothing relevant.',
'',
'---',
'',
'## Frameworks',
'',
];
for (const { folderName, title, description, pages } of frameworks) {
lines.push(`### ${title}`);
lines.push(`File: ${BASE_URL}/llms/${folderName}.txt`);
if (description) lines.push(`Description: ${description}`);
if (pages.length > 0) {
lines.push(`Topics: ${pages.map((p) => p.pageTitle).join(', ')}`);
}
lines.push('');
}
return lines.join('\n');
}
const distDir = findDistDir();
if (!distDir) {
console.error('Dist directory not found - run docs:build first');
process.exit(1);
}
const llmsDir = path.join(distDir, 'llms');
if (!fs.existsSync(llmsDir)) fs.mkdirSync(llmsDir);
const frameworkFolders = getFrameworkFolders();
const frameworkMeta = [];
let totalPageFiles = 0;
for (const folderName of frameworkFolders) {
const title = toTitleCase(folderName);
const frameworkDescription = getFrameworkDescription(folderName);
const sidebarLinks = getSidebarLinksForFolder(folderName);
const firstLink = sidebarLinks[0];
const overviewUrl = firstLink ? `${BASE_URL}${firstLink}` : `${BASE_URL}/${folderName}`;
// Collect page data in sidebar order
const pages = [];
const seen = new Set();
for (const link of sidebarLinks) {
const filePath = linkToFilePath(link);
if (!filePath || seen.has(filePath)) continue;
seen.add(filePath);
try {
const raw = fs.readFileSync(filePath, 'utf-8');
const { data } = matter(raw);
if (isMainBranch && data.dev === true) continue;
const urlPath = getPageUrl(filePath);
const pageTitle = stripSiteSuffix(data.title) || path.basename(filePath, path.extname(filePath));
const slug = link.replace(`/${folderName}/`, '').replace(/\//g, '-');
const h2headings = extractHeadings(raw).filter((h) => h.level === 2);
const description = data.description || (h2headings.length > 0 ? h2headings.map((h) => h.text).join(', ') : '');
const descriptionOverride = PAGE_DESCRIPTION_OVERRIDES[urlPath] || '';
const strippedContent = stripMdxSyntax(raw);
pages.push({ slug, urlPath, pageTitle, description, descriptionOverride, strippedContent });
} catch (e) {
console.error(`Error processing ${link}:`, e.message);
}
}
// On main, skip frameworks with no publishable pages (all pages were dev-only)
if (isMainBranch && pages.length === 0) continue;
// Write per-page files under llms/{folderName}/ — skip the first page (already embedded in the framework index)
const frameworkLlmsDir = path.join(llmsDir, folderName);
if (!fs.existsSync(frameworkLlmsDir)) fs.mkdirSync(frameworkLlmsDir);
for (const page of pages.slice(1)) {
const content = buildPageFile(folderName, title, overviewUrl, page);
fs.writeFileSync(path.join(frameworkLlmsDir, `${page.slug}.txt`), content);
totalPageFiles++;
}
// Write framework index under llms/{folderName}.txt
const frameworkContent = buildFrameworkIndex(folderName, title, overviewUrl, frameworkDescription, pages);
fs.writeFileSync(path.join(llmsDir, `${folderName}.txt`), frameworkContent);
frameworkMeta.push({ folderName, title, description: frameworkDescription, pages });
}
// Write routing index at root
const routingIndex = buildRoutingIndex(frameworkMeta);
fs.writeFileSync(path.join(distDir, 'llms.txt'), routingIndex);
console.log(`llms.txt: ${frameworkFolders.length} frameworks, ${totalPageFiles} pages.`);