This guide provides everything you need to understand, build, and contribute to Lash.
- Architecture Overview
- Development Setup
- Code Organization
- Core Components
- Database Schema
- Error Handling
- Testing Strategy
- Contributing
- Release Process
Lash is built as a modular Rust workspace with clear separation of concerns. The architecture follows these key principles:
- Markdown as Source of Truth: All task data lives in Markdown files
- SQLite as Acceleration Layer: Database is fully reconstructible from Markdown
- Layered Design: Each crate has a single, well-defined responsibility
- Interface-Based: Components program to interfaces, not implementations
lash/
βββ crates/
β βββ lash-types/ # Shared types, errors, config
β βββ lash-core/ # Markdown parsing & validation
β βββ lash-db/ # SQLite indexing & queries
β βββ lash-agent/ # Agent integration & prompt generation
β βββ lash-tui/ # Terminal UI
β βββ lash-cli/ # CLI binary
βββ docs/ # Design docs & error codes
βββ tasks/ # Development task tracking
Purpose: Foundation types shared across all crates
Key Components:
Task,TaskFile- Core data structuresLashError- Unified error taxonomy with 25+ error codesLashConfig- Project and user configurationStatus,Label,Dependency- Domain typesTaskCreationBuilder- Builder pattern for task creationErrorFormatter- Rich error output with miette integration
Dependencies: None (foundational crate)
When to use: Define new types that need to be shared across multiple crates
Purpose: Markdown parsing, linting, validation, and dependency resolution
Key Components:
Parser- Markdown parsing using pulldown-cmarkLinter- 20+ validation rules with auto-fix capabilitiesFormatter- Markdown normalization and pretty-printingDependencyGraph- Task dependency resolution with cycle detectionContextualNotesParser- Plain bullet note parsingFuzzyMatcher- String similarity matching for search
Dependencies: lash-types
Performance:
- File parsing: ~67.7Β΅s (benchmark target met)
- Linting: >500 tasks/sec
When to use: Any Markdown format changes, new lint rules, parser features
Purpose: SQLite indexing, querying, and incremental updates
Key Components:
Indexer- Fast indexing engine with profiling supportSearchEngine- Full-text search with FTS5DependencyUpdater- Incremental dependency graph updatesGraphBuilder- Dependency closure computationVerifier- Database consistency checkingProjectRoot- Project root detection
Dependencies: lash-types, lash-core
Performance:
- Small projects (10 files, ~50 tasks): 10.5ms (target: <50ms) β
- Medium projects (100 files, ~500 tasks): 61ms (target: <500ms) β
- Large projects (1000 files, ~5000 tasks): 425ms (target: <5s) β
When to use: Database schema changes, query optimization, indexing improvements
Purpose: LLM agent integration and token minimization
Key Components:
PromptGenerator- Token-minimized context generationSchemaGenerator- JSON schema for agent toolingTokenCounter- Token estimation utilitiesContextBuilder- Sparse context for specific agent actions
Dependencies: lash-types, lash-core, lash-db
When to use: New agent features, prompt templates, token optimization
Purpose: Terminal user interface
Key Components:
App- Main TUI application state machineEventHandler- Keyboard and mouse event processingThemeManager- 300+ Gogh color scheme supportTaskCreator- Interactive task creation UITerminal- Crossterm/ratatui integration
Dependencies: lash-types, lash-core, lash-db
When to use: New TUI features, UI improvements, theme additions
Purpose: Command-line interface and user-facing binary
Key Components:
Cli- Clap-based CLI parserCommandExecutor- Command dispatch and executionErrorReporter- Rich terminal error outputTreeFormatter- Hierarchical task tree renderingProgressIndicator- Operation progress UIConfigManager- Configuration file handlingCliTheme- Centralized colored output
Dependencies: All crates
When to use: New CLI commands, output formatting, user experience
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β User Interaction β
βββββββββββββββββββ¬ββββββββββββββββββββββββββββββββββββββββββββ€
β CLI Commands β TUI β
β (lash-cli) β (lash-tui) β
ββββββββββ¬βββββββββ΄ββββββββββββββββ¬βββββββββββββββββββββββββββ
β β
βββββββββββββββββββββββββββ
β
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Command Processing β
β (lash-cli) β
βββββββββββ¬βββββββββββββββββββββββββββββββ¬βββββββββββββββββββββ
β β
βΌ βΌ
ββββββββββββββββββββββββ ββββββββββββββββββββββββ
β Markdown Layer β β Database Layer β
β (lash-core) βββββββΊβ (lash-db) β
ββββββββββββββββββββββββ ββββββββββββββββββββββββ
β β
β β
ββββββββββββββββ¬ββββββββββββββββ€
β β β
βΌ βΌ βΌ
βββββββββββββββ βββββββββββββββ βββββββββββββββ
β Parser β β Linter β β Indexer β
βββββββββββββββ βββββββββββββββ βββββββββββββββ
Typical Operation Flow:
- Parse: User runs
lash list --label backend - CLI Layer:
lash-cliparses arguments, validates project root - Database Query:
lash-dbqueries SQLite for matching tasks - Markdown Read: If needed,
lash-coreparses files for details - Format Output:
lash-cliformats results for terminal display - Display: Rendered output shown to user
Clear boundaries between crates prevent coupling:
- Types flow downward:
lash-typesβlash-coreβlash-db - No circular dependencies: Enforced by Cargo
- Interface segregation: Each crate exposes minimal public API
- Data ownership: Markdown owns state, SQLite caches it
- Rust: Version 1.75+ (see
rust-toolchain.toml) - Git: For version control
- SQLite: Bundled via rusqlite (no external install needed)
- Optional:
cargo-llvm-covfor coverage reports
# Via rustup (recommended)
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
# Verify installation
rustc --version
cargo --version# Clone repository
git clone https://github.com/fixture-dev/lash.git
cd lash
# Build all crates
cargo build --workspace
# Build in release mode (optimized)
cargo build --workspace --release
# Install locally
cargo install --path crates/lash-cliSee TESTING.md for comprehensive testing documentation.
Quick start:
# Run all tests
cargo test --workspace
# Run specific crate tests
cargo test -p lash-core
# Run with output
cargo test --workspace -- --nocapture
# Run doc tests
cargo test --docLash includes comprehensive benchmarks for performance-critical code:
# Run all benchmarks
cargo bench --workspace
# Run specific benchmark suite
cargo bench -p lash-db --bench indexing
cargo bench -p lash-core --bench parser_bench
# Quick benchmarks (faster, less accurate)
cargo bench -- --quick
# View HTML reports
open target/criterion/report/index.htmlBenchmark suites:
parser_bench- Markdown parsing performancegraph_bench- Dependency graph operationslinter_bench- Linting speedindexing- Database indexing throughputsearch_bench- Full-text search latencynotes_parser_bench- Contextual notes parsingnotes_indexing_bench- Notes indexing performance
# Format code
cargo fmt --all
# Run clippy (strict mode)
cargo clippy --workspace --all-targets -- -D warnings
# Check format without modifying
cargo fmt --check
# Run all pre-commit checks
./scripts/pre-commitAutomate quality checks before every commit:
./scripts/install-pre-commit-hook.shThe hook runs:
cargo fmt --check- Code formattingcargo clippy- Lint checkscargo test --workspace --lib- Unit testscargo test --doc- Doc tests
To bypass (not recommended): git commit --no-verify
Recommended tools:
# Coverage reporting
cargo install cargo-llvm-cov
# Generate coverage report
cargo llvm-cov --workspace --html
open target/llvm-cov/html/index.html
# Benchmarking (included with criterion)
cargo bench
# Watch mode (auto-rebuild on changes)
cargo install cargo-watch
cargo watch -x test
# Dependency graph visualization
cargo install cargo-depgraph
cargo depgraph | dot -Tpng > deps.pnglash/
βββ .github/
β βββ workflows/
β βββ ci.yml # CI/CD pipeline
βββ crates/
β βββ lash-types/
β β βββ src/
β β β βββ lib.rs # Public API
β β β βββ error.rs # Error types
β β β βββ task.rs # Task model
β β β βββ file.rs # TaskFile model
β β β βββ config.rs # Configuration
β β β βββ ...
β β βββ Cargo.toml
β βββ lash-core/
β β βββ src/
β β β βββ lib.rs # Parser & linter
β β β βββ parser/ # Markdown parser
β β β βββ linter/ # Validation rules
β β β βββ formatter/ # Code formatting
β β β βββ ...
β β βββ benches/ # Benchmarks
β β βββ Cargo.toml
β βββ lash-db/
β β βββ src/
β β β βββ lib.rs # Database API
β β β βββ indexer.rs # Indexing engine
β β β βββ search.rs # Search queries
β β β βββ migrations.rs # Schema migrations
β β β βββ ...
β β βββ Cargo.toml
β βββ lash-agent/
β β βββ src/
β β βββ lib.rs # Agent API
β β βββ prompt.rs # Prompt generation
β β βββ ...
β βββ lash-tui/
β β βββ src/
β β βββ lib.rs # TUI library
β β βββ app.rs # Application state
β β βββ state.rs # UI state machine
β β βββ ...
β βββ lash-cli/
β βββ src/
β β βββ main.rs # Binary entry point
β β βββ cli.rs # CLI argument parsing
β β βββ command.rs # Command implementations
β β βββ ...
β βββ tests/
β β βββ common/ # Test utilities
β β βββ fixtures/ # Test data
β β βββ e2e_cli_tests.rs # End-to-end tests
β βββ Cargo.toml
βββ docs/
β βββ design-doc.md # Technical specification
β βββ error-codes.md # Error catalog
β βββ TESTING.md # Testing guide
β βββ developer-guide.md # This file
βββ tasks/ # Development tracking
β βββ tasks.md # Task index
β βββ tasks.*.md # Feature-specific tasks
βββ scripts/
β βββ install-pre-commit-hook.sh # Hook installer
β βββ pre-commit # Pre-commit checks
βββ Cargo.toml # Workspace manifest
Files:
lib.rs- Crate root, public APImod.rs- Module index (avoid when possible, prefer named files){feature}.rs- Single-responsibility modules- Test files match source:
parser.rsβparser_tests.rsor#[cfg(test)] mod tests
Functions:
snake_casefor functions and methodsnew()for constructorstry_*()or*_checked()for fallible operationsto_*()for conversions (consumes self)as_*()for cheap referencesinto_*()for consuming conversions
Types:
PascalCasefor structs, enums, traitsSCREAMING_SNAKE_CASEfor constants- Prefer descriptive names:
TaskFileoverTF - Error types:
*Errorsuffix - Result types:
Result<T, Error>or type aliastype Result<T> = std::result::Result<T, Error>
Modules:
- One concept per module
- Public items explicitly marked
pub - Re-export commonly used types in
lib.rs
Public API Surface:
// lash-types/src/lib.rs - Everything needed by consumers
pub use error::{LashError, LashResult};
pub use task::{Task, TaskStatus};
pub use file::TaskFile;
pub use config::LashConfig;
// Internal-only modules
mod internal_utils; // Not re-exportedDependency rules:
- Never import from
lash-cli(it's the integration layer) lash-typesimports nothing internal (foundation)lash-corecan importlash-typesonlylash-dbcan importlash-types,lash-corelash-agentcan importlash-types,lash-core,lash-dblash-tuican importlash-types,lash-core,lash-db
Location: crates/lash-core/src/parser/
Purpose: Parse Lash Markdown files into structured TaskFile objects
Key Files:
parser.rs- Main parsing logicannotations.rs- Metadata parsing (@id,@labels, etc.)notes.rs- Contextual notes parsing
Example Usage:
use lash_core::parser::parse_file;
use lash_types::LashConfig;
use std::path::PathBuf;
let path = PathBuf::from("tasks.md");
let config = LashConfig::default();
let task_file = parse_file(&path, &config)?;
println!("Found {} tasks", task_file.tasks.len());Key Concepts:
-
Two-pass parsing:
- First pass: Extract metadata block
- Second pass: Parse task tree
-
Contextual notes: Plain bullets (
- Text) vs checkboxes (- [ ] Task)- Notes provide context without completion tracking
- Indexed in database for search
- Cannot have children (linter enforced)
-
Annotations: Structured metadata
@id: unique.identifier @labels: backend, api @owner: alice @estimate: 2h
Performance: ~67.7Β΅s per file (benchmark)
Location: crates/lash-core/src/linter/
Purpose: Validate Markdown format and enforce rules
Validation Rules (20+ total):
E_LINT_MISSING_ID- Files must have@idE_LINT_DUPLICATE_ID- IDs must be uniqueE_LINT_DEPTH_EXCEEDED- Respect max nesting (default: 3)E_LINT_INVALID_STATUS- Status values must be validE_LINT_UNKNOWN_ANNOTATION- No unknown annotationsE_LINT_BAD_INDENTATION- Consistent indentation (2 spaces)E_LINT_NOTE_HAS_CHILDREN- Notes cannot have sub-itemsE_LINT_DESCRIPTION_TOO_LONG- Description length limits
Auto-fix capabilities:
- Normalize indentation
- Sort annotations
- Fix spacing around headings
- Add missing header boilerplate
Example Usage:
use lash_core::linter::{lint_file, LintOptions};
use lash_types::LashConfig;
let options = LintOptions {
fix: true,
interactive: false,
};
let config = LashConfig::default();
let diagnostics = lint_file(&path, &config, &options)?;
for diag in diagnostics.errors {
println!("Error: {} at line {}", diag.message, diag.line);
}Location: crates/lash-core/src/formatter/
Purpose: Normalize Markdown formatting for consistency
Features:
- Consistent indentation (configurable, default 2 spaces)
- Annotation ordering (alphabetical)
- Heading spacing
- Task tree structure preservation
- Diff mode (
--diff) shows changes without applying
Example:
# Format file in place
lash format tasks.md
# Show diff without applying
lash format tasks.md --diff
# Check if formatting needed
lash format tasks.md --checkLocation: crates/lash-core/src/graph/
Purpose: Build and analyze task dependency graphs
Dependency Types:
-
Implicit hierarchy: Parent tasks depend on children
- [ ] Parent - [ ] Child A - [ ] Child B
-
Explicit cross-file: Via
@depends-on@depends-on: path/to/file.md#task:id
-
Directory-level: File dependencies on subdirectories
Key Operations:
build_graph()- Construct dependency graphdetect_cycles()- Find circular dependenciescompute_closure()- Transitive dependency closuretopological_sort()- Valid execution order
Example:
use lash_core::graph::DependencyGraph;
let graph = DependencyGraph::from_files(&files)?;
// Check for cycles
if let Some(cycle) = graph.detect_cycles() {
eprintln!("Cycle detected: {:?}", cycle);
}
// Get all dependencies of a task
let deps = graph.get_all_dependencies("task:foo");Location: crates/lash-db/src/indexer.rs
Purpose: Fast incremental indexing of task files into SQLite
Features:
- Incremental: Only re-parse changed files (via hash comparison)
- Parallel: Multi-threaded parsing with rayon
- Profiled: Built-in performance profiling
- Transactional: Atomic database updates
Indexing Pipeline:
1. Discovery - Walk filesystem, enumerate .md files
2. Diff - Compare hashes, identify changed files
3. Parse - Parse changed files in parallel
4. Database - Insert/update SQLite rows
5. Closure - Rebuild dependency closure table
Performance Profiling:
use lash_db::{Indexer, IndexerConfig};
let config = IndexerConfig::new(root)
.with_profiling(true);
let mut indexer = Indexer::new(&conn, config, &lash_config);
let report = indexer.index_project()?;
// Print performance breakdown
if let Some(profile) = report.profile {
profile.print_summary();
// Outputs:
// Discovery: 5.2ms
// Diff: 1.3ms
// Parsing: 15.8ms (10 files, 1.58ms avg)
// Database: 8.1ms (150 rows inserted)
// Closure rebuild: 3.2ms
// Total: 33.6ms
}Performance Targets (all exceeded 8-12x):
- Small (10 files): <50ms (achieved: 10.5ms)
- Medium (100 files): <500ms (achieved: 61ms)
- Large (1000 files): <5s (achieved: 425ms)
Location: crates/lash-db/src/search.rs, crates/lash-db/src/lib.rs
Purpose: Fast querying of indexed tasks
Query Types:
-
Filtered list: Query by label, status, owner, path
let tasks = conn.query_tasks(QueryOptions { labels: Some(vec!["backend".into()]), status: Some(TaskStatus::Open), limit: Some(50), ..Default::default() })?;
-
Full-text search: FTS5-powered fuzzy search
let results = search_tasks(&conn, "authentication", 20)?; for result in results { println!("{}: {}", result.rank, result.task.title); }
-
Dependency queries: Get dependencies/dependents
let deps = get_task_dependencies(&conn, task_id)?; let rdeps = get_task_dependents(&conn, task_id)?;
Search Features:
- FTS5 full-text indexing
- Rank-based result ordering
- Search across: titles, bodies, notes, descriptions, file paths
- Configurable result limits
Location: crates/lash-cli/src/
Purpose: User-facing command-line interface
Key Components:
- Argument parsing (
cli.rs): Clap-based CLI definition - Command dispatch (
command.rs): Route commands to handlers - Error reporting (
error_reporter.rs): Rich terminal errors - Configuration (
config.rs): Config file management - Logging (
logging.rs): Tracing integration - Progress (
progress.rs): Indicatif progress bars - Theme (
theme.rs): Centralized colored output
Adding a new command:
// 1. Add to CLI definition (cli.rs)
#[derive(Subcommand)]
enum Command {
#[command(about = "My new command")]
MyCommand {
#[arg(short, long)]
option: String,
},
}
// 2. Add handler (command.rs)
impl Command {
pub fn execute(&self, ctx: &Context) -> Result<()> {
match self {
Command::MyCommand { option } => {
execute_my_command(ctx, option)
}
}
}
}
// 3. Implement logic
fn execute_my_command(ctx: &Context, option: &str) -> Result<()> {
// Your implementation
Ok(())
}Location: crates/lash-tui/src/
Purpose: Interactive terminal UI with ratatui
Architecture:
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β TUI App β
ββββββββββββββββββββ¬βββββββββββββββββββββββββββββββββββββββββββ€
β Event Loop β State Machine β
β (Keyboard/ β (Navigation, β
β Mouse Input) β Selection) β
ββββββββββ¬ββββββββββ΄ββββββββββββββββββ¬βββββββββββββββββββββββββ
β β
βΌ βΌ
Event Handler Render Pipeline
β β
βββββββββββββ¬ββββββββββββββββ
βΌ
Terminal Output
(crossterm/ratatui)
Key Features:
- 300+ color schemes (Gogh collection)
- Interactive task creation
- Keyboard-driven navigation
- Fuzzy search panel
- Dependency graph view
State management (state.rs):
- Separation of UI state from data state
- State transitions via actions
- Immutable state updates
Location: crates/lash-agent/src/
Purpose: Generate token-minimized prompts for LLM agents
Key Features:
- Schema generation: JSON schema for agent tooling
- Prompt templates: Pre-built prompts for common operations
- Context minimization: Sparse, relevant-only context
- Token counting: Estimate prompt token usage
- Format variations: Plain text, JSON, Claude Code skills, Agents.md
Example:
use lash_agent::PromptGenerator;
let generator = PromptGenerator::new(&conn, &config);
// Generate minimal agent prompt
let prompt = generator.generate_prompt(PromptOptions {
format: PromptFormat::Plain,
label_filter: Some("backend"),
include_examples: false,
max_tokens: Some(4000),
})?;
println!("{}", prompt);Token optimization strategies:
- ID-based references instead of full descriptions
- Summarized file headers
- Only include changed/relevant files
- Schema-first approach (minimal examples)
SQLite database (<project>/.lash/index.db) stores parsed task data for fast queries. Schema is fully versioned and migrated.
Stores task file metadata.
CREATE TABLE files (
id INTEGER PRIMARY KEY AUTOINCREMENT,
path TEXT NOT NULL UNIQUE, -- Relative path from project root
hash TEXT NOT NULL, -- BLAKE3 content hash
mtime INTEGER NOT NULL, -- Last modified timestamp
file_id TEXT, -- @id annotation
title TEXT, -- File title (heading)
description TEXT, -- ## Description section
owner TEXT, -- @owner annotation
created TEXT, -- @created annotation (ISO 8601)
estimate TEXT, -- @estimate annotation
agent_note TEXT, -- @agent-note annotation
indexed_at INTEGER NOT NULL -- When indexed
);
CREATE INDEX idx_files_path ON files(path);
CREATE INDEX idx_files_hash ON files(hash);
CREATE INDEX idx_files_status ON files(status);Stores individual tasks within files.
CREATE TABLE tasks (
id INTEGER PRIMARY KEY AUTOINCREMENT,
file_id INTEGER NOT NULL, -- FK to files.id
task_id TEXT, -- @id annotation (unique within file)
full_id TEXT NOT NULL UNIQUE, -- path#task:id (globally unique)
title TEXT NOT NULL, -- Task title
status TEXT NOT NULL, -- open, done, waived, blocked
depth INTEGER NOT NULL, -- Nesting depth (0 = top-level)
parent_id INTEGER, -- FK to tasks.id (NULL for top-level)
order_index INTEGER NOT NULL, -- Position within parent
owner TEXT, -- @owner annotation
estimate TEXT, -- @estimate annotation
body TEXT, -- Additional task description
line_number INTEGER, -- Line in source file
FOREIGN KEY (file_id) REFERENCES files(id) ON DELETE CASCADE,
FOREIGN KEY (parent_id) REFERENCES tasks.id ON DELETE CASCADE
);
CREATE INDEX idx_tasks_file_id ON tasks(file_id);
CREATE INDEX idx_tasks_status ON tasks(status);
CREATE INDEX idx_tasks_parent_id ON tasks(parent_id);
CREATE INDEX idx_tasks_full_id ON tasks(full_id);Stores plain bullet points nested under tasks.
CREATE TABLE contextual_notes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
task_id INTEGER NOT NULL, -- FK to tasks.id
content TEXT NOT NULL, -- Note text
order_index INTEGER NOT NULL, -- Position within task
FOREIGN KEY (task_id) REFERENCES tasks(id) ON DELETE CASCADE
);
CREATE INDEX idx_notes_task_id ON contextual_notes(task_id);Normalized label storage.
CREATE TABLE labels (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE -- Label name (e.g., "backend")
);
CREATE INDEX idx_labels_name ON labels(name);Many-to-many: tasks to labels.
CREATE TABLE task_labels (
task_id INTEGER NOT NULL,
label_id INTEGER NOT NULL,
PRIMARY KEY (task_id, label_id),
FOREIGN KEY (task_id) REFERENCES tasks(id) ON DELETE CASCADE,
FOREIGN KEY (label_id) REFERENCES labels(id) ON DELETE CASCADE
);
CREATE INDEX idx_task_labels_label_id ON task_labels(label_id);Many-to-many: files to labels.
CREATE TABLE file_labels (
file_id INTEGER NOT NULL,
label_id INTEGER NOT NULL,
PRIMARY KEY (file_id, label_id),
FOREIGN KEY (file_id) REFERENCES files(id) ON DELETE CASCADE,
FOREIGN KEY (label_id) REFERENCES labels(id) ON DELETE CASCADE
);
CREATE INDEX idx_file_labels_label_id ON file_labels(label_id);Tracks task dependencies.
CREATE TABLE dependencies (
id INTEGER PRIMARY KEY AUTOINCREMENT,
from_task_id INTEGER NOT NULL, -- Dependent task
to_task_id INTEGER NOT NULL, -- Dependency target
kind TEXT NOT NULL, -- 'parent-child', 'explicit', 'file'
FOREIGN KEY (from_task_id) REFERENCES tasks(id) ON DELETE CASCADE,
FOREIGN KEY (to_task_id) REFERENCES tasks(id) ON DELETE CASCADE
);
CREATE INDEX idx_deps_from ON dependencies(from_task_id);
CREATE INDEX idx_deps_to ON dependencies(to_task_id);
CREATE INDEX idx_deps_kind ON dependencies(kind);Transitive closure of dependencies (for fast "all dependencies" queries).
CREATE TABLE dependency_closure (
ancestor_id INTEGER NOT NULL, -- Transitively depends on...
descendant_id INTEGER NOT NULL, -- ...this task
depth INTEGER NOT NULL, -- Path length
PRIMARY KEY (ancestor_id, descendant_id),
FOREIGN KEY (ancestor_id) REFERENCES tasks(id) ON DELETE CASCADE,
FOREIGN KEY (descendant_id) REFERENCES tasks(id) ON DELETE CASCADE
);
CREATE INDEX idx_closure_ancestor ON dependency_closure(ancestor_id);
CREATE INDEX idx_closure_descendant ON dependency_closure(descendant_id);FTS5 virtual table for fast text search.
CREATE VIRTUAL TABLE tasks_fts USING fts5(
full_id UNINDEXED, -- Task identifier (not searchable)
title, -- Task title (searchable)
body, -- Task body (searchable)
file_path, -- File path (searchable)
notes, -- Contextual notes (searchable)
description, -- File description (searchable)
content='' -- External content table
);
-- Triggers to keep FTS in sync
CREATE TRIGGER tasks_fts_insert AFTER INSERT ON tasks ...
CREATE TRIGGER tasks_fts_update AFTER UPDATE ON tasks ...
CREATE TRIGGER tasks_fts_delete AFTER DELETE ON tasks ...files (1) ββββββββ (many) tasks
β β
β βββ (many) contextual_notes
β β
β βββ (many) task_labels ββ labels
β
βββ (many) file_labels ββ labels
tasks (many) βββββ (many) dependencies
β β
β βββ kind: 'parent-child' | 'explicit' | 'file'
β
βββ (recursive) parent_id β tasks.id
dependency_closure (computed from dependencies)
ancestor_id β tasks.id
descendant_id β tasks.id
Location: crates/lash-db/src/migrations.rs
Version tracking:
CREATE TABLE schema_version (
version INTEGER PRIMARY KEY,
applied_at INTEGER NOT NULL
);Migration process:
- Check current version
- Apply pending migrations in order
- Update version table
- Rebuild indexes and closure
Adding a migration:
// migrations.rs
pub fn run_migrations(conn: &Connection) -> Result<()> {
let version = get_schema_version(conn)?;
if version < 1 {
migrate_v1(conn)?;
}
if version < 2 {
migrate_v2(conn)?; // Your new migration
}
Ok(())
}
fn migrate_v2(conn: &Connection) -> Result<()> {
conn.execute("ALTER TABLE tasks ADD COLUMN new_field TEXT", [])?;
set_schema_version(conn, 2)?;
Ok(())
}Get all tasks in a file:
SELECT * FROM tasks
WHERE file_id = (SELECT id FROM files WHERE path = ?)
ORDER BY order_index;Search tasks by label:
SELECT t.* FROM tasks t
JOIN task_labels tl ON t.id = tl.task_id
JOIN labels l ON tl.label_id = l.id
WHERE l.name = ?;Full-text search:
SELECT t.*, rank FROM tasks t
JOIN tasks_fts fts ON t.full_id = fts.full_id
WHERE tasks_fts MATCH ?
ORDER BY rank
LIMIT ?;Get all dependencies (transitive):
SELECT t.* FROM tasks t
JOIN dependency_closure dc ON t.id = dc.descendant_id
WHERE dc.ancestor_id = ?;Lash uses a comprehensive error taxonomy with 25+ documented error codes. See error-codes.md for the complete catalog.
Error categories:
E_PARSE_*- Parsing errors (10 codes)E_LINT_*- Linting errors (7 codes)E_DEP_*- Dependency errors (3 codes)E_IO_*- I/O errors (4 codes)E_DB_*- Database errors (4 codes)E_CFG_*- Configuration errors (4 codes)E_CREATE_*- Task creation errors (13 codes)
Location: crates/lash-types/src/error.rs
#[derive(Debug, thiserror::Error)]
pub enum LashError {
#[error("Parse error: {0}")]
Parse(String),
#[error("Lint error: {0}")]
Lint(String),
#[error("Dependency error: {0}")]
Dependency(String),
#[error("I/O error: {0}")]
Io(#[from] std::io::Error),
#[error("Database error: {0}")]
Database(String),
#[error("Configuration error: {0}")]
Config(String),
#[error("Task creation error: {0}")]
TaskCreation(String),
}
pub type LashResult<T> = Result<T, LashError>;Lash uses miette for rich error reporting with context and suggestions.
Example error output:
Error: E_LINT_DEPTH_EXCEEDED
Γ Task nesting exceeds maximum allowed depth
ββ[tasks.md:42:1]
42 β - [ ] Too deeply nested task
β ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ exceeds max depth (3)
β°ββββ
help: Reduce nesting depth or adjust max_depth in config
Caused by:
Maximum depth is 3 levels, but found 4
Formatting strategies:
- Terminal output (default): Colored, formatted with miette
- JSON output (
--json): Machine-readable for scripts/agents - Plain text: No colors, for piping/logs
Error reporter (crates/lash-cli/src/error_reporter.rs):
pub fn report_error(err: &LashError, format: OutputFormat) {
match format {
OutputFormat::Human => {
// Rich terminal output with miette
eprintln!("{:?}", miette::Report::new(err));
}
OutputFormat::Json => {
// Machine-readable JSON
let json = serde_json::json!({
"error": {
"code": err.code(),
"message": err.to_string(),
"details": err.details(),
}
});
println!("{}", json);
}
OutputFormat::Plain => {
// Simple text
eprintln!("Error: {}", err);
}
}
}Each error has a stable code for programmatic handling:
impl LashError {
pub fn code(&self) -> &'static str {
match self {
LashError::Parse(_) => "E_PARSE",
LashError::Lint(msg) if msg.contains("depth") => "E_LINT_DEPTH_EXCEEDED",
LashError::Lint(msg) if msg.contains("duplicate") => "E_LINT_DUPLICATE_ID",
// ... more specific codes
_ => "E_UNKNOWN",
}
}
}CLI error explanation:
# Explain a specific error code
lash explain E_LINT_DEPTH_EXCEEDED
# List all error codes
lash explain --listpub enum ExitCode {
Success = 0,
GeneralError = 1,
LintError = 2,
DependencyError = 3,
IoError = 4,
DatabaseError = 5,
ConfigError = 6,
}Usage in CLI:
fn main() -> ExitCode {
match run() {
Ok(()) => ExitCode::Success,
Err(e @ LashError::Lint(_)) => {
report_error(&e);
ExitCode::LintError
}
Err(e) => {
report_error(&e);
ExitCode::GeneralError
}
}
}See TESTING.md for comprehensive testing documentation.
Lash employs a multi-layered testing strategy:
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β End-to-End Tests (E2E) β
β Full CLI workflows, user scenarios β
β Location: crates/lash-cli/tests/e2e_*.rs β
β Tool: assert_cmd β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β²
β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Integration Tests β
β Multi-component interactions β
β Location: crates/*/tests/*.rs β
β Tool: tempfile, rstest β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β²
β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Unit Tests β
β Single function/module behavior β
β Location: #[cfg(test)] mod tests β
β Tool: built-in Rust test framework β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β²
β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Doc Tests β
β API examples as tests β
β Location: /// code blocks in docs β
β Tool: cargo test --doc β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Characteristics:
- Fast (<1ms each)
- Isolated (no I/O, network, database)
- Test single functions/methods
- Colocated with source code
Example:
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_checkbox_open() {
let input = "- [ ] Task";
let result = parse_checkbox(input);
assert_eq!(result.unwrap().status, TaskStatus::Open);
}
#[test]
fn parse_checkbox_invalid() {
let input = "- [?] Invalid";
assert!(parse_checkbox(input).is_err());
}
}Best practices:
- One assertion per test (or closely related assertions)
- Descriptive test names:
test_parse_checkbox_with_labels - Use
#[should_panic]sparingly (preferResultassertions) - Test edge cases and error paths
Characteristics:
- Slower (I/O, database setup)
- Test component interactions
- Use temporary files/databases
- Located in
tests/directory
Example:
// crates/lash-db/tests/indexing_tests.rs
use lash_db::{init_database, Indexer};
use tempfile::TempDir;
#[test]
fn test_incremental_indexing() {
let tmp = TempDir::new().unwrap();
let db_path = tmp.path().join("test.db");
// Initial index
let conn = init_database(&db_path).unwrap();
let mut indexer = Indexer::new(&conn, config, lash_config);
let report1 = indexer.index_project().unwrap();
assert_eq!(report1.files_indexed, 5);
// No changes - should be fast
let report2 = indexer.index_project().unwrap();
assert_eq!(report2.files_indexed, 0); // All cached
}Test fixtures: crates/lash-cli/tests/fixtures/
valid/- Valid task filesinvalid/- Files with specific errorsrepos/- Complete project structures
Characteristics:
- Test full CLI workflows
- Use actual binary (
lash) - Validate user experience
- Snapshot testing with
insta
Example:
// crates/lash-cli/tests/e2e_cli_tests.rs
use assert_cmd::Command;
use predicates::prelude::*;
#[test]
fn test_lint_command() {
let mut cmd = Command::cargo_bin("lash").unwrap();
cmd.arg("lint")
.arg("tests/fixtures/valid/simple.md")
.assert()
.success()
.stdout(predicate::str::contains("No errors"));
}
#[test]
fn test_list_with_label_filter() {
let mut cmd = Command::cargo_bin("lash").unwrap();
cmd.current_dir("tests/fixtures/repos/complete")
.arg("list")
.arg("--label")
.arg("backend")
.assert()
.success();
// Snapshot test the output
insta::assert_snapshot!(cmd.output().unwrap().stdout);
}Purpose: API examples that double as tests
Example:
/// Parse a task file from a string
///
/// # Example
///
/// ```
/// use lash_core::parser::parse_file_from_string;
/// use lash_types::LashConfig;
///
/// let content = "# Test\n\n## Tasks\n\n- [ ] Task 1\n";
/// let config = LashConfig::default();
/// let result = parse_file_from_string(content, &config);
///
/// assert!(result.is_ok());
/// let task_file = result.unwrap();
/// assert_eq!(task_file.tasks.len(), 1);
/// ```
pub fn parse_file_from_string(content: &str, config: &LashConfig) -> Result<TaskFile> {
// Implementation
}Guidelines:
- All public APIs should have doctests
- Use
#prefix to hide boilerplate - Use
no_runfor examples requiring I/O - Avoid
ignoreunless absolutely necessary
Purpose: Track performance over time
Tools: Criterion (statistical benchmarking)
Example:
// crates/lash-core/benches/parser_bench.rs
use criterion::{black_box, criterion_group, criterion_main, Criterion};
use lash_core::parser::parse_file;
fn bench_parse_small_file(c: &mut Criterion) {
let content = include_str!("../tests/fixtures/small.md");
c.bench_function("parse_small_file", |b| {
b.iter(|| {
parse_file_from_string(black_box(content), &config)
});
});
}
criterion_group!(benches, bench_parse_small_file);
criterion_main!(benches);Running benchmarks:
cargo bench # All benchmarks
cargo bench -- parse # Filter by name
cargo bench -- --save-baseline main # Save baseline
cargo bench -- --baseline main # Compare to baseline- Overall: >80% line coverage
- Critical modules (parser, linter, graph): >90%
- Less critical (TUI, agent): >70%
Generate coverage:
cargo llvm-cov --workspace --html
open target/llvm-cov/html/index.htmlcrates/lash-cli/
βββ src/
β βββ command.rs # Implementation
βββ tests/
β βββ common/
β β βββ mod.rs # Shared test utilities
β βββ fixtures/
β β βββ valid/ # Valid test data
β β βββ invalid/ # Error test cases
β β βββ repos/ # Full project structures
β βββ e2e_cli_tests.rs # End-to-end tests
β βββ integration_tests.rs # Integration tests
βββ benches/
βββ cli_bench.rs # Benchmarks
We welcome contributions! Please read this guide before submitting PRs.
-
Fork and clone:
git clone https://github.com/YOUR_USERNAME/lash.git cd lash -
Install pre-commit hooks:
./scripts/install-pre-commit-hook.sh
-
Create a feature branch:
git checkout -b feature/my-feature
-
Make changes and test:
cargo test --workspace cargo clippy --workspace -- -D warnings cargo fmt --check -
Commit and push:
git add . git commit -m "Add feature: description" git push origin feature/my-feature
-
Open a pull request on GitHub
Lash enforces strict code quality standards:
Formatting: rustfmt with default settings
cargo fmt --allLinting: clippy in pedantic mode, zero warnings allowed
cargo clippy --workspace --all-targets -- -D warningsCommon clippy rules:
clippy::all- All standard lintsclippy::pedantic- Extra lints for code quality- Allowed exceptions (rare):
#![allow(clippy::module_name_repetitions)] // When justified
- Variables:
snake_case - Functions:
snake_case - Types:
PascalCase - Constants:
SCREAMING_SNAKE_CASE - Modules:
snake_case
All public APIs must have:
- Doc comments (
///not//) - Examples (preferably as doctests)
- Description of parameters and return values
- Links to related items
/// Parse a task file from disk
///
/// This function reads the file at `path`, parses it according to the
/// Lash Markdown format, and returns a structured `TaskFile`.
///
/// # Arguments
///
/// * `path` - Path to the Markdown file
/// * `config` - Configuration for parsing behavior
///
/// # Returns
///
/// Returns `Ok(TaskFile)` on success, or `Err(LashError)` if parsing fails.
///
/// # Errors
///
/// - `E_IO_FILE_NOT_FOUND` - File does not exist
/// - `E_PARSE_BAD_CHECKBOX` - Invalid checkbox syntax
///
/// # Example
///
/// ```no_run
/// use lash_core::parser::parse_file;
/// use lash_types::LashConfig;
/// use std::path::PathBuf;
///
/// let path = PathBuf::from("tasks.md");
/// let config = LashConfig::default();
/// let task_file = parse_file(&path, &config)?;
/// # Ok::<(), lash_types::LashError>(())
/// ```
pub fn parse_file(path: &Path, config: &LashConfig) -> Result<TaskFile> {
// Implementation
}- Use
Result<T, LashError>for fallible operations - Provide context with error messages
- Use appropriate error codes from taxonomy
- Never
unwrap()orexpect()in production code- Exception: In tests, use
unwrap()freely
- Exception: In tests, use
// Good
fn read_file(path: &Path) -> Result<String> {
std::fs::read_to_string(path)
.map_err(|e| LashError::Io(format!("Failed to read {}: {}", path.display(), e)))
}
// Bad - loses context
fn read_file(path: &Path) -> Result<String> {
Ok(std::fs::read_to_string(path).unwrap()) // Don't do this
}-
Title: Use conventional commit format
feat: Add new featurefix: Resolve bugdocs: Update documentationrefactor: Improve code structuretest: Add testsperf: Performance improvement
-
Description: Explain what and why
- What does this PR do?
- Why is this change needed?
- How does it work?
- Any breaking changes?
-
Checklist:
- Tests pass (
cargo test --workspace) - Clippy passes (
cargo clippy --workspace -- -D warnings) - Code is formatted (
cargo fmt --check) - Doctests added for new public APIs
- Error codes documented (if new errors)
- Benchmarks run (if performance-critical)
- Documentation updated
- Tests pass (
-
Review: Maintainers will review and provide feedback
-
Merge: Squash-merge to main after approval
PRs are evaluated on:
- Correctness: Does it work? Are tests comprehensive?
- Code quality: Follows style guide, no clippy warnings
- Performance: No regressions, benchmarks for hot paths
- Documentation: Public APIs documented, examples provided
- Maintainability: Clear code, appropriate abstractions
- Scope: Focused changes, single responsibility
Good first issues:
- Documentation improvements
- Additional test coverage
- Bug fixes with test cases
- CLI output improvements
- New color schemes for TUI
Larger contributions:
- New lint rules
- Performance optimizations
- New CLI commands
- TUI features
- Agent prompt improvements
Before starting large work:
- Open an issue to discuss the approach
- Get feedback from maintainers
- Break into smaller PRs if possible
Lash follows Semantic Versioning 2.0.0:
- Major (x.0.0): Breaking changes
- Minor (0.x.0): New features, backwards compatible
- Patch (0.0.x): Bug fixes, backwards compatible
Examples:
0.1.0β0.2.0: Addedlash addcommand (new feature)0.2.0β0.2.1: Fixed crash inlash list(bug fix)0.9.0β1.0.0: Changed annotation format (breaking)
Location: CHANGELOG.md
Format: Keep a Changelog
# Changelog
## [Unreleased]
### Added
- New feature X
### Changed
- Improved Y
### Fixed
- Bug Z
## [0.2.0] - 2024-01-15
### Added
- Task creation with `lash add`
- Interactive mode for linting
### Changed
- Improved error messages
### Fixed
- Crash when parsing empty filesCategories:
- Added: New features
- Changed: Changes in existing functionality
- Deprecated: Soon-to-be removed features
- Removed: Removed features
- Fixed: Bug fixes
- Security: Security fixes
-
Update version in
Cargo.toml:[workspace.package] version = "0.2.0"
-
Update
CHANGELOG.md:- Move
[Unreleased]changes to new version section - Add release date
- Create new empty
[Unreleased]section
- Move
-
Run full test suite:
cargo test --workspace --all-targets cargo test --doc cargo clippy --workspace -- -D warnings cargo fmt --check
-
Run benchmarks (verify no regressions):
cargo bench --workspace -- --save-baseline release-0.2.0
-
Build release binaries:
cargo build --release
-
Tag release:
git tag -a v0.2.0 -m "Release v0.2.0" git push origin v0.2.0 -
Publish to crates.io:
# Publish in dependency order cargo publish -p lash-types cargo publish -p lash-core cargo publish -p lash-db cargo publish -p lash-agent cargo publish -p lash-tui cargo publish -p lash -
Create GitHub release:
- Go to GitHub Releases
- Create release from tag
- Copy changelog section
- Upload release binaries (optional)
Prerequisites:
- crates.io account
cargo loginwith API token- Maintainer permissions
Package preparation:
- Update
Cargo.tomlmetadata - Include
README.md,LICENSE,CHANGELOG.md - Exclude test fixtures:
exclude = ["tests/fixtures/*"]
Verify before publishing:
cargo package --list # Check included files
cargo package --allow-dirty # Create .crate file
tar -tzf target/package/lash-cli-0.2.0.crate # Inspect contents- Update documentation site (if applicable)
- Announce release:
- GitHub Discussions
- Twitter/social media
- Relevant forums
- Monitor for issues in first 24-48 hours
- Plan next release cycle
- Design Document - Complete technical specification
- Error Codes - Comprehensive error catalog
- Testing Guide - Detailed testing documentation
- Task Tracking - Current development roadmap
- Development Log - Progress and decisions
- Rust Book - Rust language guide
- Cargo Book - Cargo package manager
- clap - CLI argument parsing
- rusqlite - SQLite bindings
- ratatui - Terminal UI framework
- criterion - Benchmarking
- miette - Error reporting
- Define in
crates/lash-cli/src/cli.rs(add toCommandenum) - Add handler in
crates/lash-cli/src/command.rs - Implement logic (may involve other crates)
- Add E2E tests in
crates/lash-cli/tests/ - Update
README.mdusage section
- Add error code to
crates/lash-types/src/error.rs - Implement check in
crates/lash-core/src/linter/ - Add test cases (valid and invalid fixtures)
- Document in
docs/error-codes.md - Add to linter test suite
- Add benchmark in
crates/lash-db/benches/ - Profile with
cargo bench - Check EXPLAIN QUERY PLAN in SQLite
- Add indexes if needed
- Re-run benchmark to verify improvement
Enable profiling:
let config = IndexerConfig::new(root).with_profiling(true);
let report = indexer.index_project()?;
report.profile.unwrap().print_summary();- Update
docs/design-doc.mdwith spec - Modify parser in
lash-core - Update database schema in
lash-db(add migration) - Add linter rules as needed
- Update formatter for pretty-printing
- Add tests at all layers
Happy contributing! If you have questions, open a GitHub Discussion or reach out to maintainers.