This commit is contained in:
2025-10-16 09:51:42 +04:00
parent 91fdf9a774
commit 099b21510d
6 changed files with 283 additions and 106 deletions

View File

@@ -124,3 +124,48 @@ fn test_export_without_includes() {
exported := os.read_file('${export_path}/test_col2/page1.md')!
assert exported.contains('!!include')
}
fn test_error_deduplication() {
mut a := new(name: 'test')!
mut col := a.new_collection(name: 'test', path: test_base)!
// Report same error twice
col.error(
category: .missing_include
page_key: 'test:page1'
message: 'Test error'
)
col.error(
category: .missing_include
page_key: 'test:page1'
message: 'Test error' // Same hash, should be deduplicated
)
assert col.errors.len == 1
// Different page_key = different hash
col.error(
category: .missing_include
page_key: 'test:page2'
message: 'Test error'
)
assert col.errors.len == 2
}
fn test_error_hash() {
err1 := CollectionError{
category: .missing_include
page_key: 'col:page1'
message: 'Error message'
}
err2 := CollectionError{
category: .missing_include
page_key: 'col:page1'
message: 'Different message' // Hash is same!
}
assert err1.hash() == err2.hash()
}

View File

@@ -3,6 +3,7 @@ module atlas
import incubaid.herolib.core.pathlib
import incubaid.herolib.core.texttools
import incubaid.herolib.core.base
import incubaid.herolib.ui.console
import os
@[heap]
@@ -15,6 +16,7 @@ pub mut:
files map[string]&File
atlas &Atlas @[skip; str: skip] // Reference to parent atlas for include resolution
errors []CollectionError
error_cache map[string]bool // Track error hashes to avoid duplicates
}
@[params]
@@ -33,6 +35,7 @@ fn (mut self Atlas) new_collection(args CollectionNewArgs) !Collection {
name: name
path: path
atlas: &self // Set atlas reference
error_cache: map[string]bool{}
}
return col
@@ -208,3 +211,87 @@ pub fn (mut c Collection) export(args CollectionExportArgs) ! {
redis.hset('atlas:path', c.name, col_dir.path)!
}
}
@[params]
pub struct CollectionErrorArgs {
pub mut:
category CollectionErrorCategory @[required]
message string @[required]
page_key string
file string
show_console bool // Show error in console immediately
log_error bool = true // Log to errors array (default: true)
}
// Report an error, avoiding duplicates based on hash
pub fn (mut c Collection) error(args CollectionErrorArgs) {
// Create error struct
err := CollectionError{
category: args.category
page_key: args.page_key
message: args.message
file: args.file
}
// Calculate hash for deduplication
hash := err.hash()
// Check if this error was already reported
if hash in c.error_cache {
return // Skip duplicate
}
// Mark this error as reported
c.error_cache[hash] = true
// Log to errors array if requested
if args.log_error {
c.errors << err
}
// Show in console if requested
if args.show_console {
console.print_stderr('[${c.name}] ${err.str()}')
}
}
// Get all errors
pub fn (c Collection) get_errors() []CollectionError {
return c.errors
}
// Check if collection has errors
pub fn (c Collection) has_errors() bool {
return c.errors.len > 0
}
// Clear all errors
pub fn (mut c Collection) clear_errors() {
c.errors = []CollectionError{}
c.error_cache = map[string]bool{}
}
// Get error summary by category
pub fn (c Collection) error_summary() map[CollectionErrorCategory]int {
mut summary := map[CollectionErrorCategory]int{}
for err in c.errors {
summary[err.category] = summary[err.category] + 1
}
return summary
}
// Print all errors to console
pub fn (c Collection) print_errors() {
if c.errors.len == 0 {
console.print_green('Collection ${c.name}: No errors')
return
}
console.print_header('Collection ${c.name} - Errors (${c.errors.len})')
for err in c.errors {
console.print_stderr(' ${err.str()}')
}
}

View File

