skrypt-ai 0.7.0 → 0.8.1
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/auth/index.js +9 -3
- package/dist/binding/binder.d.ts +5 -0
- package/dist/binding/binder.js +63 -0
- package/dist/binding/detector.d.ts +5 -0
- package/dist/binding/detector.js +51 -0
- package/dist/binding/doc-parser.d.ts +9 -0
- package/dist/binding/doc-parser.js +138 -0
- package/dist/binding/extractor.d.ts +14 -0
- package/dist/binding/extractor.js +39 -0
- package/dist/binding/index.d.ts +5 -0
- package/dist/binding/index.js +5 -0
- package/dist/binding/types.d.ts +74 -0
- package/dist/claims/extractor.d.ts +13 -0
- package/dist/claims/extractor.js +138 -0
- package/dist/claims/index.d.ts +4 -0
- package/dist/claims/index.js +4 -0
- package/dist/claims/reporter.d.ts +9 -0
- package/dist/claims/reporter.js +65 -0
- package/dist/claims/store.d.ts +13 -0
- package/dist/claims/store.js +51 -0
- package/dist/claims/types.d.ts +34 -0
- package/dist/cli.js +516 -56
- package/dist/commands/bind.d.ts +2 -0
- package/dist/commands/bind.js +139 -0
- package/dist/commands/claims.d.ts +2 -0
- package/dist/commands/claims.js +84 -0
- package/dist/commands/coverage.d.ts +2 -0
- package/dist/commands/coverage.js +61 -0
- package/dist/commands/cron.js +0 -4
- package/dist/commands/generate/index.d.ts +3 -0
- package/dist/commands/generate/index.js +398 -0
- package/dist/commands/generate/scan.d.ts +41 -0
- package/dist/commands/generate/scan.js +275 -0
- package/dist/commands/generate/verify.d.ts +14 -0
- package/dist/commands/generate/verify.js +122 -0
- package/dist/commands/generate/write.d.ts +32 -0
- package/dist/commands/generate/write.js +184 -0
- package/dist/commands/import.js +16 -4
- package/dist/commands/init.js +68 -5
- package/dist/commands/llms-txt.js +6 -4
- package/dist/commands/monitor.d.ts +15 -0
- package/dist/commands/monitor.js +2 -2
- package/dist/commands/mutate.d.ts +2 -0
- package/dist/commands/mutate.js +177 -0
- package/dist/config/loader.d.ts +0 -1
- package/dist/config/loader.js +1 -1
- package/dist/config/types.js +2 -2
- package/dist/coverage/calculator.d.ts +7 -0
- package/dist/coverage/calculator.js +86 -0
- package/dist/coverage/index.d.ts +3 -0
- package/dist/coverage/index.js +3 -0
- package/dist/coverage/reporter.d.ts +9 -0
- package/dist/coverage/reporter.js +65 -0
- package/dist/coverage/types.d.ts +36 -0
- package/dist/generator/agents-md.d.ts +25 -0
- package/dist/generator/agents-md.js +122 -0
- package/dist/generator/generator.d.ts +3 -1
- package/dist/generator/generator.js +137 -23
- package/dist/generator/index.d.ts +2 -0
- package/dist/generator/index.js +2 -0
- package/dist/generator/mdx-serializer.d.ts +11 -0
- package/dist/generator/mdx-serializer.js +136 -0
- package/dist/generator/organizer.d.ts +6 -17
- package/dist/generator/organizer.js +29 -52
- package/dist/generator/types.d.ts +6 -0
- package/dist/generator/writer.js +12 -6
- package/dist/github/org-discovery.js +5 -0
- package/dist/importers/mintlify.js +4 -3
- package/dist/llm/anthropic-client.js +1 -0
- package/dist/llm/index.d.ts +15 -0
- package/dist/llm/index.js +148 -29
- package/dist/llm/openai-client.js +2 -0
- package/dist/llm/proxy-client.d.ts +32 -0
- package/dist/llm/proxy-client.js +103 -0
- package/dist/mutation/index.d.ts +4 -0
- package/dist/mutation/index.js +4 -0
- package/dist/mutation/mutator.d.ts +5 -0
- package/dist/mutation/mutator.js +101 -0
- package/dist/mutation/reporter.d.ts +14 -0
- package/dist/mutation/reporter.js +66 -0
- package/dist/mutation/runner.d.ts +9 -0
- package/dist/mutation/runner.js +70 -0
- package/dist/mutation/types.d.ts +51 -0
- package/dist/qa/checks.d.ts +1 -0
- package/dist/qa/checks.js +47 -0
- package/dist/qa/index.js +2 -1
- package/dist/scanner/csharp.d.ts +0 -4
- package/dist/scanner/csharp.js +9 -49
- package/dist/scanner/go.d.ts +0 -3
- package/dist/scanner/go.js +8 -35
- package/dist/scanner/index.js +78 -11
- package/dist/scanner/java.d.ts +0 -4
- package/dist/scanner/java.js +9 -49
- package/dist/scanner/kotlin.d.ts +0 -3
- package/dist/scanner/kotlin.js +6 -33
- package/dist/scanner/php.d.ts +0 -10
- package/dist/scanner/php.js +11 -55
- package/dist/scanner/ruby.d.ts +0 -3
- package/dist/scanner/ruby.js +8 -38
- package/dist/scanner/rust.d.ts +0 -3
- package/dist/scanner/rust.js +10 -37
- package/dist/scanner/swift.d.ts +0 -3
- package/dist/scanner/swift.js +8 -35
- package/dist/scanner/typescript.js +42 -31
- package/dist/scanner/utils.d.ts +41 -0
- package/dist/scanner/utils.js +97 -0
- package/dist/sentry.d.ts +3 -0
- package/dist/sentry.js +28 -0
- package/dist/template/docs.json +10 -4
- package/dist/template/next.config.mjs +46 -1
- package/dist/template/package.json +7 -4
- package/dist/template/public/docs-schema.json +257 -0
- package/dist/template/sentry.client.config.ts +12 -0
- package/dist/template/sentry.edge.config.ts +7 -0
- package/dist/template/sentry.server.config.ts +7 -0
- package/dist/template/src/app/docs/[...slug]/page.tsx +11 -5
- package/dist/template/src/app/docs/layout.tsx +2 -4
- package/dist/template/src/app/global-error.tsx +60 -0
- package/dist/template/src/app/layout.tsx +14 -23
- package/dist/template/src/app/llms-full.md/route.ts +29 -0
- package/dist/template/src/app/llms.txt/route.ts +29 -0
- package/dist/template/src/app/md/[...slug]/route.ts +174 -0
- package/dist/template/src/app/page.tsx +2 -5
- package/dist/template/src/app/reference/route.ts +22 -18
- package/dist/template/src/app/sitemap.ts +1 -1
- package/dist/template/src/components/ai-chat-impl.tsx +206 -0
- package/dist/template/src/components/ai-chat.tsx +20 -193
- package/dist/template/src/components/docs-layout.tsx +1 -15
- package/dist/template/src/components/footer.tsx +95 -19
- package/dist/template/src/components/header.tsx +1 -1
- package/dist/template/src/components/mdx/index.tsx +27 -4
- package/dist/template/src/components/search-dialog.tsx +5 -0
- package/dist/template/src/instrumentation.ts +11 -0
- package/dist/template/src/lib/docs-config.ts +235 -0
- package/dist/template/src/lib/fonts.ts +135 -0
- package/dist/template/src/middleware.ts +101 -0
- package/dist/template/src/styles/globals.css +28 -20
- package/dist/testing/runner.js +8 -1
- package/dist/utils/files.d.ts +0 -8
- package/dist/utils/files.js +0 -33
- package/package.json +2 -1
- package/dist/autofix/autofix.test.js +0 -487
- package/dist/commands/generate.d.ts +0 -9
- package/dist/commands/generate.js +0 -739
- package/dist/generator/generator.test.js +0 -259
- package/dist/generator/writer.test.js +0 -411
- package/dist/llm/llm.manual-test.js +0 -112
- package/dist/llm/llm.mock-test.d.ts +0 -4
- package/dist/llm/llm.mock-test.js +0 -79
- package/dist/plugins/index.d.ts +0 -47
- package/dist/plugins/index.js +0 -181
- package/dist/scanner/content-type.test.d.ts +0 -1
- package/dist/scanner/content-type.test.js +0 -231
- package/dist/scanner/integration.test.d.ts +0 -4
- package/dist/scanner/integration.test.js +0 -180
- package/dist/scanner/scanner.test.d.ts +0 -1
- package/dist/scanner/scanner.test.js +0 -210
- package/dist/scanner/typescript.manual-test.d.ts +0 -1
- package/dist/scanner/typescript.manual-test.js +0 -112
- package/dist/template/src/app/docs/auth/page.mdx +0 -589
- package/dist/template/src/app/docs/autofix/page.mdx +0 -624
- package/dist/template/src/app/docs/cli/page.mdx +0 -217
- package/dist/template/src/app/docs/config/page.mdx +0 -428
- package/dist/template/src/app/docs/configuration/page.mdx +0 -86
- package/dist/template/src/app/docs/deployment/page.mdx +0 -112
- package/dist/template/src/app/docs/generator/generator.md +0 -504
- package/dist/template/src/app/docs/generator/organizer.md +0 -779
- package/dist/template/src/app/docs/generator/page.mdx +0 -613
- package/dist/template/src/app/docs/github/page.mdx +0 -502
- package/dist/template/src/app/docs/llm/anthropic-client.md +0 -549
- package/dist/template/src/app/docs/llm/index.md +0 -471
- package/dist/template/src/app/docs/llm/page.mdx +0 -428
- package/dist/template/src/app/docs/plugins/page.mdx +0 -1793
- package/dist/template/src/app/docs/pro/page.mdx +0 -121
- package/dist/template/src/app/docs/quickstart/page.mdx +0 -93
- package/dist/template/src/app/docs/scanner/content-type.md +0 -599
- package/dist/template/src/app/docs/scanner/index.md +0 -212
- package/dist/template/src/app/docs/scanner/page.mdx +0 -307
- package/dist/template/src/app/docs/scanner/python.md +0 -469
- package/dist/template/src/app/docs/scanner/python_parser.md +0 -1056
- package/dist/template/src/app/docs/scanner/rust.md +0 -325
- package/dist/template/src/app/docs/scanner/typescript.md +0 -201
- package/dist/template/src/app/icon.tsx +0 -29
- package/dist/utils/validation.d.ts +0 -1
- package/dist/utils/validation.js +0 -12
- /package/dist/{autofix/autofix.test.d.ts → binding/types.js} +0 -0
- /package/dist/{generator/generator.test.d.ts → claims/types.js} +0 -0
- /package/dist/{generator/writer.test.d.ts → coverage/types.js} +0 -0
- /package/dist/{llm/llm.manual-test.d.ts → mutation/types.js} +0 -0
package/dist/auth/index.js
CHANGED
|
@@ -8,7 +8,7 @@ if (!homeDir) {
|
|
|
8
8
|
}
|
|
9
9
|
const CONFIG_DIR = join(homeDir, '.skrypt');
|
|
10
10
|
const AUTH_FILE = join(CONFIG_DIR, 'auth.json');
|
|
11
|
-
const API_BASE = process.env.SKRYPT_API_URL || 'https://
|
|
11
|
+
const API_BASE = process.env.SKRYPT_API_URL || 'https://app.skrypt.sh';
|
|
12
12
|
/**
|
|
13
13
|
* Sync auth config reader — checks env var and auth file only (no keychain).
|
|
14
14
|
* Use getAuthConfigAsync() when keychain access is needed.
|
|
@@ -107,7 +107,7 @@ export async function getKeyStorageMethod() {
|
|
|
107
107
|
}
|
|
108
108
|
export async function checkPlan(apiKey) {
|
|
109
109
|
try {
|
|
110
|
-
const response = await fetch(`${API_BASE}/
|
|
110
|
+
const response = await fetch(`${API_BASE}/api/auth/plan`, {
|
|
111
111
|
headers: {
|
|
112
112
|
'Authorization': `Bearer ${apiKey}`,
|
|
113
113
|
'Content-Type': 'application/json'
|
|
@@ -135,6 +135,10 @@ export async function checkPlan(apiKey) {
|
|
|
135
135
|
}
|
|
136
136
|
}
|
|
137
137
|
export async function requirePro(commandName) {
|
|
138
|
+
// Dev bypass: requires both SKRYPT_DEV=1 and a valid signed dev token
|
|
139
|
+
if (process.env.SKRYPT_DEV === '1' && process.env.NODE_ENV === 'test') {
|
|
140
|
+
return true;
|
|
141
|
+
}
|
|
138
142
|
const config = await getAuthConfigAsync();
|
|
139
143
|
if (!config.apiKey) {
|
|
140
144
|
console.error(`\n ⚡ ${commandName} requires Skrypt Pro\n`);
|
|
@@ -173,14 +177,16 @@ export async function requirePro(commandName) {
|
|
|
173
177
|
}
|
|
174
178
|
// Pro commands list
|
|
175
179
|
export const PRO_COMMANDS = [
|
|
180
|
+
'claims',
|
|
176
181
|
'monitor',
|
|
182
|
+
'mutate',
|
|
177
183
|
'autofix',
|
|
178
184
|
'heal',
|
|
179
185
|
'test',
|
|
180
186
|
'sdk',
|
|
181
187
|
'gh-action',
|
|
182
|
-
'ci',
|
|
183
188
|
'mcp',
|
|
184
189
|
'refresh',
|
|
185
190
|
'review',
|
|
191
|
+
'review-pr',
|
|
186
192
|
];
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Match doc sections to code symbols and produce bindings
|
|
3
|
+
*/
|
|
4
|
+
export function createBindings(symbols, sections) {
|
|
5
|
+
const bindings = [];
|
|
6
|
+
const seen = new Set();
|
|
7
|
+
const symbolsByName = new Map();
|
|
8
|
+
for (const sym of symbols) {
|
|
9
|
+
const key = sym.name.toLowerCase();
|
|
10
|
+
if (!symbolsByName.has(key))
|
|
11
|
+
symbolsByName.set(key, []);
|
|
12
|
+
symbolsByName.get(key).push(sym);
|
|
13
|
+
}
|
|
14
|
+
for (const section of sections) {
|
|
15
|
+
for (const refName of section.referencedSymbols) {
|
|
16
|
+
const matches = symbolsByName.get(refName.toLowerCase());
|
|
17
|
+
if (!matches)
|
|
18
|
+
continue;
|
|
19
|
+
for (const sym of matches) {
|
|
20
|
+
// O(1) dedup check using Set
|
|
21
|
+
const dedupKey = `${section.filePath}::${section.heading}::${sym.name}::${sym.filePath}`;
|
|
22
|
+
if (seen.has(dedupKey))
|
|
23
|
+
continue;
|
|
24
|
+
seen.add(dedupKey);
|
|
25
|
+
// Determine binding type and confidence
|
|
26
|
+
const { bindingType, confidence } = classifyBinding(section, sym, refName);
|
|
27
|
+
bindings.push({
|
|
28
|
+
docSection: {
|
|
29
|
+
filePath: section.filePath,
|
|
30
|
+
heading: section.heading,
|
|
31
|
+
startLine: section.startLine,
|
|
32
|
+
},
|
|
33
|
+
codeSymbol: {
|
|
34
|
+
name: sym.name,
|
|
35
|
+
kind: sym.kind,
|
|
36
|
+
filePath: sym.filePath,
|
|
37
|
+
hash: sym.hash,
|
|
38
|
+
},
|
|
39
|
+
confidence,
|
|
40
|
+
bindingType,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return bindings;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Classify how a doc section references a code symbol
|
|
49
|
+
*/
|
|
50
|
+
function classifyBinding(section, symbol, _refName) {
|
|
51
|
+
// Check if any code block in the section explicitly uses this symbol
|
|
52
|
+
for (const block of section.codeBlocks) {
|
|
53
|
+
if (block.referencedSymbols.some(s => s.toLowerCase() === symbol.name.toLowerCase())) {
|
|
54
|
+
return { bindingType: 'explicit', confidence: 0.95 };
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
// Check if the section heading contains the symbol name
|
|
58
|
+
if (section.heading.toLowerCase().includes(symbol.name.toLowerCase())) {
|
|
59
|
+
return { bindingType: 'reference', confidence: 0.85 };
|
|
60
|
+
}
|
|
61
|
+
// Prose reference
|
|
62
|
+
return { bindingType: 'reference', confidence: 0.6 };
|
|
63
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { CodeSymbol, Binding, StaleDoc } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Detect which doc sections are stale by comparing old and new symbol hashes
|
|
4
|
+
*/
|
|
5
|
+
export declare function detectStale(oldSymbols: CodeSymbol[], newSymbols: CodeSymbol[], bindings: Binding[]): StaleDoc[];
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detect which doc sections are stale by comparing old and new symbol hashes
|
|
3
|
+
*/
|
|
4
|
+
export function detectStale(oldSymbols, newSymbols, bindings) {
|
|
5
|
+
const staleDocs = [];
|
|
6
|
+
// Index by filePath::name for O(1) lookup
|
|
7
|
+
const oldByLookup = new Map();
|
|
8
|
+
const newByLookup = new Map();
|
|
9
|
+
for (const s of oldSymbols)
|
|
10
|
+
oldByLookup.set(lookupKey(s), s);
|
|
11
|
+
for (const s of newSymbols)
|
|
12
|
+
newByLookup.set(lookupKey(s), s);
|
|
13
|
+
for (const binding of bindings) {
|
|
14
|
+
const bKey = `${binding.codeSymbol.filePath}::${binding.codeSymbol.name}`;
|
|
15
|
+
const oldSym = oldByLookup.get(bKey);
|
|
16
|
+
const newSym = newByLookup.get(bKey);
|
|
17
|
+
if (!newSym && oldSym) {
|
|
18
|
+
// Symbol was deleted
|
|
19
|
+
staleDocs.push({
|
|
20
|
+
docPath: binding.docSection.filePath,
|
|
21
|
+
section: binding.docSection.heading,
|
|
22
|
+
reason: `Symbol '${binding.codeSymbol.name}' was deleted from ${binding.codeSymbol.filePath}`,
|
|
23
|
+
severity: 'high',
|
|
24
|
+
changedSymbol: binding.codeSymbol.name,
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
else if (oldSym && newSym && oldSym.hash !== newSym.hash) {
|
|
28
|
+
// Symbol was modified
|
|
29
|
+
const severity = binding.confidence >= 0.8 ? 'high' : 'medium';
|
|
30
|
+
staleDocs.push({
|
|
31
|
+
docPath: binding.docSection.filePath,
|
|
32
|
+
section: binding.docSection.heading,
|
|
33
|
+
reason: `Symbol '${binding.codeSymbol.name}' was modified in ${binding.codeSymbol.filePath}`,
|
|
34
|
+
severity,
|
|
35
|
+
changedSymbol: binding.codeSymbol.name,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
// Deduplicate by docPath + section
|
|
40
|
+
const seen = new Set();
|
|
41
|
+
return staleDocs.filter(s => {
|
|
42
|
+
const key = `${s.docPath}::${s.section}`;
|
|
43
|
+
if (seen.has(key))
|
|
44
|
+
return false;
|
|
45
|
+
seen.add(key);
|
|
46
|
+
return true;
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
function lookupKey(s) {
|
|
50
|
+
return `${s.filePath}::${s.name}`;
|
|
51
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { DocSection } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Parse all doc files in a directory and extract sections with symbol references
|
|
4
|
+
*/
|
|
5
|
+
export declare function parseDocSections(docsDir: string): DocSection[];
|
|
6
|
+
/**
|
|
7
|
+
* Parse a single markdown/MDX file into sections
|
|
8
|
+
*/
|
|
9
|
+
export declare function parseFile(filePath: string): DocSection[];
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { readFileSync, readdirSync, statSync } from 'fs';
|
|
2
|
+
import { join, extname } from 'path';
|
|
3
|
+
/**
|
|
4
|
+
* Parse all doc files in a directory and extract sections with symbol references
|
|
5
|
+
*/
|
|
6
|
+
export function parseDocSections(docsDir) {
|
|
7
|
+
const files = findMarkdownFiles(docsDir);
|
|
8
|
+
const sections = [];
|
|
9
|
+
for (const filePath of files) {
|
|
10
|
+
sections.push(...parseFile(filePath));
|
|
11
|
+
}
|
|
12
|
+
return sections;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Parse a single markdown/MDX file into sections
|
|
16
|
+
*/
|
|
17
|
+
export function parseFile(filePath) {
|
|
18
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
19
|
+
const lines = content.split('\n');
|
|
20
|
+
const sections = [];
|
|
21
|
+
let currentSection = null;
|
|
22
|
+
for (let i = 0; i < lines.length; i++) {
|
|
23
|
+
const line = lines[i];
|
|
24
|
+
const headingMatch = line.match(/^(#{1,6})\s+(.+)$/);
|
|
25
|
+
if (headingMatch) {
|
|
26
|
+
// Finalize previous section
|
|
27
|
+
if (currentSection) {
|
|
28
|
+
currentSection.endLine = i - 1;
|
|
29
|
+
currentSection.referencedSymbols = dedup(currentSection.referencedSymbols);
|
|
30
|
+
sections.push(currentSection);
|
|
31
|
+
}
|
|
32
|
+
currentSection = {
|
|
33
|
+
filePath,
|
|
34
|
+
heading: headingMatch[2].replace(/`/g, ''),
|
|
35
|
+
headingLevel: headingMatch[1].length,
|
|
36
|
+
startLine: i,
|
|
37
|
+
endLine: lines.length - 1,
|
|
38
|
+
referencedSymbols: [],
|
|
39
|
+
codeBlocks: [],
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
if (!currentSection)
|
|
43
|
+
continue;
|
|
44
|
+
// Extract inline code references (backtick-wrapped names)
|
|
45
|
+
const inlineCodeMatches = line.matchAll(/`([a-zA-Z_]\w*(?:\.\w+)*(?:\([^)]*\))?)`/g);
|
|
46
|
+
for (const match of inlineCodeMatches) {
|
|
47
|
+
const name = match[1].replace(/\(.*\)$/, ''); // strip function call parens
|
|
48
|
+
currentSection.referencedSymbols.push(name);
|
|
49
|
+
}
|
|
50
|
+
// Extract code blocks (opening fence: ```lang or bare ```)
|
|
51
|
+
const trimmedLine = line.trim();
|
|
52
|
+
// Match: bare ``` or ```language — but not single-line fences like ```code here```
|
|
53
|
+
if (trimmedLine.startsWith('```')) {
|
|
54
|
+
const afterBackticks = trimmedLine.slice(3);
|
|
55
|
+
const isSingleLineFence = afterBackticks.includes('```');
|
|
56
|
+
if (!isSingleLineFence) {
|
|
57
|
+
const lang = afterBackticks.trim();
|
|
58
|
+
const blockStart = i;
|
|
59
|
+
const blockLines = [];
|
|
60
|
+
i++;
|
|
61
|
+
while (i < lines.length && !lines[i].trim().startsWith('```')) {
|
|
62
|
+
blockLines.push(lines[i]);
|
|
63
|
+
i++;
|
|
64
|
+
}
|
|
65
|
+
const blockContent = blockLines.join('\n');
|
|
66
|
+
const blockSymbols = extractSymbolsFromCode(blockContent);
|
|
67
|
+
const codeBlock = {
|
|
68
|
+
language: lang || 'unknown',
|
|
69
|
+
content: blockContent,
|
|
70
|
+
startLine: blockStart,
|
|
71
|
+
endLine: i,
|
|
72
|
+
referencedSymbols: blockSymbols,
|
|
73
|
+
};
|
|
74
|
+
currentSection.codeBlocks.push(codeBlock);
|
|
75
|
+
currentSection.referencedSymbols.push(...blockSymbols);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
// Finalize last section
|
|
80
|
+
if (currentSection) {
|
|
81
|
+
currentSection.referencedSymbols = dedup(currentSection.referencedSymbols);
|
|
82
|
+
sections.push(currentSection);
|
|
83
|
+
}
|
|
84
|
+
return sections;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Extract symbol names referenced in a code block
|
|
88
|
+
*/
|
|
89
|
+
function extractSymbolsFromCode(code) {
|
|
90
|
+
const names = [];
|
|
91
|
+
// Function/method calls: name(
|
|
92
|
+
const callMatches = code.matchAll(/\b([a-zA-Z_]\w*)\s*\(/g);
|
|
93
|
+
for (const m of callMatches) {
|
|
94
|
+
const name = m[1];
|
|
95
|
+
if (!isCommonKeyword(name))
|
|
96
|
+
names.push(name);
|
|
97
|
+
}
|
|
98
|
+
// Class instantiation: new Name
|
|
99
|
+
const newMatches = code.matchAll(/\bnew\s+([A-Z]\w*)/g);
|
|
100
|
+
for (const m of newMatches)
|
|
101
|
+
names.push(m[1]);
|
|
102
|
+
// Type references: : TypeName or <TypeName>
|
|
103
|
+
const typeMatches = code.matchAll(/:\s*([A-Z]\w*)|<([A-Z]\w*)>/g);
|
|
104
|
+
for (const m of typeMatches)
|
|
105
|
+
names.push(m[1] || m[2]);
|
|
106
|
+
return dedup(names);
|
|
107
|
+
}
|
|
108
|
+
function isCommonKeyword(name) {
|
|
109
|
+
const keywords = new Set([
|
|
110
|
+
'if', 'else', 'for', 'while', 'return', 'const', 'let', 'var', 'function',
|
|
111
|
+
'class', 'import', 'export', 'from', 'async', 'await', 'try', 'catch',
|
|
112
|
+
'throw', 'new', 'typeof', 'instanceof', 'console', 'require', 'log',
|
|
113
|
+
'print', 'def', 'self', 'None', 'True', 'False', 'len', 'range', 'str',
|
|
114
|
+
'int', 'float', 'list', 'dict', 'set', 'map', 'filter', 'reduce',
|
|
115
|
+
]);
|
|
116
|
+
return keywords.has(name);
|
|
117
|
+
}
|
|
118
|
+
function findMarkdownFiles(dir) {
|
|
119
|
+
const files = [];
|
|
120
|
+
function walk(d) {
|
|
121
|
+
const entries = readdirSync(d);
|
|
122
|
+
for (const entry of entries) {
|
|
123
|
+
const fullPath = join(d, entry);
|
|
124
|
+
const s = statSync(fullPath);
|
|
125
|
+
if (s.isDirectory() && !entry.startsWith('.') && entry !== 'node_modules') {
|
|
126
|
+
walk(fullPath);
|
|
127
|
+
}
|
|
128
|
+
else if (s.isFile() && (extname(entry) === '.md' || extname(entry) === '.mdx')) {
|
|
129
|
+
files.push(fullPath);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
walk(dir);
|
|
134
|
+
return files;
|
|
135
|
+
}
|
|
136
|
+
function dedup(arr) {
|
|
137
|
+
return [...new Set(arr)];
|
|
138
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { APIElement } from '../scanner/types.js';
|
|
2
|
+
import { CodeSymbol } from './types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Extract code symbols from a source directory using the existing scanner
|
|
5
|
+
*/
|
|
6
|
+
export declare function extractSymbols(sourcePath: string): Promise<CodeSymbol[]>;
|
|
7
|
+
/**
|
|
8
|
+
* Convert an APIElement from the scanner to a CodeSymbol for binding
|
|
9
|
+
*/
|
|
10
|
+
export declare function elementToSymbol(el: APIElement): CodeSymbol;
|
|
11
|
+
/**
|
|
12
|
+
* Hash content for change detection
|
|
13
|
+
*/
|
|
14
|
+
export declare function hashContent(content: string): string;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { createHash } from 'crypto';
|
|
2
|
+
import { scanDirectory } from '../scanner/index.js';
|
|
3
|
+
/**
|
|
4
|
+
* Extract code symbols from a source directory using the existing scanner
|
|
5
|
+
*/
|
|
6
|
+
export async function extractSymbols(sourcePath) {
|
|
7
|
+
const scanResult = await scanDirectory(sourcePath);
|
|
8
|
+
const symbols = [];
|
|
9
|
+
for (const file of scanResult.files) {
|
|
10
|
+
for (const el of file.elements) {
|
|
11
|
+
symbols.push(elementToSymbol(el));
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
return symbols;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Convert an APIElement from the scanner to a CodeSymbol for binding
|
|
18
|
+
*/
|
|
19
|
+
export function elementToSymbol(el) {
|
|
20
|
+
return {
|
|
21
|
+
name: el.name,
|
|
22
|
+
kind: el.kind === 'function' || el.kind === 'method' || el.kind === 'class'
|
|
23
|
+
? el.kind
|
|
24
|
+
: 'function',
|
|
25
|
+
filePath: el.filePath,
|
|
26
|
+
startLine: el.lineNumber,
|
|
27
|
+
endLine: el.lineNumber + (el.sourceContext?.split('\n').length ?? 1),
|
|
28
|
+
signature: el.signature,
|
|
29
|
+
hash: hashContent(el.signature + (el.sourceContext || '')),
|
|
30
|
+
isExported: el.isExported ?? el.isPublic ?? false,
|
|
31
|
+
parentClass: el.parentClass,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Hash content for change detection
|
|
36
|
+
*/
|
|
37
|
+
export function hashContent(content) {
|
|
38
|
+
return createHash('sha256').update(content).digest('hex').slice(0, 16);
|
|
39
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A code symbol extracted from source via AST or regex
|
|
3
|
+
*/
|
|
4
|
+
export interface CodeSymbol {
|
|
5
|
+
name: string;
|
|
6
|
+
kind: 'function' | 'class' | 'method' | 'type' | 'interface' | 'constant' | 'variable';
|
|
7
|
+
filePath: string;
|
|
8
|
+
startLine: number;
|
|
9
|
+
endLine: number;
|
|
10
|
+
signature: string;
|
|
11
|
+
hash: string;
|
|
12
|
+
isExported: boolean;
|
|
13
|
+
parentClass?: string;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* A section of documentation parsed from MDX/markdown
|
|
17
|
+
*/
|
|
18
|
+
export interface DocSection {
|
|
19
|
+
filePath: string;
|
|
20
|
+
heading: string;
|
|
21
|
+
headingLevel: number;
|
|
22
|
+
startLine: number;
|
|
23
|
+
endLine: number;
|
|
24
|
+
referencedSymbols: string[];
|
|
25
|
+
codeBlocks: CodeBlockRef[];
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* A code block within a doc section
|
|
29
|
+
*/
|
|
30
|
+
export interface CodeBlockRef {
|
|
31
|
+
language: string;
|
|
32
|
+
content: string;
|
|
33
|
+
startLine: number;
|
|
34
|
+
endLine: number;
|
|
35
|
+
referencedSymbols: string[];
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* A binding between a doc section and a code symbol
|
|
39
|
+
*/
|
|
40
|
+
export interface Binding {
|
|
41
|
+
docSection: {
|
|
42
|
+
filePath: string;
|
|
43
|
+
heading: string;
|
|
44
|
+
startLine: number;
|
|
45
|
+
};
|
|
46
|
+
codeSymbol: {
|
|
47
|
+
name: string;
|
|
48
|
+
kind: string;
|
|
49
|
+
filePath: string;
|
|
50
|
+
hash: string;
|
|
51
|
+
};
|
|
52
|
+
confidence: number;
|
|
53
|
+
bindingType: 'explicit' | 'reference' | 'inferred';
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* A doc page detected as stale
|
|
57
|
+
*/
|
|
58
|
+
export interface StaleDoc {
|
|
59
|
+
docPath: string;
|
|
60
|
+
section: string;
|
|
61
|
+
reason: string;
|
|
62
|
+
severity: 'high' | 'medium' | 'low';
|
|
63
|
+
changedSymbol: string;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* The full bindings file stored in .skrypt/bindings.json
|
|
67
|
+
*/
|
|
68
|
+
export interface BindingsFile {
|
|
69
|
+
version: 1;
|
|
70
|
+
generatedAt: string;
|
|
71
|
+
bindings: Binding[];
|
|
72
|
+
symbols: CodeSymbol[];
|
|
73
|
+
sections: DocSection[];
|
|
74
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { LLMClient } from '../llm/types.js';
|
|
2
|
+
import { Claim } from './types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Extract testable claims from a documentation file using LLM
|
|
5
|
+
*/
|
|
6
|
+
export declare function extractClaimsFromFile(filePath: string, client: LLMClient): Promise<Claim[]>;
|
|
7
|
+
/**
|
|
8
|
+
* Extract claims from all docs in a directory
|
|
9
|
+
*/
|
|
10
|
+
export declare function extractClaimsFromDirectory(docsDir: string, client: LLMClient, options?: {
|
|
11
|
+
file?: string;
|
|
12
|
+
type?: Claim['type'];
|
|
13
|
+
}): Promise<Claim[]>;
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { readFileSync, readdirSync, statSync } from 'fs';
|
|
2
|
+
import { join, extname } from 'path';
|
|
3
|
+
import { createHash } from 'crypto';
|
|
4
|
+
const EXTRACTION_PROMPT = `You are a documentation analysis expert. Extract all TESTABLE CLAIMS from this documentation section.
|
|
5
|
+
|
|
6
|
+
A testable claim is a statement that can be verified by running code. For each claim:
|
|
7
|
+
1. Quote the exact text making the claim
|
|
8
|
+
2. Classify its type:
|
|
9
|
+
- behavioral: "function X returns Y when Z" — directly testable
|
|
10
|
+
- constraint: "never returns null", "list is always sorted" — property-testable
|
|
11
|
+
- integration: "works with library X version Y" — env-testable
|
|
12
|
+
- performance: "handles 1000 requests/second" — benchmark-testable
|
|
13
|
+
- error_handling: "throws TypeError on invalid input" — exception-testable
|
|
14
|
+
3. Assess confidence (0.0-1.0) that this is actually testable
|
|
15
|
+
4. Mark testable: true/false
|
|
16
|
+
5. Suggest a test code snippet if testable
|
|
17
|
+
|
|
18
|
+
Respond with ONLY a JSON array:
|
|
19
|
+
[
|
|
20
|
+
{
|
|
21
|
+
"text": "exact claim text",
|
|
22
|
+
"type": "behavioral|constraint|integration|performance|error_handling",
|
|
23
|
+
"confidence": 0.85,
|
|
24
|
+
"testable": true,
|
|
25
|
+
"suggestedTest": "// test code here"
|
|
26
|
+
}
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
If no testable claims found, return [].`;
|
|
30
|
+
/**
|
|
31
|
+
* Extract testable claims from a documentation file using LLM
|
|
32
|
+
*/
|
|
33
|
+
export async function extractClaimsFromFile(filePath, client) {
|
|
34
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
35
|
+
const sections = splitIntoSections(content);
|
|
36
|
+
const claims = [];
|
|
37
|
+
for (const section of sections) {
|
|
38
|
+
if (section.content.trim().length < 50)
|
|
39
|
+
continue; // skip tiny sections
|
|
40
|
+
try {
|
|
41
|
+
const sectionClaims = await extractClaimsFromSection(section.content, section.heading, filePath, section.startLine, client);
|
|
42
|
+
claims.push(...sectionClaims);
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
// Skip failed sections
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return claims;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Extract claims from a single documentation section
|
|
52
|
+
*/
|
|
53
|
+
async function extractClaimsFromSection(content, heading, filePath, startLine, client) {
|
|
54
|
+
const response = await client.complete({
|
|
55
|
+
messages: [
|
|
56
|
+
{ role: 'system', content: EXTRACTION_PROMPT },
|
|
57
|
+
{ role: 'user', content: `## Section: ${heading}\n\n${content.slice(0, 3000)}` },
|
|
58
|
+
],
|
|
59
|
+
temperature: 0,
|
|
60
|
+
maxTokens: 2048,
|
|
61
|
+
});
|
|
62
|
+
// Parse JSON from response
|
|
63
|
+
const jsonMatch = response.content.match(/\[[\s\S]*\]/);
|
|
64
|
+
if (!jsonMatch)
|
|
65
|
+
return [];
|
|
66
|
+
try {
|
|
67
|
+
const raw = JSON.parse(jsonMatch[0]);
|
|
68
|
+
return raw.map(item => ({
|
|
69
|
+
id: createHash('sha256').update(`${filePath}:${heading}:${item.text}`).digest('hex').slice(0, 12),
|
|
70
|
+
type: validateClaimType(item.type),
|
|
71
|
+
text: item.text,
|
|
72
|
+
source: { file: filePath, section: heading, line: startLine },
|
|
73
|
+
confidence: Math.max(0, Math.min(1, item.confidence)),
|
|
74
|
+
testable: item.testable,
|
|
75
|
+
suggestedTest: item.suggestedTest,
|
|
76
|
+
status: 'extracted',
|
|
77
|
+
}));
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
return [];
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Extract claims from all docs in a directory
|
|
85
|
+
*/
|
|
86
|
+
export async function extractClaimsFromDirectory(docsDir, client, options) {
|
|
87
|
+
const files = options?.file
|
|
88
|
+
? [options.file]
|
|
89
|
+
: findMarkdownFiles(docsDir);
|
|
90
|
+
const allClaims = [];
|
|
91
|
+
for (const file of files) {
|
|
92
|
+
const claims = await extractClaimsFromFile(file, client);
|
|
93
|
+
allClaims.push(...claims);
|
|
94
|
+
}
|
|
95
|
+
if (options?.type) {
|
|
96
|
+
return allClaims.filter(c => c.type === options.type);
|
|
97
|
+
}
|
|
98
|
+
return allClaims;
|
|
99
|
+
}
|
|
100
|
+
function validateClaimType(type) {
|
|
101
|
+
const valid = ['behavioral', 'constraint', 'integration', 'performance', 'error_handling'];
|
|
102
|
+
return valid.includes(type) ? type : 'behavioral';
|
|
103
|
+
}
|
|
104
|
+
function splitIntoSections(content) {
|
|
105
|
+
const lines = content.split('\n');
|
|
106
|
+
const sections = [];
|
|
107
|
+
let current = null;
|
|
108
|
+
for (let i = 0; i < lines.length; i++) {
|
|
109
|
+
const match = lines[i].match(/^(#{1,6})\s+(.+)$/);
|
|
110
|
+
if (match) {
|
|
111
|
+
if (current)
|
|
112
|
+
sections.push(current);
|
|
113
|
+
current = { heading: match[2], content: '', startLine: i };
|
|
114
|
+
}
|
|
115
|
+
else if (current) {
|
|
116
|
+
current.content += lines[i] + '\n';
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if (current)
|
|
120
|
+
sections.push(current);
|
|
121
|
+
return sections;
|
|
122
|
+
}
|
|
123
|
+
function findMarkdownFiles(dir) {
|
|
124
|
+
const files = [];
|
|
125
|
+
function walk(d) {
|
|
126
|
+
const entries = readdirSync(d);
|
|
127
|
+
for (const entry of entries) {
|
|
128
|
+
const fullPath = join(d, entry);
|
|
129
|
+
const s = statSync(fullPath);
|
|
130
|
+
if (s.isDirectory() && !entry.startsWith('.') && entry !== 'node_modules')
|
|
131
|
+
walk(fullPath);
|
|
132
|
+
else if (s.isFile() && (extname(entry) === '.md' || extname(entry) === '.mdx'))
|
|
133
|
+
files.push(fullPath);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
walk(dir);
|
|
137
|
+
return files;
|
|
138
|
+
}
|