codewalker

This commit is contained in:
2025-11-24 05:48:13 +01:00
parent 4402cba8ac
commit d282a5dc95
29 changed files with 1412 additions and 453 deletions

View File

@@ -93,4 +93,4 @@ pub fn (e Enum) vgen() string {
}
return '${comments}${prefix}enum ${e.name} {${values_str}\n}'
}
}

View File

@@ -0,0 +1,280 @@
module codegenerator
import incubaid.herolib.core.codeparser
import incubaid.herolib.core.pathlib
import incubaid.herolib.core.code
import incubaid.herolib.core.texttools
import os
pub struct CodeGenerator {
pub mut:
parser codeparser.CodeParser
output_dir string
format bool
}
// generate_all generates markdown docs for all modules
pub fn (mut gen CodeGenerator) generate_all() ! {
modules := gen.parser.list_modules()
for module_name in modules {
gen.generate_module(module_name)!
}
}
// generate_module generates markdown for a single module
pub fn (mut gen CodeGenerator) generate_module(module_name string) ! {
md := gen.module_to_markdown(module_name)!
// Convert module name to filename: incubaid.herolib.core.code -> code___core___code.md
filename := gen.module_to_filename(module_name)
filepath := os.join_path(gen.output_dir, filename)
mut file := pathlib.get_file(path: filepath, create: true)!
file.write(md)!
}
// module_to_markdown generates complete markdown for a module
pub fn (gen CodeGenerator) module_to_markdown(module_name string) !string {
module_obj := gen.parser.find_module(module_name)!
mut md := ''
// Use template for module header
md += $tmpl('templates/module.md.template')
// Imports section
imports := gen.parser.list_imports(module_name)
if imports.len > 0 {
md += gen.imports_section(imports)
}
// Constants section
consts := gen.parser.list_constants(module_name)
if consts.len > 0 {
md += gen.constants_section(consts)
}
// Structs section
structs := gen.parser.list_structs(module_name)
if structs.len > 0 {
md += gen.structs_section(structs, module_name)
}
// Functions section
functions := gen.parser.list_functions(module_name)
if functions.len > 0 {
md += gen.functions_section(functions, module_name)
}
// Interfaces section
interfaces := gen.parser.list_interfaces(module_name)
if interfaces.len > 0 {
md += gen.interfaces_section(interfaces)
}
return md
}
// imports_section generates imports documentation
fn (gen CodeGenerator) imports_section(imports []code.Import) string {
mut md := '## Imports\n\n'
for imp in imports {
md += '- `' + imp.mod + '`\n'
}
md += '\n'
return md
}
// constants_section generates constants documentation
fn (gen CodeGenerator) constants_section(consts []code.Const) string {
mut md := '## Constants\n\n'
for const_ in consts {
md += '- `' + const_.name + '` = `' + const_.value + '`\n'
}
md += '\n'
return md
}
// structs_section generates structs documentation
fn (gen CodeGenerator) structs_section(structs []code.Struct, module_name string) string {
mut md := '## Structs\n\n'
for struct_ in structs {
md += gen.struct_to_markdown(struct_)
}
return md
}
// functions_section generates functions documentation
fn (gen CodeGenerator) functions_section(functions []code.Function, module_name string) string {
mut md := '## Functions & Methods\n\n'
// Separate regular functions and methods
regular_functions := functions.filter(it.receiver.typ.symbol() == '')
methods := functions.filter(it.receiver.typ.symbol() != '')
// Regular functions
if regular_functions.len > 0 {
md += '### Functions\n\n'
for func in regular_functions {
md += gen.function_to_markdown(func)
}
}
// Methods (grouped by struct)
if methods.len > 0 {
md += '### Methods\n\n'
structs := gen.parser.list_structs(module_name)
for struct_ in structs {
struct_methods := methods.filter(it.receiver.typ.symbol().contains(struct_.name))
if struct_methods.len > 0 {
md += '#### ' + struct_.name + '\n\n'
for method in struct_methods {
md += gen.function_to_markdown(method)
}
}
}
}
return md
}
// interfaces_section generates interfaces documentation
fn (gen CodeGenerator) interfaces_section(interfaces []code.Interface) string {
mut md := '## Interfaces\n\n'
for iface in interfaces {
md += '### ' + iface.name + '\n\n'
if iface.description != '' {
md += iface.description + '\n\n'
}
md += '```v\n'
if iface.is_pub {
md += 'pub '
}
md += 'interface ' + iface.name + ' {\n'
for field in iface.fields {
md += ' ' + field.name + ': ' + field.typ.symbol() + '\n'
}
md += '}\n```\n\n'
}
return md
}
// struct_to_markdown converts struct to markdown
fn (gen CodeGenerator) struct_to_markdown(struct_ code.Struct) string {
mut md := '### '
if struct_.is_pub {
md += '**pub** '
}
md += 'struct ' + struct_.name + '\n\n'
if struct_.description != '' {
md += struct_.description + '\n\n'
}
md += '```v\n'
if struct_.is_pub {
md += 'pub '
}
md += 'struct ' + struct_.name + ' {\n'
for field in struct_.fields {
md += ' ' + field.name + ' ' + field.typ.symbol() + '\n'
}
md += '}\n'
md += '```\n\n'
// Field documentation
if struct_.fields.len > 0 {
md += '**Fields:**\n\n'
for field in struct_.fields {
visibility := if field.is_pub { 'public' } else { 'private' }
mutability := if field.is_mut { ', mutable' } else { '' }
md += '- `' + field.name + '` (`' + field.typ.symbol() + '`)' + mutability + ' - ' +
visibility + '\n'
if field.description != '' {
md += ' - ' + field.description + '\n'
}
}
md += '\n'
}
return md
}
// function_to_markdown converts function to markdown
fn (gen CodeGenerator) function_to_markdown(func code.Function) string {
mut md := ''
// Function signature
signature := gen.function_signature(func)
md += '- `' + signature + '`\n'
// Description
if func.description != '' {
md += ' - *' + func.description + '*\n'
}
// Parameters
if func.params.len > 0 {
md += '\n **Parameters:**\n'
for param in func.params {
md += ' - `' + param.name + '` (`' + param.typ.symbol() + '`)'
if param.description != '' {
md += ' - ' + param.description
}
md += '\n'
}
}
// Return type
if func.result.typ.symbol() != '' {
md += '\n **Returns:** `' + func.result.typ.symbol() + '`\n'
}
md += '\n'
return md
}
// function_signature generates a function signature string
fn (gen CodeGenerator) function_signature(func code.Function) string {
mut sig := if func.is_pub { 'pub ' } else { '' }
if func.receiver.name != '' {
sig += '(' + func.receiver.name + ' ' + func.receiver.typ.symbol() + ') '
}
sig += func.name
// Parameters
params := func.params.map(it.name + ': ' + it.typ.symbol()).join(', ')
sig += '(' + params + ')'
// Return type
if func.result.typ.symbol() != '' {
sig += ' -> ' + func.result.typ.symbol()
}
return sig
}
// module_to_filename converts module name to filename
// e.g., incubaid.herolib.core.code -> code__core__code.md
pub fn (gen CodeGenerator) module_to_filename(module_name string) string {
// Get last part after last dot, then add __ and rest in reverse
parts := module_name.split('.')
filename := parts[parts.len - 1]
return filename + '.md'
}