@@ -1,22 +1,61 @@
module atlas
import crypto.md5
import incubaid.herolib.ui.console
pub enum CollectionErrorCategory {
circular_include
missing_include
include_syntax_error
circular_include
page_not_found
invalid_page_reference
file_not_found
collection_not_found
other
invalid_collection
general_error
}
pub struct CollectionError {
pub:
page_key string // "collection:page_name" if applicable
message string
pub mut:
category CollectionErrorCategory
page_key string // Format: "collection:page" or just collection name
message string
file string // Optional: specific file path if relevant
}
pub fn (e CollectionError) markdown() string {
return 'ERROR [${e.category.str()}]: ${e.message}' + (if e.page_key != '' { ' (Page: `${e.page_key}`)' } else { '' })
// Generate MD5 hash for error deduplication
// Hash is based on category + page_key (or file if page_key is empty)
pub fn (e CollectionError) hash() string {
mut hash_input := '${e.category}'
if e.page_key != '' {
hash_input += ':${e.page_key}'
} else if e.file != '' {
hash_input += ':${e.file}'
}
return md5.hexhash(hash_input)
}
// Get human-readable error message
pub fn (e CollectionError) str() string {
mut location := ''
if e.page_key != '' {
location = ' [${e.page_key}]'
} else if e.file != '' {
location = ' [${e.file}]'
}
return '[${e.category}]${location}: ${e.message}'
}
// Get category as string
pub fn (e CollectionError) category_str() string {
return match e.category {
.circular_include { 'Circular Include' }
.missing_include { 'Missing Include' }
.include_syntax_error { 'Include Syntax Error' }
.invalid_page_reference { 'Invalid Page Reference' }
.file_not_found { 'File Not Found' }
.invalid_collection { 'Invalid Collection' }
.general_error { 'General Error' }
}
}

View File

@@ -26,5 +26,10 @@ pub fn (mut a Atlas) export(args ExportArgs) ! {
include: args.include
redis: args.redis
)!
// Print errors for this collection if any
if col.has_errors() {
col.print_errors()
}
}
}

View File

@@ -2,7 +2,6 @@ module atlas
import incubaid.herolib.core.pathlib
import incubaid.herolib.core.texttools
import incubaid.herolib.data.atlas.collection_error { CollectionError, CollectionErrorCategory }
@[heap]
pub struct Page {
@@ -59,12 +58,13 @@ fn (mut p Page) process_includes(content string, mut visited map[string]bool) !s
// Prevent circular includes
page_key := p.key()
if page_key in visited {
p.collection.errors << CollectionError{
page_key: page_key
message: 'Circular include detected for page `${page_key}`.'
p.collection.error(
category: .circular_include
}
return '' // Return empty string for circular includes
page_key: page_key
message: 'Circular include detected for page `${page_key}`'
show_console: false // Don't show immediately, collect for later
)
return ''
}
visited[page_key] = true
@@ -90,11 +90,12 @@ fn (mut p Page) process_includes(content string, mut visited map[string]bool) !s
target_collection = texttools.name_fix(parts[0])
target_page = texttools.name_fix(parts[1])
} else {
p.collection.errors << CollectionError{
page_key: page_key
message: 'Invalid include format: `${include_ref}`.'
p.collection.error(
category: .include_syntax_error
}
page_key: page_key
message: 'Invalid include format: `${include_ref}`'
show_console: false
)
processed_lines << '<!-- Invalid include format: ${include_ref} -->'
continue
}
@@ -112,12 +113,12 @@ fn (mut p Page) process_includes(content string, mut visited map[string]bool) !s
// Get the referenced page from atlas
mut include_page := atlas.page_get(page_ref) or {
p.collection.errors << CollectionError{
page_key: page_key
message: 'Included page `${page_ref}` not found.'
p.collection.error(
category: .missing_include
}
// If page not found, keep original line as comment
page_key: page_key
message: 'Included page `${page_ref}` not found'
show_console: false
)
processed_lines << '<!-- Include not found: ${page_ref} -->'
continue
}

View File

@@ -25,8 +25,8 @@ fn (mut a Atlas) scan_directory(mut dir pathlib.Path) ! {
}
// Scan subdirectories
entries := dir.list(recursive: false)!
for entry in entries.paths {
mut entries := dir.list(recursive: false)!
for mut entry in entries.paths {
if !entry.is_dir() || should_skip_dir(entry) {
continue
}
@@ -69,9 +69,9 @@ fn (mut c Collection) scan() ! {
}
fn (mut c Collection) scan_path(mut dir pathlib.Path) ! {
entries := dir.list(recursive: false)!
mut entries := dir.list(recursive: false)!
for entry in entries.paths {
for mut entry in entries.paths {
// Skip hidden files/dirs
if entry.name().starts_with('.') || entry.name().starts_with('_') {
continue