Module: Parsing & Validation Priority: CRITICAL Estimated Duration: 7-9 days Dependencies: tasks.core-data-model (all tasks)
Implement the Markdown parser that transforms raw .md files into the core data structures (TaskFile, Task, etc.). The parser must be robust, performant, and produce helpful error messages.
Design Approach: Use pulldown-cmark for Markdown parsing, then walk the event stream to build our task structures.
Performance Target: <100ms for typical files (for pre-commit hooks)
- Choose parsing library
- Add
pulldown-cmarkdependency tolash-core - Evaluate streaming vs tree-based parsing (choose streaming)
- Document decision in parser module
- Add
- Define parser module structure
- Create
lash-core/src/parser/mod.rs - Create
lash-core/src/parser/events.rs- event stream processing - Create
lash-core/src/parser/checkbox.rs- checkbox line parsing - Create
lash-core/src/parser/annotations.rs- annotation parsing - Create
lash-core/src/parser/builder.rs- task tree builder
- Create
- Define
ParseContextstruct- Fields:
-
file_path: PathBuf- Current file being parsed -
current_line: usize- Line number tracking -
current_section: Section- Header, Tasks, References, etc. -
errors: Vec<Diagnostic>- Accumulated errors -
config: &LashConfig- Parser configuration
-
- Methods for error reporting with location context
- Fields:
- Define parsing result types
-
ParseResult<T> = Result<T, Vec<Diagnostic>>- can have multiple errors -
ParsedFilestruct - intermediate representation beforeTaskFile
-
- Document parser architecture
- Create
lash-core/src/parser/README.md - Document parsing phases: events β lines β structure β validation
- Document error handling strategy
- Add architecture diagram (ASCII art)
- Create
Priority: CRITICAL Estimate: 1 day Dependencies: tasks.core-data-model#2 Success Criteria: Architecture documented; module structure created; ready to implement
- Parse checkbox pattern
- Create
parse_checkbox_line(line: &str, line_num: usize) -> Result<CheckboxLine> - Detect indentation (count leading spaces)
- Enforce indentation is multiple of 2 (per design decisions)
- Parse
- [STATUS]pattern - Extract status character (space, x, -, !)
- Handle both lowercase 'x' and uppercase 'X'
- Return error for invalid checkbox patterns
- Create
- Extract task title
- Get text after checkbox to end of line
- Trim leading/trailing whitespace
- Validate title not empty
- [-] Handle multiline titles (continuation lines) - not part of current spec
- Parse inline labels
- Scan title for
#wordpatterns - Extract all hashtag labels
- Use
Label::parse_inline_labels()from core-data-model - Keep labels in title or remove? (design decision: keep)
- Scan title for
- Parse trailing metadata blocks
- Detect
[@key: value, @key2: value2]pattern - Extract key-value pairs
- Validate bracket matching
- [-] Parse individual annotations within brackets - deferred to annotation parser (Task #3)
- [-] Merge with inline labels - deferred to annotation parser (Task #3)
- Detect
- Handle edge cases
- Mixed tabs and spaces (error: must be spaces only)
- [-] Inconsistent indentation jumps (error: can't skip levels) - validated in tree builder (Task #5)
- Empty titles
- Very long titles (>1000 chars warning) - handled, no artificial limit
- Invalid status characters
- Create intermediate representation
- Define
CheckboxLinestruct:-
indent: usize- number of spaces -
depth: u8- computed nesting level (indent / 2) -
status: TaskStatus -
title: String -
labels: Vec<Label> - [-]
metadata: Option<TaskMetadata>- not in current implementation, deferred -
line_num: usize
-
- Define
- Write comprehensive tests
- Valid patterns:
- [ ],- [x],- [-],- [!] - Uppercase X:
- [X] - With labels:
- [ ] Task #label1 #label2 - With metadata:
- [ ] Task [@owner: alice] - Indentation levels: 0, 2, 4 spaces
- Error cases: invalid indent, bad checkbox, empty title
- Edge cases: mixed whitespace, weird characters
- 50+ test cases total (56 tests implemented)
- Valid patterns:
Priority: CRITICAL Estimate: 2 days Dependencies: Task #1 Success Criteria: Can parse all valid checkbox variations; reports errors clearly with line numbers
- Parse
@key: valueformat- Create
parse_annotation(line: &str) -> Result<(String, String)> - Detect
@at start (after trimming) - Split on first
:to get key and value - Trim whitespace from both key and value
- Validate key format (alphanumeric + hyphen + underscore)
- Create
- Handle multiline values
- Detect when value continues on next line (indented)
- Accumulate continuation lines
- Preserve internal newlines/formatting
- Trim trailing whitespace only
- Parse specific annotation types
-
@id: string- validate ID format -
@labels: a, b, c- split on commas, parse as Label list -
@owner: string- any string -
@created: YYYY-MM-DD- validate date format -
@estimate: duration- validate format (e.g., "2h", "3d") -
@depends-on: ref- parse as DependencyRef -
@agent-note: text- any text
-
- Validate known annotation keys
- Load custom keys from config
- Check against built-in keys: id, labels, status, owner, created, estimate, depends-on, agent-note
- Check against custom keys from
.lash/config.toml - Error on unknown key not in either list
- Suggest adding to config if looks like custom key
- Parse value types
- String values (default)
- Comma-separated lists (for labels, depends-on)
- Date values (YYYY-MM-DD format)
- Duration values (regex:
\d+[hdwmy]for hours/days/weeks/months/years) - Return structured errors for invalid formats
- Handle multiple
@depends-onannotations- Allow multiple
@depends-onlines - Accumulate into Vec
- Each can be separate file or task reference
- Allow multiple
- Create annotation collection
- Define
AnnotationBlockstruct:-
annotations: HashMap<String, Vec<String>>- allow multiple values per key - Helper methods:
get_single(),get_list(),get_date(), etc.
-
- Validate no duplicate single-value annotations
- Allow multiple values for
depends-on,labels
- Define
- Write tests
- Parse all built-in annotation types
- Parse custom annotations (with config)
- Multiline values
- Multiple depends-on
- Invalid key names
- Invalid value formats (dates, durations)
- Unknown annotations (error)
- 30+ test cases (51 tests implemented)
Priority: CRITICAL Estimate: 1.5 days Dependencies: Task #2 Success Criteria: Can parse all annotation types; validates keys against config; handles multiline values
- Parse H1 title
- Use pulldown-cmark to find first
Heading(1)event - Extract text content
- Store as file title
- Warning if no H1 found (synthesize from filename)
- Warning if multiple H1s found (use first)
- Use pulldown-cmark to find first
- Extract header annotations
- Parse lines between H1 and first H2
- Identify annotation lines (start with
@) - Skip blank lines
- Collect overview text (non-annotation, non-blank lines)
- Parse all annotations into
AnnotationBlock
- Parse overview section
- Collect all text between annotations and "## Tasks"
- Preserve paragraph structure
- Trim excessive whitespace
- Store as optional overview text
- Detect "## Tasks" section boundary
- Find
Heading(2)with text "Tasks" - Case-insensitive comparison
- Set parser state to TasksSection
- Warning if no Tasks section found
- Find
- Parse optional "## References" section
- Find
Heading(2)with text "References" - Collect content (markdown list, prose, etc.)
- Store as optional references text
- [-] Parse depends-on links from references if present (deferred to Task #6)
- Find
- Handle malformed headers
- Missing H1: synthesize from filename
- Missing Tasks section: treat whole file as tasks
- Missing annotations: use defaults
- Graceful degradation, emit warnings
- Create header representation
- Define
ParsedHeaderstruct:-
title: String -
annotations: AnnotationBlock -
overview: Option<String>
-
- [-] Convert to
FileMetadatafrom annotations (deferred to Task #6)
- Define
- Write tests
- Complete valid header
- Minimal header (H1 + Tasks only)
- With overview text
- With references
- Multiple H1s (warning)
- No H1 (synthesize)
- No Tasks section (warning, treat all as tasks)
- 24 test cases (exceeds 20+ requirement)
Priority: HIGH Estimate: 1 day Dependencies: Task #3 Success Criteria: Can parse complete header block; handles missing sections gracefully
- Build parent-child relationships from indentation
- Create
TaskTreeBuilderstruct - Maintain stack of current parents at each depth
- For each checkbox line:
- Compute depth from indentation (indent / 2)
- Validate depth doesn't exceed max (3 levels = depth 0,1,2)
- Validate depth doesn't jump (can't go from 0 to 2)
- Pop stack to current depth
- Set parent as top of stack
- Push current task onto stack
- Create
- Validate depth limits during construction
- Check
depth <= config.max_depth(max_depth = 2 for 3 levels) - Emit error with line number if exceeded
- [-] Suggest splitting into separate file
- Check
- Handle malformed indentation
- Detect inconsistent depth jumps (0 β 2 without 1)
- Emit error: "Cannot skip indentation levels"
- Provide suggestion: indent by 2 spaces
- [-] Attempt recovery: treat as sibling of last task
- Compute task order indices
- Assign sequential order_index to siblings
- Reset count for each parent
- Maintain document order for traversal
- Generate synthetic IDs for tasks without
@id- Format:
task-{order}or{title-slug} - Use task title slug if unique (lowercase, hyphenated)
- Fall back to numeric index if title slugs collide
- Ensure IDs unique within file
- Format:
- Validate ID uniqueness within file
- Track all task IDs in HashSet
- Error on duplicate IDs
- Provide line numbers for both occurrences
- [-] Suggest renaming one (error message is clear enough)
- Build task hierarchy
- Create
Taskinstances fromCheckboxLinedata - Set parent_id based on indentation stack
- Set depth, order_index
- Collect all tasks into
TaskTree
- Create
- Apply auto-waiving logic
- If parent status is Waived, mark all children as Waived
- Traverse depth-first after tree construction
- [-] Emit info diagnostic: "Auto-waived due to parent" (applied automatically, no diagnostic needed)
- Write comprehensive tests
- Simple flat list (all depth 0)
- Two-level hierarchy (parent + children)
- Three-level hierarchy (max depth)
- Sibling tasks at same depth
- Multiple parent-child chains
- Depth limit exceeded (error)
- Skipped indentation level (error)
- Duplicate IDs (error)
- Synthetic ID generation
- Auto-waiving propagation
- 40+ test cases including edge cases (42 tests implemented)
Priority: CRITICAL Estimate: 2 days Dependencies: Task #2 Success Criteria: Builds correct hierarchical structure; validates constraints; generates IDs
- Create main
parse_file()entry point- Signature:
pub fn parse_file(path: &Path, config: &LashConfig) -> ParseResult<TaskFile> - Read file content
- Create ParseContext
- Parse in phases: header β tasks β finalize
- Collect all errors
- Return TaskFile or aggregated errors
- Signature:
- Integrate all parsing phases
- Phase 1: Parse header (H1, annotations, overview)
- Phase 2: Parse task list (checkboxes β tree)
- Phase 3: Parse references section
- Phase 4: Validate and build TaskFile
- Compute content hash
- Use blake3 to hash file content
- Store in TaskFile.hash
- Used for incremental indexing (detect changes)
- Extract file metadata
- Get mtime from filesystem
- Synthesize file ID from path if no @id
- Build FileMetadata from header annotations
- Validate metadata consistency
- Implement error collection
- Continue parsing after errors when possible
- Collect all diagnostics in ParseContext
- Sort diagnostics by line number
- Return complete error list (don't stop at first error)
- [-] Add performance optimization
- [-] Use arena allocation for temporary parsing structures
- [-] Add
typed-arenadependency - [-] Allocate CheckboxLine instances in arena during parse
- [-] Convert to owned Task instances at end
- Target: <100ms for typical files
- Create convenience methods
-
parse_file_from_string(content: &str, config: &LashConfig) -> ParseResult<TaskFile> - [-]
parse_task_section(content: &str) -> ParseResult<Vec<Task>>- for testing - [-]
validate_file(file: &TaskFile, config: &LashConfig) -> Vec<Diagnostic>- post-parse validation
-
- Write integration tests
- Complete valid file (all sections)
- Minimal valid file (H1 + tasks)
- File with multiple errors (collect all)
- Large file (100+ tasks) - performance test
- [-] Files from fixtures/valid/ directory
- [-] Files from fixtures/invalid/ directory (expect errors)
- Round-trip: parse β serialize β parse (preserve structure)
- 20+ integration tests
- Benchmark performance
- Create benchmark suite using
criterion - Benchmark small file (10 tasks)
- Benchmark medium file (100 tasks)
- Benchmark large file (1000 tasks)
- Target: <100ms for small, <500ms for medium, <5s for large
- [-] Profile hot paths if needed
- Create benchmark suite using
Priority: CRITICAL Estimate: 1 day Dependencies: Tasks #4, #5 Success Criteria: Can parse complete files; reports all errors; meets performance targets
7-9 days total for Markdown parser implementation
- All tasks above completed (Tasks #1-6 complete, all commits pushed)
- Parser handles all valid Lash Markdown formats (validated through 200+ tests)
- Comprehensive error reporting with line numbers (implemented in ParseContext)
- Performance targets met (<100ms for typical files) - Achieved: 67.7Β΅s for realistic file
- 100+ unit tests covering edge cases (180 parser unit tests + 20+ integration tests)
- [-] Integration tests with fixture files (waived: comprehensive integration tests use inline content)
- Benchmarks established (criterion benchmarks show 67.7Β΅s for realistic workload)
File Content
β
pulldown-cmark (Markdown events)
β
Header Parser β ParsedHeader
β
Checkbox Parser β Vec<CheckboxLine>
β
Tree Builder β TaskTree
β
File Builder β TaskFile
- Continue parsing after errors when possible
- Collect all errors, sort by line number
- Provide actionable error messages with context
- Include suggestions for fixes
- Support
--jsonoutput for tooling
| File Size | Task Count | Target Time |
|---|---|---|
| Small | 10 tasks | <100ms |
| Medium | 100 tasks | <500ms |
| Large | 1000 tasks | <5s |
- 80%+ code coverage
- Edge cases for all parsing rules
- Round-trip tests (parse β format β parse)
- Performance regression tests
After completing Markdown parser, proceed to:
- tasks.linter.md - Build on parser to validate semantics
- tasks.sqlite-schema.md - Store parsed structures in database