View File

@@ -1,5 +1,7 @@
module codegenerator
import incubaid.herolib.core.codeparser
@[params]
pub struct GeneratorOptions {
pub:
@@ -10,18 +12,16 @@ pub:
}
pub fn new(args GeneratorOptions) !CodeGenerator {
import incubaid.herolib.core.codeparser
mut parser := codeparser.new(
path: args.parser_path
recursive: args.recursive
)!
parser.parse()!
return CodeGenerator{
parser: parser
output_dir: args.output_dir
format: args.format
format: args.format
}
}
}

View File

@@ -0,0 +1,31 @@
module codegenerator
import incubaid.herolib.core.pathlib
pub struct MarkdownGenerator {
pub mut:
generator CodeGenerator
output_dir string
}
// write_all writes all generated markdown files to disk
pub fn (mut mgen MarkdownGenerator) write_all() ! {
modules := mgen.generator.parser.list_modules()
// Ensure output directory exists
mut out_dir := pathlib.get_dir(path: mgen.output_dir, create: true)!
for module_name in modules {
mgen.write_module(module_name)!
}
}
// write_module writes a single module's markdown to disk
pub fn (mut mgen MarkdownGenerator) write_module(module_name string) ! {
md := mgen.generator.module_to_markdown(module_name)!
filename := mgen.generator.module_to_filename(module_name)
filepath := mgen.output_dir + '/' + filename
mut file := pathlib.get_file(path: filepath, create: true)!
file.write(md)!
}

