Files
tolaria/docs/ABSTRACTIONS.md
lucaronin e7c4563200 feat: add comprehensive documentation and CI/CD pipeline
- 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/
2026-02-17 13:10:43 +01:00

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 - item or 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:

  1. Validates the path exists and is a directory
  2. Uses walkdir to recursively traverse the directory (follows symlinks)
  3. Filters to .md files only
  4. 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)
  5. Sorts results by modified_at descending (newest first)
  6. 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:

  1. Finds the frontmatter block between --- delimiters
  2. Iterates through lines looking for the target key (handles quoted keys like "Is A")
  3. If found: replaces the value (consuming multi-line list items if present)
  4. If not found: appends the new key-value at the end of the frontmatter
  5. 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_CONTENT in mock-tauri.ts
  • Content for backlink detection (allContent) is stored in memory as Record<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.

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.

Two navigation mechanisms:

  1. Click handler: A DOM event listener on .editor__blocknote-container catches clicks on .wikilink elements and calls onNavigateWikilink(target).

  2. Suggestion menu: Typing [[ triggers BlockNote's SuggestionMenuController, 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-cellinimatteo 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:

  1. DynamicPropertiesPanel (src/components/DynamicPropertiesPanel.tsx): Renders frontmatter as editable key-value pairs. Uses EditableValue for inline editing.
  2. Relationships: Shows belongs_to and related_to wikilinks as clickable chips.
  3. Backlinks: Scans allContent for notes that reference the current note via [[title]] or [[path]].
  4. Git History: Shows the last few commits from gitHistory state.

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.