Module: Parsing & Validation Priority: CRITICAL (syntax rules), HIGH (semantic rules) Estimated Duration: 8-10 days Dependencies: tasks.markdown-parser (all tasks)
Implement the linter that validates Lash Markdown files for both syntax and semantic correctness. The linter enforces the strict, predictable format that makes Lash agent-friendly.
Key Principle: Linter provides rich, actionable diagnostics with auto-fix suggestions where possible.
Design: Rule-based architecture where each rule is independent and can be enabled/disabled.
- Create
LintRuletrait inlash-core/src/linter/rule.rs- Methods:
-
fn code(&self) -> &'static str- Stable rule code (e.g., "E_DEPTH_EXCEEDED") -
fn severity(&self) -> Severity- Error, Warning, or Info -
fn check_file(&self, file: &TaskFile, ctx: &LintContext) -> Vec<Diagnostic> -
fn check_task(&self, task: &Task, ctx: &LintContext) -> Vec<Diagnostic>
-
- Some rules apply to whole file, some to individual tasks
- Methods:
- Define
Diagnosticstruct (extend from error-handling)- Fields:
-
code: &'static str- Rule code -
severity: Severity -
message: String -
location: Location- File, line, column, span -
suggestion: Option<Fix>- Auto-fix if available
-
- Methods:
-
to_human_string() -> String- Colored, formatted for terminal (via Display trait) -
to_json() -> String- Machine-readable format
-
- Fields:
- Define
Fixstruct for auto-fixes- Fields:
-
description: String- What the fix does -
replacement: Replacement- Text replacement or operation
-
- Support types:
- Text replacement (old β new)
- Insertion (at position)
- Deletion (range)
- Reformat (whole file)
- Fields:
- Create
Linterstruct- Fields:
-
rules: Vec<Arc<dyn LintRule>>- Registered rules (using Arc for shared ownership) -
config: LintConfig- Enable/disable rules, severity overrides
-
- Methods:
-
new(config: LintConfig) -> Self -
register_rule(rule: Arc<dyn LintRule>)- Add rule -
lint_file(file: &TaskFile) -> Vec<Diagnostic>- Run all rules -
lint_project(files: &HashMap<PathBuf, TaskFile>) -> Vec<Diagnostic>- Lint all files
-
- Fields:
- Create
LintContextfor rule execution- Provides shared data to rules:
-
config: &LashConfig- Project config -
file_path: PathBuf- Current file -
all_files: &HashMap<PathBuf, TaskFile>- For cross-file validation - Method:
is_annotation_allowed(&self, key: &str)- Validates custom annotations
-
- Provides shared data to rules:
- Define
LintConfigfor linter configuration- Fields:
-
enabled_rules: HashSet<String>- Which rules to run -
disabled_rules: HashSet<String>- Which to skip -
severity_overrides: HashMap<String, Severity>- Override default severity -
auto_fix: bool- Apply fixes automatically
-
- Serde support for loading from
.lash/config.tomlunder[linter]section
- Fields:
- Create rule registry
- Struct
RuleRegistrywith category-based organization - Function
register_default_rules() -> RuleRegistry- Returns registry (rules will be added in Task #2/#3) - Organized by category: syntax, semantic, cross-file
- Struct
Priority: CRITICAL Estimate: 1 day Dependencies: tasks.markdown-parser#6 Success Criteria: Rule engine works; can run rules and collect diagnostics
- Rule: Valid Checkbox Pattern
- Code:
E_SYNTAX_CHECKBOX - Check:
- [X]where X is,x,X,-, or! - Error on:
- [],- [ x](extra space),- [v], etc. - Suggestion: Show valid patterns
- Auto-fix: None (ambiguous intent)
- Note: Validation performed at parse time (see rule documentation)
- Code:
- Rule: Consistent Indentation
- Code:
E_SYNTAX_INDENT - Check: All checkbox lines use exactly 2 spaces per level
- Error on: Tabs, 4 spaces, 3 spaces, mixed
- Suggestion: "Use 2 spaces per indentation level"
- Auto-fix: Normalize to 2 spaces (calculate depth, reformat)
- Note: Validation performed at parse time (see rule documentation)
- Code:
- Rule: Depth Limit
- Code:
E_SYNTAX_DEPTH - Check: Task depth β€ 2 (3 levels: 0, 1, 2)
- Error on: Depth 3+ (6+ spaces of indentation)
- Suggestion: "Split deep hierarchies into separate files"
- Auto-fix: None (requires restructuring)
- Code:
- Rule: Valid Annotation Syntax
- Code:
E_SYNTAX_ANNOTATION - Check: Lines starting with
@match@key: valueformat - Error on:
@key value(no colon),@ key: value(space after @) - Suggestion: Show correct format with example
- Auto-fix: Add missing colon if detectable
- Note: Validation performed at parse time (see rule documentation)
- Code:
- Rule: Unknown Annotation Keys
- Code:
E_SYNTAX_UNKNOWN_KEY - Check:
@keyis in built-in list OR custom_keys from config - Built-in: id, labels, status, owner, created, estimate, depends-on, agent-note
- Error on: Unknown key not in either list
- Suggestion: "Add to .lash/config.toml [annotations.custom_keys] or fix typo"
- Auto-fix: None (ambiguous whether typo or intentional)
- Include fuzzy match suggestions for likely typos
- Code:
- Rule: Header Structure
- Code:
W_SYNTAX_HEADER - Check: File has H1 title and "## Tasks" section
- Warning on: Missing H1, missing Tasks section
- Suggestion: Add required sections
- Auto-fix: Insert template header structure
- Note: Only checks H1 title with parsed structure; "## Tasks" check requires raw content
- Code:
- Rule: Annotation Ordering
- Code:
I_SYNTAX_ORDER(Info severity) - Check: Annotations in alphabetical order (optional style)
- Info on: Out of order
- Suggestion: "Consider sorting for consistency"
- Auto-fix: Sort annotations alphabetically
- Note: Requires raw content access; deferred to formatter
- Code:
- Write tests for each rule
- Valid cases (should pass)
- Invalid cases (should fail)
- Edge cases
- Auto-fix application
- 30 tests total across all 7 rules (comprehensive coverage)
Priority: CRITICAL Estimate: 2 days Dependencies: Task #1 Success Criteria: All syntax rules implemented; catches formatting errors; provides fixes
- Rule: ID Uniqueness Within File
- Code:
E_SEM_DUPLICATE_ID - Check: No two tasks in same file have same ID
- Error on: Duplicate IDs
- Provide line numbers for all occurrences
- Suggestion: "Rename one of these tasks"
- Auto-fix: None (ambiguous which to keep)
- Code:
- Rule: Parent-Child Status Consistency
- Code:
W_SEM_STATUS_INCONSISTENT - Check: Parent cannot be Done if children are Open
- Warning on: Parent marked [x] but has [ ] children
- Suggestion: "Complete all children first, or waive them"
- Auto-fix: Option 1: Unmark parent; Option 2: Mark children
- Default auto-fix: Unmark parent (safer)
- Code:
- Rule: Waived Children (auto-fix)
- Code:
I_SEM_AUTO_WAIVE - Check: Parent is Waived
- Action: Auto-waive all children (per design decision)
- Info severity: "Auto-waiving children due to waived parent"
- Auto-fix: Set all descendant status to Waived
- Always apply in formatter
- Code:
- Rule: Valid Label Format
- Code:
E_SEM_INVALID_LABEL - Check: Labels match pattern:
[a-z0-9][a-z0-9-_]* - Error on: Uppercase, spaces, special chars, starts with number
- Suggestion: "Labels must be lowercase alphanumeric with - or _"
- Auto-fix: Normalize label (lowercase, replace spaces with -)
- Code:
- Rule: Valid Date Format
- Code:
E_SEM_INVALID_DATE - Check:
@createdmatches YYYY-MM-DD - Error on: Other formats, invalid dates (Feb 30, etc.)
- Suggestion: "Use YYYY-MM-DD format"
- Auto-fix: Attempt to parse and reformat if possible
- Code:
- Rule: Valid Estimate Format
- Code:
E_SEM_INVALID_ESTIMATE - Check:
@estimatematches pattern:\d+[hdwmy] - h=hours, d=days, w=weeks, m=months, y=years
- Error on: Invalid format
- Suggestion: "Use format like: 2h, 3d, 1w"
- Auto-fix: None (ambiguous conversion)
- Code:
- Rule: Valid Owner Format
- Code:
W_SEM_OWNER_FORMAT - Check:
@owneris non-empty, reasonable length - Warning on: Very long names (>100 chars)
- Suggestion: "Owner name seems unusually long"
- Auto-fix: Trim to reasonable length
- Code:
- Rule: Empty Task Title
- Code:
E_SEM_EMPTY_TITLE - Check: Task title is not empty or whitespace-only
- Error on:
- [ ]with no text - Suggestion: "Tasks must have a title"
- Auto-fix: None (needs content)
- Code:
- Write tests for each rule
- Valid cases
- Invalid cases (duplicates, inconsistencies, format errors)
- Auto-fix application
- Complex scenarios (parent-child chains)
- 5-10 tests per rule = 40-80 tests total (69 tests written)
Priority: HIGH Estimate: 2 days Dependencies: Task #1 Success Criteria: Semantic rules catch logical errors; status consistency enforced
- Rule: Dependency Reference Exists
- Code:
E_LINK_NOT_FOUND - Check:
@depends-ontargets exist - For file refs: Check file exists in project
- For task refs: Check file exists AND contains task ID
- Error on: Broken references
- Provide: Path to missing file/task
- Suggestion: Check path spelling, or create the file/task
- Auto-fix: None (can't auto-create tasks)
- Code:
- Rule: Circular Dependencies
- Code:
E_LINK_CYCLE - Check: No cycles in dependency graph
- Build graph from all
@depends-onannotations - Run cycle detection (DFS with visited set)
- Error on: Any cycle detected
- Provide: Full cycle path (A β B β C β A)
- Suggestion: "Remove one dependency to break cycle"
- Auto-fix: None (ambiguous which edge to remove)
- Code:
- Rule: Root Index File References
- Code:
E_INDEX_FILE_MISSING - Check: Files referenced in root index exist
- Parse root index checkbox list
- Verify each referenced file path exists
- Error on: Missing files
- Suggestion: "Create file or remove from index"
- Auto-fix: Option to remove from index
- Code:
- Rule: Orphaned Files
- Code:
W_INDEX_ORPHAN - Check: All .md files in project are in root index
- Warning on: Files not referenced in index
- Suggestion: "Add to lash.index.md or move to archive"
- Auto-fix: Add to index under appropriate section
- Code:
- Rule: Valid Dependency Path Resolution
- Code:
E_LINK_INVALID_PATH - Check: Relative paths resolve correctly
-
../core/api.mdfromtasks/ui/login.mdresolves - Error on: Paths escaping project root
- Error on: Malformed paths (double //, etc.)
- Suggestion: Fix path syntax
- Auto-fix: Normalize path separators
- Code:
- Implement cross-file context
-
LintContextincludes all parsed files - Build dependency graph across project
- Cache file lookups for performance
- Support incremental linting (only check changed files' deps)
-
- Write tests
- Valid cross-file dependencies
- Broken file references
- Broken task references
- Circular dependencies (various patterns)
- Index validation
- Orphaned files detection
- 25+ tests for cross-file scenarios
Priority: MEDIUM (can lint single files without this) Estimate: 2 days Dependencies: tasks.dependency-resolution#1 (graph building) Success Criteria: Detects broken references and cycles; validates index
- Create
Formatterstruct inlash-core/src/formatter/- Fields:
-
config: LashConfig -
format_options: FormatOptions
-
- Methods:
-
format_file(&self, file: &TaskFile) -> String- Format to string -
format_file_in_place(&self, path: &Path) -> Result<()>- Write back
-
- Fields:
- Define
FormatOptions-
indent_spaces: u8- Default 2 -
sort_annotations: bool- Default true -
normalize_whitespace: bool- Default true -
apply_auto_fixes: bool- Default true (waiving, status consistency) -
preserve_blank_lines: bool- How many to keep (max 2)
-
- Normalize indentation
- Convert all task indentation to exactly 2 spaces per level
- Maintain correct depth hierarchy
- Preserve non-task content indentation (handled by parser validation)
- Sort annotations alphabetically
- Within header block, sort
@key:lines by key name - Keep
@idfirst always (special case) - Sort labels alphabetically for consistency
- Within header block, sort
- Normalize whitespace
- Trim trailing whitespace from all lines
- Ensure single blank line between sections
- Collapse multiple blank lines to max 2
- Ensure file ends with single newline
- Apply auto-fixes
- Auto-waive children when parent is waived
- Fix parent-child status consistency
- Controlled by
apply_auto_fixesoption
- Preserve non-task content
- Format based on parsed AST
- Preserve markdown formatting
- Only format task structure and annotations
- Ensure round-trip safety
- parse β format β parse is idempotent
- Content semantics preserved
- No data loss
- Add round-trip tests
- Write comprehensive tests
- Format messy file (whitespace)
- Sort annotations
- Apply auto-fixes (waiving, status consistency)
- Round-trip tests (format is idempotent)
- Preserve task content and order
- Minimal vs strict formatting modes
- Files with various issues
- 22+ tests written
Priority: MEDIUM Estimate: 2 days Dependencies: Tasks #2, #3 (need rules to auto-fix) Success Criteria: Can format files without data loss; idempotent formatting
- Implement
lash lintcommand inlash-cli- Command structure:
-
lash lint [PATH...]- Lint specific files or directories -
lash lint(no args) - Lint entire project from root
-
- Options:
-
--json- Output JSON diagnostics -
--fix- Apply auto-fixes -
--rule <code>- Run only specific rule(s) -
--severity <level>- Only show errors of this severity or higher -
--no-color- Disable colored output
-
- Exit codes:
- 0 - No errors
- 1 - General error (file not found, etc.)
- 2 - Lint errors found
- Command structure:
- Implement
lash formatcommand- Command structure:
-
lash format [PATH...]- Format specific files -
lash format(no args) - Format entire project
-
- Options:
-
--check- Check formatting without modifying (dry-run) -
--diff- Show diff of changes -
--no-fix- Only normalize formatting, don't apply lint fixes
-
- Exit codes:
- 0 - All files properly formatted (or successfully formatted)
- 1 - General error
- 2 - Files need formatting (with --check)
- Command structure:
- Implement progress reporting
- Show progress bar for multi-file operations
- "Linting file.md..." spinner
- Summary: "Checked 42 files, found 7 errors"
- Use
indicatiffor progress bars
- Format diagnostic output (human-readable)
- Format:
path/to/file.md:line:col: error[CODE]: message - Example:
tasks/api.md:42:3: error[E_SYNTAX_DEPTH]: Task depth exceeds maximum (3 > 2) - Color code by severity:
- Red for errors
- Yellow for warnings
- Blue for info
- Show code snippet with error location marked
- Show suggestion/fix if available
- Format:
- Format diagnostic output (JSON)
- Schema:
{ "diagnostics": [ { "code": "E_SYNTAX_DEPTH", "severity": "error", "message": "Task depth exceeds maximum", "location": { "file": "tasks/api.md", "line": 42, "column": 3, "span": { "start": 120, "end": 145 } }, "suggestion": "Split deep hierarchies into separate files", "fix": null } ], "summary": { "files_checked": 42, "errors": 7, "warnings": 3, "info": 1 } } - Stable field names for machine parsing
- Include all diagnostics, not just first few
- Schema:
- Implement file discovery
- If path is directory, find all
.mdfiles recursively - Respect
.gitignorepatterns - Respect
.lashignoreif present - Sort files for deterministic output
- If path is directory, find all
- Add
--fixmode implementation- Run linter to collect diagnostics
- Filter for diagnostics with auto-fix available
- Apply fixes to files
- Re-lint to verify fixes worked
- Report what was fixed
- Warn if any fixes failed to apply
- Write integration tests
- Run lint on valid file (exit 0, no output)
- Run lint on invalid file (exit 2, show errors)
- Run lint with --json (verify JSON schema)
- Run lint with --fix (verify file changed)
- Run format with --check (exit 2 if needs formatting)
- Run format without --check (modify file)
- Run on directory (multiple files)
- Run with various option combinations
- 14 unit tests for utilities
Priority: HIGH Estimate: 1 day Dependencies: Tasks #2, #3, #5 Success Criteria: Commands work correctly; output is clear; exit codes correct
8-10 days total for linter implementation
- All tasks above completed β
- 20+ linting rules implemented (syntax + semantic + cross-file) β (20 rules total)
- Auto-formatter works without data loss β (22 tests, round-trip safe)
- CLI commands functional with good UX β
(
lash lintandlash format) - 100+ unit tests for rules β (607 total tests across all modules)
- Integration tests for CLI β (14 CLI-specific tests)
- JSON output for machine parsing β (stable schema implemented)
Syntax Rules (7):
- Valid checkbox pattern
- Consistent indentation (2 spaces)
- Depth limit enforcement (max depth 2)
- Valid annotation syntax
- Unknown annotation keys (strict with config)
- Header structure
- Annotation ordering (optional style)
Semantic Rules (8):
- ID uniqueness within file
- Parent-child status consistency
- Auto-waive children (formatter rule)
- Valid label format
- Valid date format
- Valid estimate format
- Valid owner format
- Non-empty task titles
Cross-File Rules (5):
- Dependency references exist
- No circular dependencies
- Root index file references valid
- Orphaned files detection
- Valid path resolution
Total: 20 rules covering all aspects of file validity
LintRule (trait)
β
Concrete Rules (20+)
β
Linter (orchestrates rules)
β
Diagnostic (reports issues)
β
Fix (auto-fix suggestions)
β
Formatter (applies fixes)
After completing linter, proceed to:
- tasks.sqlite-schema.md - Store validated data in database
- Use linter in
lash indexto validate before indexing - Use formatter in pre-commit hooks