package board import ( "fmt" "regexp" "strings" "time" ) // BoardValidationError represents a validation error type BoardValidationError struct { Type string // "missing_header", "invalid_row", "malformed_table", etc. Message string Line int Context string } // BoardValidator validates and repairs board files type BoardValidator struct { boardPath string errors []BoardValidationError warnings []string } // NewBoardValidator creates a new board validator func NewBoardValidator(boardPath string) *BoardValidator { return &BoardValidator{ boardPath: boardPath, errors: make([]BoardValidationError, 0), warnings: make([]string, 0), } } // TaskRow represents a parsed task row from the board type TaskRow struct { ID string Description string Status string // "[x]", "[ ]" Branch string Verification string LineNo int } // ValidateBoard validates the board structure func (bv *BoardValidator) ValidateBoard(content string) bool { bv.errors = make([]BoardValidationError, 0) bv.warnings = make([]string, 0) lines := strings.Split(content, "\n") // Check for required headers if !bv.hasValidHeader(lines) { bv.errors = append(bv.errors, BoardValidationError{ Type: "missing_header", Message: "Board must have a valid markdown header", }) return false } // Check for table separator if !bv.hasTableSeparator(lines) { bv.errors = append(bv.errors, BoardValidationError{ Type: "missing_table_separator", Message: "Board must have a markdown table separator line (|---|---|...)", }) return false } // Validate task rows tableStartIdx := bv.findTableStart(lines) if tableStartIdx >= 0 { bv.validateTaskRows(lines[tableStartIdx:], tableStartIdx) } return len(bv.errors) == 0 } // hasValidHeader checks if the board has a valid header func (bv *BoardValidator) hasValidHeader(lines []string) bool { for _, line := range lines { line = strings.TrimSpace(line) if strings.HasPrefix(line, "#") && strings.Contains(line, "Task Board") { return true } } return false } // hasTableSeparator checks if the board has a table separator func (bv *BoardValidator) hasTableSeparator(lines []string) bool { for _, line := range lines { if strings.Contains(line, "|") && strings.Contains(line, "-") && strings.Contains(line, "-|-") { return true } } return false } // findTableStart finds the start of the task table func (bv *BoardValidator) findTableStart(lines []string) int { for i, line := range lines { line = strings.TrimSpace(line) if strings.HasPrefix(line, "|") && !strings.Contains(line, "---") && !strings.Contains(line, "ID") { continue } if strings.HasPrefix(line, "|") && strings.Contains(line, "ID") { return i + 2 // Skip header and separator } } return -1 } // validateTaskRows validates all task rows in the table func (bv *BoardValidator) validateTaskRows(lines []string, startIdx int) { for i, line := range lines { line = strings.TrimSpace(line) if line == "" || !strings.HasPrefix(line, "|") { break } if strings.Contains(line, "---") { continue // Skip separator } lineNo := startIdx + i err := bv.validateTaskRow(line, lineNo) if err.Message != "" { bv.errors = append(bv.errors, err) } } } // validateTaskRow validates a single task row func (bv *BoardValidator) validateTaskRow(line string, lineNo int) BoardValidationError { parts := strings.Split(line, "|") // Should have at least 6 parts: [empty, ID, Desc, Status, Branch, Verif, empty] if len(parts) < 6 { return BoardValidationError{ Type: "invalid_row", Message: fmt.Sprintf("Invalid row format (expected at least 5 columns, got %d)", len(parts)-2), Line: lineNo, Context: line, } } id := strings.TrimSpace(parts[1]) status := strings.TrimSpace(parts[3]) // Validate ID (should be T1.1 format or similar) if !isValidTaskID(id) { bv.warnings = append(bv.warnings, fmt.Sprintf("Line %d: Invalid task ID format: %s", lineNo, id)) } // Validate status (should be [x] or [ ]) if status != "[x]" && status != "[ ]" && status != "[X]" { return BoardValidationError{ Type: "invalid_status", Message: fmt.Sprintf("Status must be '[x]' or '[ ]', got '%s'", status), Line: lineNo, Context: line, } } return BoardValidationError{} // Valid } // isValidTaskID checks if a task ID is valid func isValidTaskID(id string) bool { // Match patterns like T0, T1.1, T1.2, etc. pattern := regexp.MustCompile(`^T\d+(\.\d+)?$`) return pattern.MatchString(id) } // ParseTasks parses all tasks from board content func (bv *BoardValidator) ParseTasks(content string) ([]TaskRow, error) { lines := strings.Split(content, "\n") tasks := make([]TaskRow, 0) tableStartIdx := bv.findTableStart(lines) if tableStartIdx < 0 { return nil, fmt.Errorf("no task table found") } for i := tableStartIdx; i < len(lines); i++ { line := strings.TrimSpace(lines[i]) if line == "" || !strings.HasPrefix(line, "|") { break } if strings.Contains(line, "---") { continue } parts := strings.Split(line, "|") if len(parts) < 6 { continue } task := TaskRow{ ID: strings.TrimSpace(parts[1]), Description: strings.TrimSpace(parts[2]), Status: strings.TrimSpace(parts[3]), Branch: strings.TrimSpace(parts[4]), Verification: strings.TrimSpace(parts[5]), LineNo: i, } if task.ID != "" { tasks = append(tasks, task) } } return tasks, nil } // GetErrors returns validation errors func (bv *BoardValidator) GetErrors() []BoardValidationError { return bv.errors } // GetWarnings returns validation warnings func (bv *BoardValidator) GetWarnings() []string { return bv.warnings } // HasErrors checks if there are any errors func (bv *BoardValidator) HasErrors() bool { return len(bv.errors) > 0 } // ErrorSummary returns a summary of errors func (bv *BoardValidator) ErrorSummary() string { if len(bv.errors) == 0 { return "No errors found" } summary := fmt.Sprintf("Found %d error(s):\n", len(bv.errors)) for i, err := range bv.errors { summary += fmt.Sprintf("%d. [Line %d] %s: %s\n", i+1, err.Line, err.Type, err.Message) if err.Context != "" { summary += fmt.Sprintf(" Context: %s\n", err.Context) } } return summary } // Warnings returns all warnings func (bv *BoardValidator) WarningsSummary() string { if len(bv.warnings) == 0 { return "No warnings found" } summary := fmt.Sprintf("Found %d warning(s):\n", len(bv.warnings)) for i, warn := range bv.warnings { summary += fmt.Sprintf("%d. %s\n", i+1, warn) } return summary } // RepairBoard attempts to repair common board issues func (bv *BoardValidator) RepairBoard(content string) (string, error) { lines := strings.Split(content, "\n") // Add header if missing if !bv.hasValidHeader(lines) { newLines := make([]string, 0) newLines = append(newLines, "# Task Board — Milestone T1: Production Hardening") newLines = append(newLines, "") newLines = append(newLines, "**Submilestone:** T1 (Error recovery, observability, metrics, reliability)") newLines = append(newLines, "") newLines = append(newLines, lines...) lines = newLines } // Add table separator if missing if !bv.hasTableSeparator(lines) { for i, line := range lines { if strings.HasPrefix(line, "|") && strings.Contains(line, "ID") { // Insert separator after header newLines := make([]string, 0) newLines = append(newLines, lines[:i+1]...) newLines = append(newLines, "|---|---|---|---|---|") newLines = append(newLines, lines[i+1:]...) lines = newLines break } } } // Repair invalid status values for i, line := range lines { if strings.Contains(line, "|") && !strings.Contains(line, "---|") { // Replace invalid status markers line = strings.ReplaceAll(line, "[ ]", "[ ]") // Normalize line = strings.ReplaceAll(line, "[X]", "[x]") // Normalize lines[i] = line } } return strings.Join(lines, "\n"), nil } // BoardDivergence represents a difference between expected and actual state type BoardDivergence struct { TaskID string ExpectedStatus string ActualStatus string DiscoveredAt time.Time } // DetectDivergence detects differences between expected and actual task states func (bv *BoardValidator) DetectDivergence(content string, actualStates map[string]bool) []BoardDivergence { tasks, err := bv.ParseTasks(content) if err != nil { return nil } divergences := make([]BoardDivergence, 0) for _, task := range tasks { expectedComplete := task.Status == "[x]" actualComplete, exists := actualStates[task.ID] if !exists { // Task not in actual state - assume not complete actualComplete = false } if expectedComplete != actualComplete { divergences = append(divergences, BoardDivergence{ TaskID: task.ID, ExpectedStatus: fmt.Sprintf("%v", expectedComplete), ActualStatus: fmt.Sprintf("%v", actualComplete), DiscoveredAt: time.Now(), }) } } return divergences } // HealDivergence updates board to match actual state func (bv *BoardValidator) HealDivergence(content string, actualStates map[string]bool) (string, []string, error) { lines := strings.Split(content, "\n") changes := make([]string, 0) for i, line := range lines { if !strings.HasPrefix(strings.TrimSpace(line), "|") || strings.Contains(line, "---") || strings.Contains(line, "ID") { continue } parts := strings.Split(line, "|") if len(parts) < 4 { continue } taskID := strings.TrimSpace(parts[1]) currentStatus := strings.TrimSpace(parts[3]) if actualState, exists := actualStates[taskID]; exists { var expectedStatus string if actualState { expectedStatus = "[x]" } else { expectedStatus = "[ ]" } if currentStatus != expectedStatus { // Update the status parts[3] = " " + expectedStatus + " " lines[i] = strings.Join(parts, "|") changes = append(changes, fmt.Sprintf("Fixed %s: %s → %s", taskID, currentStatus, expectedStatus)) } } } return strings.Join(lines, "\n"), changes, nil }