View File

@@ -0,0 +1,188 @@
module codegenerator
import incubaid.herolib.ui.console
import incubaid.herolib.core.codeparser
import incubaid.herolib.core.pathlib
import os
fn test_markdown_generation() {
console.print_header('CodeGenerator Markdown Test')
console.print_lf(1)
// Setup: Use the same test data as codeparser
test_dir := setup_test_directory()
defer {
os.rmdir_all(test_dir) or {}
}
// Create output directory
output_dir := '/tmp/codegen_output'
os.rmdir_all(output_dir) or {}
os.mkdir_all(output_dir) or { panic('Failed to create output dir') }
defer {
os.rmdir_all(output_dir) or {}
}
// Create generator
console.print_item('Creating CodeGenerator...')
mut gen := new(
parser_path: test_dir
output_dir: output_dir
recursive: true
)!
console.print_item('Parser found ${gen.parser.list_modules().len} modules')
console.print_lf(1)
// Test filename conversion
console.print_header('Test 1: Filename Conversion')
struct TestCase {
module_name string
expected string
}
test_cases := [
TestCase{
module_name: 'incubaid.herolib.core.code'
expected: 'code.md'
},
TestCase{
module_name: 'testdata'
expected: 'testdata.md'
},
TestCase{
module_name: 'testdata.services'
expected: 'services.md'
},
]
for test_case in test_cases {
result := gen.module_to_filename(test_case.module_name)
assert result == test_case.expected, 'Expected ${test_case.expected}, got ${result}'
console.print_item(' ${test_case.module_name} -> ${result}')
}
console.print_lf(1)
// Test module documentation generation
console.print_header('Test 2: Module Documentation Generation')
// Get a testdata module
modules := gen.parser.list_modules()
testdata_modules := modules.filter(it.contains('testdata'))
assert testdata_modules.len > 0, 'No testdata modules found'
for mod_name in testdata_modules {
console.print_item('Generating docs for: ${mod_name}')
md := gen.module_to_markdown(mod_name)!
// Validate markdown content
assert md.len > 0, 'Generated markdown is empty'
assert md.contains('# Module:'), 'Missing module header'
// List basic structure checks
structs := gen.parser.list_structs(mod_name)
functions := gen.parser.list_functions(mod_name)
consts := gen.parser.list_constants(mod_name)
if structs.len > 0 {
assert md.contains('## Structs'), 'Missing Structs section'
console.print_item(' - Found ${structs.len} structs')
}
if functions.len > 0 {
assert md.contains('## Functions'), 'Missing Functions section'
console.print_item(' - Found ${functions.len} functions')
}
if consts.len > 0 {
assert md.contains('## Constants'), 'Missing Constants section'
console.print_item(' - Found ${consts.len} constants')
}
}
console.print_lf(1)
// Test file writing
console.print_header('Test 3: Write Generated Files')
for mod_name in testdata_modules {
gen.generate_module(mod_name)!
}
// Verify files were created
files := os.ls(output_dir)!
assert files.len > 0, 'No files generated'
console.print_item('Generated ${files.len} markdown files:')
for file in files {
console.print_item(' - ${file}')
// Verify file content
filepath := os.join_path(output_dir, file)
content := os.read_file(filepath)!
assert content.len > 0, 'Generated file is empty: ${file}'
}
console.print_lf(1)
// Test content validation
console.print_header('Test 4: Content Validation')
for file in files {
filepath := os.join_path(output_dir, file)
content := os.read_file(filepath)!
// Check for required sections
has_module_header := content.contains('# Module:')
has_imports := content.contains('## Imports') || !content.contains('import ')
has_valid_format := content.contains('```v')
assert has_module_header, '${file}: Missing module header'
assert has_valid_format || file.contains('services'), '${file}: Invalid markdown format'
console.print_item(' ${file}: Valid content')
}
console.print_lf(1)
console.print_green(' All CodeGenerator tests passed!')
}
// Helper: Setup test directory (copy from codeparser test)
fn setup_test_directory() string {
test_dir := '/tmp/codegen_test_data'
os.rmdir_all(test_dir) or {}
current_file := @FILE
current_dir := os.dir(current_file)
// Navigate to codeparser testdata
codeparser_dir := os.join_path(os.dir(current_dir), 'codeparser')
testdata_dir := os.join_path(codeparser_dir, 'testdata')
if !os.is_dir(testdata_dir) {
panic('testdata directory not found at: ${testdata_dir}')
}
os.mkdir_all(test_dir) or { panic('Failed to create test directory') }
copy_directory(testdata_dir, test_dir) or { panic('Failed to copy testdata: ${err}') }
return test_dir
}
fn copy_directory(src string, dst string) ! {
entries := os.ls(src)!
for entry in entries {
src_path := os.join_path(src, entry)
dst_path := os.join_path(dst, entry)
if os.is_dir(src_path) {
os.mkdir_all(dst_path)!
copy_directory(src_path, dst_path)!
} else {
content := os.read_file(src_path)!
os.write_file(dst_path, content)!
}
}
}

