Files
tolaria/docs/ABSTRACTIONS.md

251 lines
9.1 KiB
Markdown
Raw Normal View History

# 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`):
```typescript
// 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:
```yaml
---
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:
```typescript
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
```typescript
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](https://www.blocknotejs.org/) (not CodeMirror 6) for rich text editing.
### Custom Wikilink Inline Content
Defined in `src/components/Editor.tsx`:
```typescript
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:
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-cellini``matteo cellini`)
## Theme System
See [THEMING.md](./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.