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.
Files changed (88) hide show
  1. package/dist/audit/doc-parser.d.ts +5 -0
  2. package/dist/audit/doc-parser.js +106 -0
  3. package/dist/audit/index.d.ts +4 -0
  4. package/dist/audit/index.js +4 -0
  5. package/dist/audit/matcher.d.ts +6 -0
  6. package/dist/audit/matcher.js +94 -0
  7. package/dist/audit/reporter.d.ts +9 -0
  8. package/dist/audit/reporter.js +106 -0
  9. package/dist/audit/types.d.ts +37 -0
  10. package/dist/audit/types.js +1 -0
  11. package/dist/auth/index.js +3 -1
  12. package/dist/cli.js +11 -1
  13. package/dist/commands/audit.d.ts +2 -0
  14. package/dist/commands/audit.js +59 -0
  15. package/dist/commands/config.d.ts +2 -0
  16. package/dist/commands/config.js +73 -0
  17. package/dist/commands/cron.js +4 -0
  18. package/dist/commands/generate.d.ts +7 -0
  19. package/dist/commands/generate.js +528 -234
  20. package/dist/commands/refresh.d.ts +2 -0
  21. package/dist/commands/refresh.js +158 -0
  22. package/dist/commands/review-pr.js +5 -0
  23. package/dist/commands/review.d.ts +2 -0
  24. package/dist/commands/review.js +110 -0
  25. package/dist/commands/test.js +177 -236
  26. package/dist/commands/watch.js +29 -20
  27. package/dist/config/loader.d.ts +6 -1
  28. package/dist/config/loader.js +38 -2
  29. package/dist/config/types.d.ts +7 -0
  30. package/dist/generator/generator.js +2 -1
  31. package/dist/generator/types.d.ts +3 -0
  32. package/dist/generator/writer.js +60 -28
  33. package/dist/github/org-discovery.d.ts +17 -0
  34. package/dist/github/org-discovery.js +93 -0
  35. package/dist/llm/index.d.ts +2 -0
  36. package/dist/llm/index.js +8 -2
  37. package/dist/next-actions/actions.d.ts +2 -0
  38. package/dist/next-actions/actions.js +190 -0
  39. package/dist/next-actions/index.d.ts +6 -0
  40. package/dist/next-actions/index.js +39 -0
  41. package/dist/next-actions/setup.d.ts +2 -0
  42. package/dist/next-actions/setup.js +72 -0
  43. package/dist/next-actions/state.d.ts +7 -0
  44. package/dist/next-actions/state.js +68 -0
  45. package/dist/next-actions/suggest.d.ts +3 -0
  46. package/dist/next-actions/suggest.js +47 -0
  47. package/dist/next-actions/types.d.ts +26 -0
  48. package/dist/next-actions/types.js +1 -0
  49. package/dist/refresh/differ.d.ts +9 -0
  50. package/dist/refresh/differ.js +67 -0
  51. package/dist/refresh/index.d.ts +4 -0
  52. package/dist/refresh/index.js +4 -0
  53. package/dist/refresh/manifest.d.ts +18 -0
  54. package/dist/refresh/manifest.js +71 -0
  55. package/dist/refresh/splicer.d.ts +9 -0
  56. package/dist/refresh/splicer.js +50 -0
  57. package/dist/refresh/types.d.ts +37 -0
  58. package/dist/refresh/types.js +1 -0
  59. package/dist/review/index.d.ts +8 -0
  60. package/dist/review/index.js +94 -0
  61. package/dist/review/parser.d.ts +16 -0
  62. package/dist/review/parser.js +95 -0
  63. package/dist/review/types.d.ts +18 -0
  64. package/dist/review/types.js +1 -0
  65. package/dist/scanner/types.d.ts +2 -0
  66. package/dist/structure/index.d.ts +19 -0
  67. package/dist/structure/index.js +92 -0
  68. package/dist/structure/planner.d.ts +8 -0
  69. package/dist/structure/planner.js +180 -0
  70. package/dist/structure/topology.d.ts +16 -0
  71. package/dist/structure/topology.js +49 -0
  72. package/dist/structure/types.d.ts +26 -0
  73. package/dist/structure/types.js +1 -0
  74. package/dist/testing/comparator.d.ts +7 -0
  75. package/dist/testing/comparator.js +77 -0
  76. package/dist/testing/docker.d.ts +21 -0
  77. package/dist/testing/docker.js +234 -0
  78. package/dist/testing/env.d.ts +16 -0
  79. package/dist/testing/env.js +58 -0
  80. package/dist/testing/extractor.d.ts +9 -0
  81. package/dist/testing/extractor.js +195 -0
  82. package/dist/testing/index.d.ts +6 -0
  83. package/dist/testing/index.js +6 -0
  84. package/dist/testing/runner.d.ts +5 -0
  85. package/dist/testing/runner.js +225 -0
  86. package/dist/testing/types.d.ts +58 -0
  87. package/dist/testing/types.js +1 -0
  88. 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 {};
@@ -27,6 +27,8 @@ export interface APIElement {
27
27
  sourceContext?: string;
28
28
  packageName?: string;
29
29
  relatedTypes?: string[];
30
+ sourceLabel?: string;
31
+ sourceRoot?: string;
30
32
  }
31
33
  /**
32
34
  * Result of scanning a file
@@ -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,7 @@
1
+ /**
2
+ * Compare actual output against expected output
3
+ */
4
+ export declare function compareOutput(actual: string, expected: string, mode?: 'exact' | 'contains'): {
5
+ match: boolean;
6
+ diff: string;
7
+ };
@@ -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>;