View File

@@ -0,0 +1 @@
fn ${func.name}(${func.params.map(it.name + ': ' + it.typ.symbol()).join(', ')}) ${func.result.typ.symbol()}

View File

@@ -0,0 +1,5 @@
# Module: ${module_name}
This module provides functionality for code generation and documentation.
**Location:** `${module_name.replace('.', '/')}`

View File

@@ -0,0 +1,2 @@
struct ${struct_.name} {
}

View File

@@ -204,4 +204,4 @@ pub fn (parser CodeParser) to_json(module_name string) !string {
}
return json.encode_pretty(result)
}
}

View File

@@ -0,0 +1,22 @@
module heromodels
import incubaid.herolib.develop.gittools
import incubaid.herolib.core.pathlib
pub fn aiprompts_path() !string {
return gittools.path(
git_url: 'https://github.com/Incubaid/herolib/tree/development/aiprompts'
)!.path
}
pub fn ai_instructions_hero_models() !string {
path := '${aiprompts_path()!}/ai_instructions_hero_models.md'
mut ppath := pathlib.get_file(path: path, create: false)!
return ppath.read()!
}
pub fn ai_instructions_vlang_herolib_core() !string {
path := '${aiprompts_path()!}/vlang_herolib_core.md'
mut ppath := pathlib.get_file(path: path, create: false)!
return ppath.read()!
}

View File

