Files
tolaria/docs/ABSTRACTIONS.md
lucaronin cf398a620f docs: document types-as-files architecture
Add documentation for type documents, the Type relationship, and
the UI behavior changes (pinned type docs in section groups,
Instances group in entity view, clickable Type chip in Inspector).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 21:55:25 +01:00

268 lines
10 KiB
Markdown

# 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/
├── type/ → "Type" ← type definition documents
├── 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.
### Types as Files
Each entity type can have a corresponding **type document** in the `type/` folder (e.g., `type/project.md`, `type/person.md`). Type documents:
- Have `Is A: Type` in their frontmatter
- Describe what the type means, its expected properties, and how it relates to other types
- Are navigable entities — they appear in the sidebar under "Types" and can be opened/edited like any other note
- Serve as the "definition" for their type category
**Type relationship**: When any entry has an `isA` value (e.g., "Project"), the Rust backend automatically adds a `"Type"` entry to its `relationships` map pointing to `[[type/project]]`. This makes the type navigable from the Inspector panel.
**UI behavior**:
- Clicking a section group header (e.g., "Projects") pins the type document at the top of the NoteList if it exists, with instances listed below
- Viewing a type document in entity view shows an "Instances" group listing all entries of that type
- The Type field in the Inspector properties panel is rendered as a clickable chip that navigates to the type document
### 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.