- Add docs/ with ARCHITECTURE, ABSTRACTIONS, GETTING-STARTED, THEMING - Add GitHub Actions workflow with automated quality checks: • Tests (frontend + Rust) • Coverage (70% threshold) • Code Health (CodeScene delta analysis) • Documentation check (warning only) • Lint & Format (ESLint + Clippy + rustfmt) - Configure coverage in vite.config.ts (v8 provider, 70% threshold) - Add test:coverage script to package.json - Install @vitest/coverage-v8 - Update CLAUDE.md with docs-update requirement - Add CI/CD setup guides in .github/
9.1 KiB
Abstractions
Key abstractions and domain models in Laputa.
Document Model
All data lives in markdown files with YAML frontmatter. There is no database — the filesystem is the source of truth.
VaultEntry
The core data type representing a single note, defined identically in Rust (src-tauri/src/vault.rs) and TypeScript (src/types.ts):
// src/types.ts
interface VaultEntry {
path: string // Absolute file path: /Users/luca/Laputa/project/my-project.md
filename: string // Just the filename: my-project.md
title: string // Extracted from first # heading, or filename as fallback
isA: string | null // Entity type: Project, Procedure, Person, etc.
aliases: string[] // Alternative names for wikilink resolution
belongsTo: string[] // Parent relationships (wikilinks)
relatedTo: string[] // Related entity links (wikilinks)
status: string | null // Active, Done, Paused, Archived, Dropped
owner: string | null // Person responsible
cadence: string | null // Update frequency: Weekly, Monthly, etc.
modifiedAt: number | null // Unix timestamp (seconds)
createdAt: number | null // Unix timestamp (seconds)
fileSize: number
}
Entity Types (isA)
Entity type is inferred from the folder structure. The vault is organized by type:
~/Laputa/
├── project/ → "Project"
├── responsibility/ → "Responsibility"
├── procedure/ → "Procedure"
├── experiment/ → "Experiment"
├── person/ → "Person"
├── event/ → "Event"
├── topic/ → "Topic"
├── note/ → "Note"
├── quarter/ → "Quarter"
├── journal/ → "Journal"
├── essay/ → "Essay"
└── evergreen/ → "Evergreen"
Mapping logic lives in vault.rs:parse_md_file(). If a folder doesn't match any known type, the folder name is capitalized and used as-is.
Frontmatter Format
Standard YAML frontmatter between --- delimiters:
---
title: Write Weekly Essays
is_a: Procedure
status: Active
owner: Luca Rossi
cadence: Weekly
belongs_to:
- "[[responsibility/grow-newsletter]]"
related_to:
- "[[topic/writing]]"
aliases:
- Weekly Writing
---
Supported value types (defined in src-tauri/src/frontmatter.rs as FrontmatterValue):
- String:
status: Active - Number:
priority: 5 - Bool:
archived: true - List: Multi-line
- itemor inline[item1, item2] - Null:
owner:(empty value)
Title Extraction
Title comes from the first # Heading in the markdown body. If none is found, the filename (without .md) is used as fallback. This logic lives in vault.rs:extract_title().
Sidebar Selection
Navigation state is modeled as a discriminated union:
type SidebarSelection =
| { kind: 'filter'; filter: 'all' | 'favorites' }
| { kind: 'sectionGroup'; type: string } // e.g. type: 'Project'
| { kind: 'entity'; entry: VaultEntry } // specific entity selected
| { kind: 'topic'; entry: VaultEntry } // topic selected
File System Integration
Vault Scanning (Rust)
vault::scan_vault(path) in src-tauri/src/vault.rs:
- Validates the path exists and is a directory
- Uses
walkdirto recursively traverse the directory (follows symlinks) - Filters to
.mdfiles only - For each file, calls
parse_md_file():- Reads file content with
fs::read_to_string() - Parses frontmatter with
gray_matter::Matter::<YAML> - Extracts title from first
#heading - Infers entity type from parent folder name
- Parses dates (
created_at,created_time) as ISO 8601 to Unix timestamps - Collects file metadata (size, modification time)
- Reads file content with
- Sorts results by
modified_atdescending (newest first) - Skips unparseable files with a warning log
Frontmatter Manipulation (Rust)
frontmatter::update_frontmatter_content() in src-tauri/src/frontmatter.rs performs line-by-line YAML editing:
- Finds the frontmatter block between
---delimiters - Iterates through lines looking for the target key (handles quoted keys like
"Is A") - If found: replaces the value (consuming multi-line list items if present)
- If not found: appends the new key-value at the end of the frontmatter
- If no frontmatter exists: creates a new
---block with the key-value
The with_frontmatter() helper wraps this in a read-transform-write cycle on the actual file.
Content Loading
- Tauri mode: Content is loaded on-demand when a tab is opened via
invoke('get_note_content', { path }) - Browser mode: All content is loaded at startup from
MOCK_CONTENTinmock-tauri.ts - Content for backlink detection (
allContent) is stored in memory asRecord<string, string>
Git Integration
Git operations live in src-tauri/src/git.rs. All operations shell out to the git CLI (not libgit2).
Data Types
interface GitCommit {
hash: string // Full SHA-1
shortHash: string // First 7 chars
message: string
author: string
date: number // Unix timestamp
}
interface ModifiedFile {
path: string // Absolute path
relativePath: string // Relative to vault root
status: 'modified' | 'added' | 'deleted' | 'untracked' | 'renamed'
}
Operations
| Operation | Git command | Notes |
|---|---|---|
| File history | git log --format=%H|%h|%an|%aI|%s -n 20 -- <file> |
Last 20 commits for a file |
| Modified files | git status --porcelain |
Filtered to .md files only |
| File diff | git diff -- <file>, fallback to --cached, then synthetic diff for untracked |
Unified diff format |
| Commit | git add -A && git commit -m "<message>" |
Stages all changes |
| Push | git push |
Pushes to upstream of current branch |
Frontend Integration
- Modified file badges: Loaded at startup, shown in sidebar and breadcrumb bar
- Diff view: Loaded on-demand when user clicks the diff toggle in the breadcrumb bar
- Git history: Loaded when active tab changes, shown in Inspector panel
- Commit dialog: Triggered from sidebar, runs commit + push
BlockNote Customization
The editor uses BlockNote (not CodeMirror 6) for rich text editing.
Custom Wikilink Inline Content
Defined in src/components/Editor.tsx:
const WikiLink = createReactInlineContentSpec(
{
type: "wikilink",
propSchema: { target: { default: "" } },
content: "none",
},
{
render: (props) => (
<span className="wikilink" data-target={props.inlineContent.props.target}>
{props.inlineContent.props.target}
</span>
),
}
)
const schema = BlockNoteSchema.create({
inlineContentSpecs: {
...defaultInlineContentSpecs,
wikilink: WikiLink,
},
})
Markdown-to-BlockNote Pipeline
Since BlockNote doesn't natively understand [[wikilinks]], content goes through a preprocessing pipeline in src/utils/wikilinks.ts:
Raw markdown
→ splitFrontmatter() → [yaml, body]
→ preProcessWikilinks(body) → replaces [[target]] with Unicode placeholder tokens
→ editor.tryParseMarkdownToBlocks() → BlockNote block tree
→ injectWikilinks(blocks) → walks tree, replaces placeholder text with wikilink inline content nodes
→ editor.replaceBlocks()
Placeholder tokens use \u2039 (single left-pointing angle quotation mark) and \u203A (single right-pointing) to avoid colliding with markdown syntax.
Wikilink Navigation
Two navigation mechanisms:
-
Click handler: A DOM event listener on
.editor__blocknote-containercatches clicks on.wikilinkelements and callsonNavigateWikilink(target). -
Suggestion menu: Typing
[[triggers BlockNote'sSuggestionMenuController, which shows a filtered list of all vault entries. Selecting one inserts a wikilink inline content node.
Wikilink resolution in useNoteActions.handleNavigateWikilink() uses fuzzy matching:
- Exact title match
- Alias match
- Path stem match (e.g.,
person/matteo-cellini) - Filename stem match
- Slug-to-words match (e.g.,
matteo-cellini→matteo cellini)
Theme System
See THEMING.md for the full theme system documentation.
In brief: src/theme.json defines editor typography and styling as nested JSON. The useEditorTheme hook flattens it into CSS custom properties that are applied as inline styles on the BlockNote container.
Inspector Abstraction
The Inspector panel (src/components/Inspector.tsx) is composed of four sub-panels:
- DynamicPropertiesPanel (
src/components/DynamicPropertiesPanel.tsx): Renders frontmatter as editable key-value pairs. UsesEditableValuefor inline editing. - Relationships: Shows
belongs_toandrelated_towikilinks as clickable chips. - Backlinks: Scans
allContentfor notes that reference the current note via[[title]]or[[path]]. - Git History: Shows the last few commits from
gitHistorystate.
Frontmatter parsing on the TypeScript side is handled by src/utils/frontmatter.ts:parseFrontmatter(), a lightweight YAML parser that handles strings, booleans, inline arrays, and multi-line lists.