@@ -0,0 +1,182 @@
module heromodels
import incubaid.herolib.core.pathlib
import incubaid.herolib.ui.console
import incubaid.herolib.ai.client
import os
pub fn do() {
console.print_header('Code Generator - V File Analyzer Using AI')
// Find herolib root directory using @FILE
script_dir := os.dir(@FILE)
// Navigate from examples/core/code to root: up 4 levels
herolib_root := os.dir(os.dir(os.dir(script_dir)))
console.print_item('HeroLib Root: ${herolib_root}')
// The directory we want to analyze (lib/core in this case)
target_dir := herolib_root + '/lib/core'
console.print_item('Target Directory: ${target_dir}')
console.print_lf(1)
// Load instruction files from aiprompts
console.print_item('Loading instruction files...')
mut ai_instructions_file := pathlib.get(herolib_root +
'/aiprompts/ai_instructions_hero_models.md')
mut vlang_core_file := pathlib.get(herolib_root + '/aiprompts/vlang_herolib_core.md')
ai_instructions_content := ai_instructions_file.read()!
vlang_core_content := vlang_core_file.read()!
console.print_green(' Instruction files loaded successfully')
console.print_lf(1)
// Initialize AI client
console.print_item('Initializing AI client...')
mut aiclient := client.new()!
console.print_green(' AI client initialized')
console.print_lf(1)
// Get all V files from target directory
console.print_item('Scanning directory for V files...')
mut target_path := pathlib.get_dir(path: target_dir, create: false)!
mut all_files := target_path.list(
regex: [r'\.v$']
recursive: true
)!
console.print_item('Found ${all_files.paths.len} total V files')
// TODO: Walk over all files which do NOT end with _test.v and do NOT start with factory
// Each file becomes a src_file_content object
mut files_to_process := []pathlib.Path{}
for file in all_files.paths {
file_name := file.name()
// Skip test files
if file_name.ends_with('_test.v') {
continue
}
// Skip factory files
if file_name.starts_with('factory') {
continue
}
files_to_process << file
}
console.print_green(' After filtering: ${files_to_process.len} files to process')
console.print_lf(2)
// Process each file with AI
total_files := files_to_process.len
for idx, mut file in files_to_process {
current_idx := idx + 1
process_file_with_ai(mut aiclient, mut file, ai_instructions_content, vlang_core_content,
current_idx, total_files)!
}
console.print_lf(1)
console.print_header(' Code Generation Complete')
console.print_item('Processed ${files_to_process.len} files')
console.print_lf(1)
}
fn process_file_with_ai(mut aiclient client.AIClient, mut file pathlib.Path, ai_instructions string, vlang_core string, current int, total int) ! {
file_name := file.name()
src_file_path := file.absolute()
console.print_item('[${current}/${total}] Analyzing: ${file_name}')
// Read the file content - this is the src_file_content
src_file_content := file.read()!
// Build comprehensive system prompt
// TODO: Load instructions from prompt files and use in prompt
// Build the user prompt with context
user_prompt := '
File: ${file_name}
Path: ${src_file_path}
Current content:
\`\`\`v
${src_file_content}
\`\`\`
Please improve this V file by:
1. Following V language best practices
2. Ensuring proper error handling with ! and or blocks
3. Adding clear documentation comments
4. Following herolib patterns and conventions
5. Improving code clarity and readability
Context from herolib guidelines:
VLANG HEROLIB CORE:
${vlang_core}
AI INSTRUCTIONS FOR HERO MODELS:
${ai_instructions}
Return ONLY the complete improved file wrapped in \`\`\`v code block.
'
console.print_debug_title('Sending to AI', 'Calling AI model to improve ${file_name}...')
// TODO: Call AI client with model gemini-3-pro
aiclient.write_from_prompt(file, user_prompt, [.pro]) or {
console.print_stderr('Error processing ${file_name}: ${err}')
return
}
mut improved_file := pathlib.get(src_file_path + '.improved')
improved_content := improved_file.read()!
// Display improvements summary
sample_chars := 250
preview := if improved_content.len > sample_chars {
improved_content[..sample_chars] + '... (preview truncated)'
} else {
improved_content
}
console.print_debug_title('AI Analysis Results for ${file_name}', preview)
// Optional: Save improved version for review
// Uncomment to enable saving
// improved_file_path := src_file_path + '.improved'
// mut improved_file := pathlib.get_file(path: improved_file_path, create: true)!
// improved_file.write(improved_content)!
// console.print_green(' Improvements saved to: ${improved_file_path}')
console.print_lf(1)
}
// Extract V code from markdown code block
fn extract_code_block(response string) string {
// Look for ```v ... ``` block
start_marker := '\`\`\`v'
end_marker := '\`\`\`'
start_idx := response.index(start_marker) or {
// If no ```v, try to return as-is
return response
}
mut content_start := start_idx + start_marker.len
if content_start < response.len && response[content_start] == `\n` {
content_start++
}
end_idx := response.index(end_marker) or { return response[content_start..] }
extracted := response[content_start..end_idx]
return extracted.trim_space()
}

View File

@@ -0,0 +1,25 @@
File: ${file_name}
Path: ${src_file_path}
Current content:
```v
${src_file_content}
```
Please improve this V file by:
1. Following V language best practices
2. Ensuring proper error handling with ! and or blocks
3. Adding clear documentation comments
4. Following herolib patterns and conventions
5. Improving code clarity and readability
Context from herolib guidelines:
VLANG HEROLIB CORE:
${vlang_core}
AI INSTRUCTIONS FOR HERO MODELS:
${ai_instructions}
Return ONLY the complete improved file wrapped in ```v code block.

View File

@@ -2,7 +2,6 @@ module pathlib
import os
import regex
// import incubaid.herolib.core.smartid
import incubaid.herolib.ui.console
@[params]
@@ -38,6 +37,7 @@ pub mut:
// example see https://github.com/incubaid/herolib/blob/development/examples/core/pathlib/examples/list/path_list.v
//
// e.g. p.list(regex:[r'.*\.v$'])! //notice the r in front of string, this is regex for all files ending with .v
// e.g.
//
// ```
// please note links are ignored for walking over dirstructure (for files and dirs)