# ADR-004: Logseq Blocks Support ## Status **Proposed** (Design phase) ## Context Logseq uses **content blocks** as the fundamental unit of information, not full documents. KB currently treats `Node.content` as flat markdown string, which loses block-level features on import/export: **Lost features**: - Block properties (`#card`, `TODO`, custom properties) - Block hierarchy (outliner nesting) - Block references (`((block-uuid))`) - Block-level queries (find all flashcards, TODOs) **User requirement**: Round-trip Logseq import/export with full fidelity: ```text Logseq → KOGRAL Import → KOGRAL Storage → KOGRAL Export → Logseq (blocks preserved at every step) ``` ## Decision Implement **Hybrid Block Support** (structured + markdown): ### 1. Add Block Data Structure ```rust pub struct Block { pub id: String, // UUID pub content: String, // Block text pub properties: BlockProperties, // Tags, status, custom pub children: Vec, // Nested blocks // ... timestamps ... } pub struct BlockProperties { pub tags: Vec, // #card, #important pub status: Option, // TODO, DONE, etc. pub custom: HashMap, // property:: value pub block_refs: Vec, // ((uuid)) pub page_refs: Vec, // [[page]] } ``` ### 2. Extend Node Model ```rust pub struct Node { // ... existing fields ... pub content: String, // Source of truth (markdown) pub blocks: Option>, // Cached structure (optional) } ``` ### 3. Bidirectional Parser - **Parse**: Markdown → `Vec` (lazy, on-demand) - **Serialize**: `Vec` → Markdown (for export) ### 4. Storage Strategy **Filesystem** (git-friendly, Logseq-compatible): ```markdown - Block 1 #card - Nested answer - TODO Block 2 priority:: high ``` **SurrealDB** (queryable): ```sql DEFINE TABLE block; DEFINE FIELD node_id ON block TYPE record(node); DEFINE FIELD block_id ON block TYPE string; DEFINE FIELD properties ON block TYPE object; DEFINE INDEX block_tags ON block COLUMNS properties.tags; ``` ### 5. Query Extensions ```rust // Find all flashcards graph.find_blocks_by_tag("card") // Find all TODOs graph.find_all_todos() // Find blocks with custom property node.find_blocks_by_property("priority", "high") ``` ## Consequences ### Positive ✅ **Full Logseq Compatibility** - Import/export preserves all block features ✅ **Queryable Blocks** - Find #card, TODO, custom properties across KOGRAL ✅ **Backward Compatible** - Existing nodes without blocks still work ✅ **Type-Safe** - Structured data instead of regex parsing everywhere ✅ **Extensible** - Custom block properties supported ✅ **Hierarchy Preserved** - Nested blocks maintain parent-child relationships ### Negative ⚠️ **Added Complexity** - New data structures, parser, sync logic ⚠️ **Dual Representation** - Must keep `content` and `blocks` in sync ⚠️ **Storage Overhead** - SurrealDB stores both markdown and structure ⚠️ **Migration Required** - Existing data needs parsing to populate blocks ### Neutral ⚙️ **Lazy Parsing** - Blocks parsed on-demand (not stored by default) ⚙️ **Opt-In** - Config flag `blocks.enabled` to activate features ⚙️ **Gradual Adoption** - Can implement in phases ## Implementation Phases **Phase 1: Foundation** (No behavior change) - Add `Block` struct to `models/block.rs` - Add optional `blocks` field to `Node` - Add config: `blocks.enabled = false` (default off) **Phase 2: Parser** - Implement `BlockParser::parse()` (markdown → blocks) - Implement `BlockParser::serialize()` (blocks → markdown) - Add `Node::get_blocks()` method (lazy parsing) **Phase 3: Logseq Integration** - Update `LogseqImporter` to parse blocks - Update `LogseqExporter` to serialize blocks - Test round-trip (Logseq → KB → Logseq) **Phase 4: Query API** - Add `Graph::find_blocks_by_tag()` - Add `Graph::find_all_todos()` - Add `Node::find_blocks_by_property()` **Phase 5: MCP/CLI Integration** - Add `kb/find_blocks` MCP tool - Add `kogral find-cards` CLI command - Add `kogral find-todos` CLI command **Phase 6: SurrealDB Backend** - Create `block` table schema - Index on tags, status, properties - Store blocks alongside nodes ## Alternatives Considered ### Alternative 1: Blocks as First-Class Nodes Convert each Logseq block to a separate KOGRAL Node. **Rejected**: Too granular, explosion of nodes, loses document context. ### Alternative 2: Parser-Only (No Storage) Keep `content: String`, parse blocks on every access. **Rejected**: Can't query blocks in database, parse overhead, can't index. ### Alternative 3: Metadata Field Store blocks in `metadata: HashMap`. **Rejected**: Not type-safe, harder to query, no schema validation. ## References - [Logseq Block Format](https://docs.logseq.com/#/page/blocks) - [Full Design Document](../logseq-blocks-design.md) - [Implementation Tracking](https://github.com/.../issues/XXX) ## Notes **Backward Compatibility Strategy**: - `content` remains source of truth - `blocks` is optional enhancement - Old code works unchanged - New features opt-in via config **Migration Path**: - Existing users: blocks disabled by default - New users: blocks enabled, parsed on import - Manual: `kogral reindex --parse-blocks` to populate --- **Decision Date**: 2026-01-17 **Approvers**: TBD **Review Date**: After Phase 2 implementation