skrypt-ai 0.6.0 → 0.7.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.
- package/dist/audit/doc-parser.d.ts +5 -0
- package/dist/audit/doc-parser.js +106 -0
- package/dist/audit/index.d.ts +4 -0
- package/dist/audit/index.js +4 -0
- package/dist/audit/matcher.d.ts +6 -0
- package/dist/audit/matcher.js +94 -0
- package/dist/audit/reporter.d.ts +9 -0
- package/dist/audit/reporter.js +106 -0
- package/dist/audit/types.d.ts +37 -0
- package/dist/audit/types.js +1 -0
- package/dist/auth/index.js +3 -1
- package/dist/cli.js +11 -1
- package/dist/commands/audit.d.ts +2 -0
- package/dist/commands/audit.js +59 -0
- package/dist/commands/config.d.ts +2 -0
- package/dist/commands/config.js +73 -0
- package/dist/commands/cron.js +4 -0
- package/dist/commands/generate.d.ts +7 -0
- package/dist/commands/generate.js +528 -234
- package/dist/commands/refresh.d.ts +2 -0
- package/dist/commands/refresh.js +158 -0
- package/dist/commands/review-pr.js +5 -0
- package/dist/commands/review.d.ts +2 -0
- package/dist/commands/review.js +110 -0
- package/dist/commands/test.js +177 -236
- package/dist/commands/watch.js +29 -20
- package/dist/config/loader.d.ts +6 -1
- package/dist/config/loader.js +38 -2
- package/dist/config/types.d.ts +7 -0
- package/dist/generator/generator.js +2 -1
- package/dist/generator/types.d.ts +3 -0
- package/dist/generator/writer.js +60 -28
- package/dist/github/org-discovery.d.ts +17 -0
- package/dist/github/org-discovery.js +93 -0
- package/dist/llm/index.d.ts +2 -0
- package/dist/llm/index.js +8 -2
- package/dist/next-actions/actions.d.ts +2 -0
- package/dist/next-actions/actions.js +190 -0
- package/dist/next-actions/index.d.ts +6 -0
- package/dist/next-actions/index.js +39 -0
- package/dist/next-actions/setup.d.ts +2 -0
- package/dist/next-actions/setup.js +72 -0
- package/dist/next-actions/state.d.ts +7 -0
- package/dist/next-actions/state.js +68 -0
- package/dist/next-actions/suggest.d.ts +3 -0
- package/dist/next-actions/suggest.js +47 -0
- package/dist/next-actions/types.d.ts +26 -0
- package/dist/next-actions/types.js +1 -0
- package/dist/refresh/differ.d.ts +9 -0
- package/dist/refresh/differ.js +67 -0
- package/dist/refresh/index.d.ts +4 -0
- package/dist/refresh/index.js +4 -0
- package/dist/refresh/manifest.d.ts +18 -0
- package/dist/refresh/manifest.js +71 -0
- package/dist/refresh/splicer.d.ts +9 -0
- package/dist/refresh/splicer.js +50 -0
- package/dist/refresh/types.d.ts +37 -0
- package/dist/refresh/types.js +1 -0
- package/dist/review/index.d.ts +8 -0
- package/dist/review/index.js +94 -0
- package/dist/review/parser.d.ts +16 -0
- package/dist/review/parser.js +95 -0
- package/dist/review/types.d.ts +18 -0
- package/dist/review/types.js +1 -0
- package/dist/scanner/types.d.ts +2 -0
- package/dist/structure/index.d.ts +19 -0
- package/dist/structure/index.js +92 -0
- package/dist/structure/planner.d.ts +8 -0
- package/dist/structure/planner.js +180 -0
- package/dist/structure/topology.d.ts +16 -0
- package/dist/structure/topology.js +49 -0
- package/dist/structure/types.d.ts +26 -0
- package/dist/structure/types.js +1 -0
- package/dist/testing/comparator.d.ts +7 -0
- package/dist/testing/comparator.js +77 -0
- package/dist/testing/docker.d.ts +21 -0
- package/dist/testing/docker.js +234 -0
- package/dist/testing/env.d.ts +16 -0
- package/dist/testing/env.js +58 -0
- package/dist/testing/extractor.d.ts +9 -0
- package/dist/testing/extractor.js +195 -0
- package/dist/testing/index.d.ts +6 -0
- package/dist/testing/index.js +6 -0
- package/dist/testing/runner.d.ts +5 -0
- package/dist/testing/runner.js +225 -0
- package/dist/testing/types.d.ts +58 -0
- package/dist/testing/types.js +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { readFileSync, readdirSync, statSync } from 'fs';
|
|
2
|
+
import { join, extname } from 'path';
|
|
3
|
+
/**
|
|
4
|
+
* Parse a feedback markdown file.
|
|
5
|
+
*
|
|
6
|
+
* Format:
|
|
7
|
+
* ## docs/api/users.md
|
|
8
|
+
* - Line 42: Use the SDK client.get() method, not raw fetch()
|
|
9
|
+
* - General: Too formal, make it conversational
|
|
10
|
+
*/
|
|
11
|
+
export function parseFeedbackFile(feedbackPath) {
|
|
12
|
+
const content = readFileSync(feedbackPath, 'utf-8');
|
|
13
|
+
const lines = content.split('\n');
|
|
14
|
+
const feedback = [];
|
|
15
|
+
let currentFile = null;
|
|
16
|
+
for (const line of lines) {
|
|
17
|
+
// ## path/to/file.md
|
|
18
|
+
const fileMatch = line.match(/^##\s+(.+\.(?:md|mdx))\s*$/);
|
|
19
|
+
if (fileMatch) {
|
|
20
|
+
currentFile = fileMatch[1].trim();
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
if (!currentFile)
|
|
24
|
+
continue;
|
|
25
|
+
// - Line 42: feedback text
|
|
26
|
+
const lineMatch = line.match(/^-\s+Line\s+(\d+)\s*:\s*(.+)$/);
|
|
27
|
+
if (lineMatch) {
|
|
28
|
+
feedback.push({
|
|
29
|
+
filePath: currentFile,
|
|
30
|
+
lineNumber: parseInt(lineMatch[1], 10),
|
|
31
|
+
feedback: lineMatch[2].trim(),
|
|
32
|
+
source: 'file',
|
|
33
|
+
});
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
// - "elementName": feedback text
|
|
37
|
+
const elementMatch = line.match(/^-\s+"([^"]+)"\s*:\s*(.+)$/);
|
|
38
|
+
if (elementMatch) {
|
|
39
|
+
feedback.push({
|
|
40
|
+
filePath: currentFile,
|
|
41
|
+
elementName: elementMatch[1].trim(),
|
|
42
|
+
feedback: elementMatch[2].trim(),
|
|
43
|
+
source: 'file',
|
|
44
|
+
});
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
// - General: feedback text or - feedback text
|
|
48
|
+
const generalMatch = line.match(/^-\s+(?:General\s*:\s*)?(.+)$/);
|
|
49
|
+
if (generalMatch) {
|
|
50
|
+
feedback.push({
|
|
51
|
+
filePath: currentFile,
|
|
52
|
+
feedback: generalMatch[1].trim(),
|
|
53
|
+
source: 'file',
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return feedback;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Scan markdown files for inline FIXME/TODO comments.
|
|
61
|
+
*
|
|
62
|
+
* Looks for: <!-- FIXME: ... --> or <!-- TODO: ... -->
|
|
63
|
+
*/
|
|
64
|
+
export function parseInlineComments(docsDir) {
|
|
65
|
+
const feedback = [];
|
|
66
|
+
function walk(dir) {
|
|
67
|
+
const entries = readdirSync(dir);
|
|
68
|
+
for (const entry of entries) {
|
|
69
|
+
const fullPath = join(dir, entry);
|
|
70
|
+
const stat = statSync(fullPath);
|
|
71
|
+
if (stat.isDirectory() && !entry.startsWith('.') && entry !== 'node_modules') {
|
|
72
|
+
walk(fullPath);
|
|
73
|
+
}
|
|
74
|
+
else if (stat.isFile() && (extname(entry) === '.md' || extname(entry) === '.mdx')) {
|
|
75
|
+
const content = readFileSync(fullPath, 'utf-8');
|
|
76
|
+
const lines = content.split('\n');
|
|
77
|
+
for (let i = 0; i < lines.length; i++) {
|
|
78
|
+
const line = lines[i];
|
|
79
|
+
// <!-- FIXME: ... --> or <!-- TODO: ... -->
|
|
80
|
+
const commentMatch = line.match(/<!--\s*(?:FIXME|TODO)\s*:\s*(.*?)\s*-->/);
|
|
81
|
+
if (commentMatch) {
|
|
82
|
+
feedback.push({
|
|
83
|
+
filePath: fullPath,
|
|
84
|
+
lineNumber: i + 1,
|
|
85
|
+
feedback: commentMatch[1].trim(),
|
|
86
|
+
source: 'inline',
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
walk(docsDir);
|
|
94
|
+
return feedback;
|
|
95
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A piece of feedback targeting a specific doc file or element
|
|
3
|
+
*/
|
|
4
|
+
export interface ReviewFeedback {
|
|
5
|
+
filePath: string;
|
|
6
|
+
lineNumber?: number;
|
|
7
|
+
elementName?: string;
|
|
8
|
+
feedback: string;
|
|
9
|
+
source: 'file' | 'inline';
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Result of processing a single feedback item
|
|
13
|
+
*/
|
|
14
|
+
export interface ReviewResult {
|
|
15
|
+
feedback: ReviewFeedback;
|
|
16
|
+
applied: boolean;
|
|
17
|
+
error?: string;
|
|
18
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/scanner/types.d.ts
CHANGED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { APIElement } from '../scanner/types.js';
|
|
2
|
+
import { LLMClient } from '../llm/types.js';
|
|
3
|
+
import { GenerationOptions, GeneratedDoc } from '../generator/types.js';
|
|
4
|
+
import { DocStructure } from './types.js';
|
|
5
|
+
export * from './types.js';
|
|
6
|
+
export { analyzeTopology } from './topology.js';
|
|
7
|
+
export { planStructure } from './planner.js';
|
|
8
|
+
/**
|
|
9
|
+
* Plan a smart documentation structure using LLM analysis
|
|
10
|
+
*/
|
|
11
|
+
export declare function planSmartStructure(elements: APIElement[], client: LLMClient): Promise<DocStructure>;
|
|
12
|
+
/**
|
|
13
|
+
* Generate documentation following a smart structure plan
|
|
14
|
+
*/
|
|
15
|
+
export declare function generateWithStructure(structure: DocStructure, client: LLMClient, outputPath: string, options: GenerationOptions): Promise<{
|
|
16
|
+
filesWritten: number;
|
|
17
|
+
totalDocs: number;
|
|
18
|
+
docs: GeneratedDoc[];
|
|
19
|
+
}>;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { writeFileSync, mkdirSync } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import { generateForElements } from '../generator/generator.js';
|
|
4
|
+
import { analyzeTopology } from './topology.js';
|
|
5
|
+
import { planStructure } from './planner.js';
|
|
6
|
+
export * from './types.js';
|
|
7
|
+
export { analyzeTopology } from './topology.js';
|
|
8
|
+
export { planStructure } from './planner.js';
|
|
9
|
+
/**
|
|
10
|
+
* Plan a smart documentation structure using LLM analysis
|
|
11
|
+
*/
|
|
12
|
+
export async function planSmartStructure(elements, client) {
|
|
13
|
+
const topology = analyzeTopology(elements);
|
|
14
|
+
return planStructure(elements, topology, client);
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Generate documentation following a smart structure plan
|
|
18
|
+
*/
|
|
19
|
+
export async function generateWithStructure(structure, client, outputPath, options) {
|
|
20
|
+
let filesWritten = 0;
|
|
21
|
+
let totalDocs = 0;
|
|
22
|
+
const allDocs = [];
|
|
23
|
+
// Create category directories
|
|
24
|
+
const categoryDirs = {
|
|
25
|
+
quickstart: outputPath,
|
|
26
|
+
concepts: join(outputPath, 'concepts'),
|
|
27
|
+
guides: join(outputPath, 'guides'),
|
|
28
|
+
api: join(outputPath, 'api'),
|
|
29
|
+
};
|
|
30
|
+
for (const dir of Object.values(categoryDirs)) {
|
|
31
|
+
mkdirSync(dir, { recursive: true });
|
|
32
|
+
}
|
|
33
|
+
// Generate docs page by page
|
|
34
|
+
for (const page of structure.pages) {
|
|
35
|
+
if (page.elements.length === 0)
|
|
36
|
+
continue;
|
|
37
|
+
const dir = categoryDirs[page.category] || outputPath;
|
|
38
|
+
const filePath = join(dir, `${page.slug}.md`);
|
|
39
|
+
// Generate docs for all elements on this page
|
|
40
|
+
const docs = await generateForElements(page.elements, client, {
|
|
41
|
+
...options,
|
|
42
|
+
onProgress: (progress) => {
|
|
43
|
+
options.onProgress?.({
|
|
44
|
+
...progress,
|
|
45
|
+
element: `${page.title}: ${progress.element}`,
|
|
46
|
+
});
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
// Write the page
|
|
50
|
+
const content = buildPageContent(page, docs);
|
|
51
|
+
writeFileSync(filePath, content);
|
|
52
|
+
filesWritten++;
|
|
53
|
+
totalDocs += docs.length;
|
|
54
|
+
allDocs.push(...docs);
|
|
55
|
+
}
|
|
56
|
+
// Write navigation file
|
|
57
|
+
mkdirSync(join(outputPath, '.skrypt'), { recursive: true });
|
|
58
|
+
const navContent = JSON.stringify(structure.navigation, null, 2);
|
|
59
|
+
writeFileSync(join(outputPath, '.skrypt', 'navigation.json'), navContent);
|
|
60
|
+
return { filesWritten, totalDocs, docs: allDocs };
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Build markdown content for a smart-structured page
|
|
64
|
+
*/
|
|
65
|
+
function buildPageContent(page, docs) {
|
|
66
|
+
let content = `---\ntitle: ${page.title}\ndescription: ${page.description}\n---\n\n`;
|
|
67
|
+
content += `# ${page.title}\n\n`;
|
|
68
|
+
content += `${page.description}\n\n`;
|
|
69
|
+
for (const doc of docs) {
|
|
70
|
+
if (doc.error)
|
|
71
|
+
continue;
|
|
72
|
+
content += `## \`${doc.element.name}\`\n\n`;
|
|
73
|
+
content += `\`\`\`${doc.codeLanguage}\n${doc.element.signature}\n\`\`\`\n\n`;
|
|
74
|
+
if (doc.markdown) {
|
|
75
|
+
content += doc.markdown + '\n\n';
|
|
76
|
+
}
|
|
77
|
+
const hasMultiLang = doc.typescriptExample && doc.pythonExample;
|
|
78
|
+
if (hasMultiLang) {
|
|
79
|
+
content += '**Examples:**\n\n';
|
|
80
|
+
content += '<CodeGroup>\n\n';
|
|
81
|
+
content += `\`\`\`typescript example.ts\n${doc.typescriptExample}\n\`\`\`\n\n`;
|
|
82
|
+
content += `\`\`\`python example.py\n${doc.pythonExample}\n\`\`\`\n\n`;
|
|
83
|
+
content += '</CodeGroup>\n\n';
|
|
84
|
+
}
|
|
85
|
+
else if (doc.codeExample) {
|
|
86
|
+
content += '**Example:**\n\n';
|
|
87
|
+
content += `\`\`\`${doc.codeLanguage}\n${doc.codeExample}\n\`\`\`\n\n`;
|
|
88
|
+
}
|
|
89
|
+
content += '---\n\n';
|
|
90
|
+
}
|
|
91
|
+
return content;
|
|
92
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { APIElement } from '../scanner/types.js';
|
|
2
|
+
import { LLMClient } from '../llm/types.js';
|
|
3
|
+
import { DocStructure } from './types.js';
|
|
4
|
+
import { TopologyAnalysis } from './topology.js';
|
|
5
|
+
/**
|
|
6
|
+
* Use LLM to plan a smart documentation structure
|
|
7
|
+
*/
|
|
8
|
+
export declare function planStructure(elements: APIElement[], topology: TopologyAnalysis, client: LLMClient): Promise<DocStructure>;
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Use LLM to plan a smart documentation structure
|
|
3
|
+
*/
|
|
4
|
+
export async function planStructure(elements, topology, client) {
|
|
5
|
+
// Build a summary of the API surface for the LLM
|
|
6
|
+
const summary = buildAPISummary(elements, topology);
|
|
7
|
+
const response = await client.complete({
|
|
8
|
+
messages: [
|
|
9
|
+
{
|
|
10
|
+
role: 'system',
|
|
11
|
+
content: STRUCTURE_PLANNER_PROMPT,
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
role: 'user',
|
|
15
|
+
content: summary,
|
|
16
|
+
},
|
|
17
|
+
],
|
|
18
|
+
temperature: 0,
|
|
19
|
+
maxTokens: 4096,
|
|
20
|
+
});
|
|
21
|
+
return parseStructurePlan(response.content, elements);
|
|
22
|
+
}
|
|
23
|
+
const STRUCTURE_PLANNER_PROMPT = `You are an expert documentation architect. Given a list of API elements, plan a documentation structure organized by user journey — NOT by file.
|
|
24
|
+
|
|
25
|
+
## Output Structure
|
|
26
|
+
|
|
27
|
+
Return a JSON object with this shape:
|
|
28
|
+
\`\`\`json
|
|
29
|
+
{
|
|
30
|
+
"pages": [
|
|
31
|
+
{
|
|
32
|
+
"slug": "quickstart",
|
|
33
|
+
"title": "Quickstart",
|
|
34
|
+
"category": "quickstart",
|
|
35
|
+
"description": "Get from zero to first API call in 5 minutes",
|
|
36
|
+
"elementNames": ["createClient", "configure"]
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"slug": "authentication",
|
|
40
|
+
"title": "Authentication",
|
|
41
|
+
"category": "concepts",
|
|
42
|
+
"description": "How auth works — API keys, OAuth, sessions",
|
|
43
|
+
"elementNames": ["authenticate", "refreshToken", "SessionManager"]
|
|
44
|
+
}
|
|
45
|
+
]
|
|
46
|
+
}
|
|
47
|
+
\`\`\`
|
|
48
|
+
|
|
49
|
+
## Rules
|
|
50
|
+
1. ALWAYS start with a "quickstart" page covering entry points
|
|
51
|
+
2. Group by CONCEPT (auth, data model, errors), not by file
|
|
52
|
+
3. Put classes and their methods together on one page
|
|
53
|
+
4. "api" category pages are per-class or per-module API reference
|
|
54
|
+
5. "guides" are task-oriented: "Setting up webhooks", "Testing your integration"
|
|
55
|
+
6. Keep slugs lowercase-kebab-case
|
|
56
|
+
7. Every element must appear in exactly one page
|
|
57
|
+
8. Return ONLY the JSON, no markdown fences, no explanation`;
|
|
58
|
+
function buildAPISummary(elements, topology) {
|
|
59
|
+
let summary = `## API Surface (${elements.length} elements)\n\n`;
|
|
60
|
+
if (topology.entryPoints.length > 0) {
|
|
61
|
+
summary += `### Entry Points\n`;
|
|
62
|
+
for (const ep of topology.entryPoints) {
|
|
63
|
+
summary += `- ${ep.kind} \`${ep.name}\` (${ep.filePath})\n`;
|
|
64
|
+
}
|
|
65
|
+
summary += '\n';
|
|
66
|
+
}
|
|
67
|
+
summary += `### Public API (${topology.publicAPI.length} elements)\n`;
|
|
68
|
+
for (const el of topology.publicAPI.slice(0, 100)) {
|
|
69
|
+
const parent = el.parentClass ? ` (${el.parentClass})` : '';
|
|
70
|
+
summary += `- ${el.kind} \`${el.name}\`${parent}: ${el.signature.slice(0, 80)}\n`;
|
|
71
|
+
}
|
|
72
|
+
if (topology.publicAPI.length > 100) {
|
|
73
|
+
summary += `... and ${topology.publicAPI.length - 100} more\n`;
|
|
74
|
+
}
|
|
75
|
+
if (topology.classClusters.size > 0) {
|
|
76
|
+
summary += `\n### Classes (${topology.classClusters.size})\n`;
|
|
77
|
+
for (const [className, methods] of topology.classClusters) {
|
|
78
|
+
summary += `- \`${className}\` with methods: ${methods.map(m => m.name).join(', ')}\n`;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return summary;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Parse LLM response into DocStructure
|
|
85
|
+
*/
|
|
86
|
+
function parseStructurePlan(content, elements) {
|
|
87
|
+
// Extract JSON from response
|
|
88
|
+
let jsonStr = content.trim();
|
|
89
|
+
// Remove markdown fences if present
|
|
90
|
+
jsonStr = jsonStr.replace(/^```(?:json)?\s*\n?/, '').replace(/\n?```\s*$/, '');
|
|
91
|
+
let parsed;
|
|
92
|
+
try {
|
|
93
|
+
parsed = JSON.parse(jsonStr);
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
// Fallback: create a simple structure
|
|
97
|
+
return createFallbackStructure(elements);
|
|
98
|
+
}
|
|
99
|
+
// Map element names to actual elements (case-insensitive for LLM tolerance)
|
|
100
|
+
const elementByName = new Map();
|
|
101
|
+
const elementByNameLower = new Map();
|
|
102
|
+
for (const el of elements) {
|
|
103
|
+
elementByName.set(el.name, el);
|
|
104
|
+
elementByNameLower.set(el.name.toLowerCase(), el);
|
|
105
|
+
}
|
|
106
|
+
const pages = [];
|
|
107
|
+
const assignedElements = new Set();
|
|
108
|
+
for (const page of parsed.pages) {
|
|
109
|
+
const pageElements = [];
|
|
110
|
+
for (const name of page.elementNames || []) {
|
|
111
|
+
// Try exact match first, then case-insensitive
|
|
112
|
+
let el = elementByName.get(name);
|
|
113
|
+
if (!el) {
|
|
114
|
+
el = elementByNameLower.get(name.toLowerCase());
|
|
115
|
+
if (el) {
|
|
116
|
+
console.log(` Note: LLM returned "${name}", matched to "${el.name}" (case-insensitive)`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if (el) {
|
|
120
|
+
pageElements.push(el);
|
|
121
|
+
assignedElements.add(el.name);
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
console.log(` Warning: LLM referenced unknown element "${name}" — skipping`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
const category = (['quickstart', 'concepts', 'guides', 'api'].includes(page.category)
|
|
128
|
+
? page.category
|
|
129
|
+
: 'api');
|
|
130
|
+
pages.push({
|
|
131
|
+
slug: page.slug,
|
|
132
|
+
title: page.title,
|
|
133
|
+
category,
|
|
134
|
+
description: page.description,
|
|
135
|
+
elements: pageElements,
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
// Add unassigned elements to an "API Reference" catch-all
|
|
139
|
+
const unassigned = elements.filter(el => !assignedElements.has(el.name));
|
|
140
|
+
if (unassigned.length > 0) {
|
|
141
|
+
pages.push({
|
|
142
|
+
slug: 'api-reference',
|
|
143
|
+
title: 'API Reference',
|
|
144
|
+
category: 'api',
|
|
145
|
+
description: 'Complete API reference',
|
|
146
|
+
elements: unassigned,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
// Build navigation
|
|
150
|
+
const navigation = buildNavigation(pages);
|
|
151
|
+
return { pages, navigation };
|
|
152
|
+
}
|
|
153
|
+
function createFallbackStructure(elements) {
|
|
154
|
+
const pages = [{
|
|
155
|
+
slug: 'api-reference',
|
|
156
|
+
title: 'API Reference',
|
|
157
|
+
category: 'api',
|
|
158
|
+
description: 'Complete API reference',
|
|
159
|
+
elements,
|
|
160
|
+
}];
|
|
161
|
+
return {
|
|
162
|
+
pages,
|
|
163
|
+
navigation: [{ title: 'API Reference', slug: 'api-reference' }],
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
function buildNavigation(pages) {
|
|
167
|
+
const categories = {
|
|
168
|
+
quickstart: { title: 'Getting Started', slug: '', children: [] },
|
|
169
|
+
concepts: { title: 'Concepts', slug: '', children: [] },
|
|
170
|
+
guides: { title: 'Guides', slug: '', children: [] },
|
|
171
|
+
api: { title: 'API Reference', slug: '', children: [] },
|
|
172
|
+
};
|
|
173
|
+
for (const page of pages) {
|
|
174
|
+
const cat = categories[page.category];
|
|
175
|
+
if (cat) {
|
|
176
|
+
cat.children.push({ title: page.title, slug: page.slug });
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return Object.values(categories).filter(c => c.children.length > 0);
|
|
180
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { APIElement } from '../scanner/types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Analysis of a codebase's API surface
|
|
4
|
+
*/
|
|
5
|
+
export interface TopologyAnalysis {
|
|
6
|
+
entryPoints: APIElement[];
|
|
7
|
+
publicAPI: APIElement[];
|
|
8
|
+
internalElements: APIElement[];
|
|
9
|
+
classClusters: Map<string, APIElement[]>;
|
|
10
|
+
fileClusters: Map<string, APIElement[]>;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Analyze the topology of a set of API elements.
|
|
14
|
+
* Identifies entry points, public surface, class groupings, and file clusters.
|
|
15
|
+
*/
|
|
16
|
+
export declare function analyzeTopology(elements: APIElement[]): TopologyAnalysis;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Analyze the topology of a set of API elements.
|
|
3
|
+
* Identifies entry points, public surface, class groupings, and file clusters.
|
|
4
|
+
*/
|
|
5
|
+
export function analyzeTopology(elements) {
|
|
6
|
+
const entryPoints = [];
|
|
7
|
+
const publicAPI = [];
|
|
8
|
+
const internalElements = [];
|
|
9
|
+
const classClusters = new Map();
|
|
10
|
+
const fileClusters = new Map();
|
|
11
|
+
for (const el of elements) {
|
|
12
|
+
// Classify by visibility
|
|
13
|
+
if (el.isExported || el.isPublic) {
|
|
14
|
+
publicAPI.push(el);
|
|
15
|
+
}
|
|
16
|
+
else {
|
|
17
|
+
internalElements.push(el);
|
|
18
|
+
}
|
|
19
|
+
// Identify likely entry points (exported top-level functions/classes)
|
|
20
|
+
if ((el.isExported || el.isPublic) && !el.parentClass && (el.kind === 'function' || el.kind === 'class')) {
|
|
21
|
+
// Heuristic: entry points are often in index.ts, main.ts, or have names like init, create, setup
|
|
22
|
+
const isEntryFile = /(?:index|main|app|server|client)\.\w+$/.test(el.filePath);
|
|
23
|
+
const isEntryName = /^(?:create|init|setup|configure|connect|start|build)/i.test(el.name);
|
|
24
|
+
if (isEntryFile || isEntryName) {
|
|
25
|
+
entryPoints.push(el);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
// Group by parent class
|
|
29
|
+
if (el.parentClass) {
|
|
30
|
+
if (!classClusters.has(el.parentClass)) {
|
|
31
|
+
classClusters.set(el.parentClass, []);
|
|
32
|
+
}
|
|
33
|
+
classClusters.get(el.parentClass).push(el);
|
|
34
|
+
}
|
|
35
|
+
// Group by source file
|
|
36
|
+
const fileKey = el.filePath.replace(/\.(ts|js|py|go|rs|java|cs|php|kt|swift|rb)x?$/, '');
|
|
37
|
+
if (!fileClusters.has(fileKey)) {
|
|
38
|
+
fileClusters.set(fileKey, []);
|
|
39
|
+
}
|
|
40
|
+
fileClusters.get(fileKey).push(el);
|
|
41
|
+
}
|
|
42
|
+
return {
|
|
43
|
+
entryPoints,
|
|
44
|
+
publicAPI,
|
|
45
|
+
internalElements,
|
|
46
|
+
classClusters,
|
|
47
|
+
fileClusters,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { APIElement } from '../scanner/types.js';
|
|
2
|
+
/**
|
|
3
|
+
* A planned page in the smart-structured docs
|
|
4
|
+
*/
|
|
5
|
+
export interface PagePlan {
|
|
6
|
+
slug: string;
|
|
7
|
+
title: string;
|
|
8
|
+
category: 'quickstart' | 'concepts' | 'guides' | 'api';
|
|
9
|
+
description: string;
|
|
10
|
+
elements: APIElement[];
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Full documentation structure plan
|
|
14
|
+
*/
|
|
15
|
+
export interface DocStructure {
|
|
16
|
+
pages: PagePlan[];
|
|
17
|
+
navigation: NavigationNode[];
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Navigation tree node
|
|
21
|
+
*/
|
|
22
|
+
export interface NavigationNode {
|
|
23
|
+
title: string;
|
|
24
|
+
slug: string;
|
|
25
|
+
children?: NavigationNode[];
|
|
26
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compare actual output against expected output
|
|
3
|
+
*/
|
|
4
|
+
export function compareOutput(actual, expected, mode = 'exact') {
|
|
5
|
+
const normalizedActual = normalize(actual);
|
|
6
|
+
const normalizedExpected = normalize(expected);
|
|
7
|
+
if (mode === 'contains') {
|
|
8
|
+
const match = normalizedActual.includes(normalizedExpected);
|
|
9
|
+
if (match) {
|
|
10
|
+
return { match: true, diff: '' };
|
|
11
|
+
}
|
|
12
|
+
return {
|
|
13
|
+
match: false,
|
|
14
|
+
diff: buildDiff(normalizedExpected, normalizedActual, 'contains'),
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
// Exact mode
|
|
18
|
+
if (normalizedActual === normalizedExpected) {
|
|
19
|
+
return { match: true, diff: '' };
|
|
20
|
+
}
|
|
21
|
+
return {
|
|
22
|
+
match: false,
|
|
23
|
+
diff: buildDiff(normalizedExpected, normalizedActual, 'exact'),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Normalize output for comparison: trim whitespace, normalize line endings
|
|
28
|
+
*/
|
|
29
|
+
function normalize(s) {
|
|
30
|
+
return s
|
|
31
|
+
.replace(/\r\n/g, '\n')
|
|
32
|
+
.replace(/\r/g, '\n')
|
|
33
|
+
.trim();
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Build a unified-style diff for display
|
|
37
|
+
*/
|
|
38
|
+
function buildDiff(expected, actual, mode) {
|
|
39
|
+
const expectedLines = expected.split('\n');
|
|
40
|
+
const actualLines = actual.split('\n');
|
|
41
|
+
const lines = [];
|
|
42
|
+
if (mode === 'contains') {
|
|
43
|
+
lines.push('--- expected (contains) ---');
|
|
44
|
+
lines.push('+++ actual +++');
|
|
45
|
+
lines.push('');
|
|
46
|
+
for (const line of expectedLines) {
|
|
47
|
+
lines.push(`- ${line}`);
|
|
48
|
+
}
|
|
49
|
+
lines.push('---');
|
|
50
|
+
for (const line of actualLines) {
|
|
51
|
+
lines.push(`+ ${line}`);
|
|
52
|
+
}
|
|
53
|
+
return lines.join('\n');
|
|
54
|
+
}
|
|
55
|
+
lines.push('--- expected ---');
|
|
56
|
+
lines.push('+++ actual +++');
|
|
57
|
+
lines.push('');
|
|
58
|
+
const maxLen = Math.max(expectedLines.length, actualLines.length);
|
|
59
|
+
for (let i = 0; i < maxLen; i++) {
|
|
60
|
+
const exp = expectedLines[i];
|
|
61
|
+
const act = actualLines[i];
|
|
62
|
+
if (exp === undefined) {
|
|
63
|
+
lines.push(`+ ${act}`);
|
|
64
|
+
}
|
|
65
|
+
else if (act === undefined) {
|
|
66
|
+
lines.push(`- ${exp}`);
|
|
67
|
+
}
|
|
68
|
+
else if (exp !== act) {
|
|
69
|
+
lines.push(`- ${exp}`);
|
|
70
|
+
lines.push(`+ ${act}`);
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
lines.push(` ${exp}`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return lines.join('\n');
|
|
77
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { ExtractedSnippet, TestResult, DockerEnvironment, RunnerConfig } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Available Docker environments for multi-env testing
|
|
4
|
+
*/
|
|
5
|
+
export declare const DOCKER_ENVIRONMENTS: DockerEnvironment[];
|
|
6
|
+
/**
|
|
7
|
+
* Check if Docker is available on this machine
|
|
8
|
+
*/
|
|
9
|
+
export declare function isDockerAvailable(): boolean;
|
|
10
|
+
/**
|
|
11
|
+
* Parse a comma-separated environments string into DockerEnvironment objects
|
|
12
|
+
*/
|
|
13
|
+
export declare function parseEnvironments(envString: string): DockerEnvironment[];
|
|
14
|
+
/**
|
|
15
|
+
* Get compatible Docker environments for a given language
|
|
16
|
+
*/
|
|
17
|
+
export declare function getCompatibleEnvironments(language: string, environments: DockerEnvironment[]): DockerEnvironment[];
|
|
18
|
+
/**
|
|
19
|
+
* Run a snippet in a Docker container
|
|
20
|
+
*/
|
|
21
|
+
export declare function runInDocker(snippet: ExtractedSnippet, environment: DockerEnvironment, config: RunnerConfig): Promise<TestResult>;
|