Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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:

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

#![allow(unused)]
fn main() {
pub struct Block {
    pub id: String,                   // UUID
    pub content: String,              // Block text
    pub properties: BlockProperties,  // Tags, status, custom
    pub children: Vec<Block>,         // Nested blocks
    // ... timestamps ...
}

pub struct BlockProperties {
    pub tags: Vec<String>,            // #card, #important
    pub status: Option<TaskStatus>,   // TODO, DONE, etc.
    pub custom: HashMap<String, String>, // property:: value
    pub block_refs: Vec<String>,      // ((uuid))
    pub page_refs: Vec<String>,       // [[page]]
}
}

2. Extend Node Model

#![allow(unused)]
fn main() {
pub struct Node {
    // ... existing fields ...
    pub content: String,              // Source of truth (markdown)
    pub blocks: Option<Vec<Block>>,   // Cached structure (optional)
}
}

3. Bidirectional Parser

  • Parse: Markdown → Vec<Block> (lazy, on-demand)
  • Serialize: Vec<Block> → Markdown (for export)

4. Storage Strategy

Filesystem (git-friendly, Logseq-compatible):

- Block 1 #card
  - Nested answer
- TODO Block 2
  priority:: high

SurrealDB (queryable):

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

#![allow(unused)]
fn main() {
// 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 kb find-cards CLI command
  • Add kb 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<String, Value>.

Rejected: Not type-safe, harder to query, no schema validation.

References

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: kb reindex --parse-blocks to populate

Decision Date: 2026-01-17 Approvers: TBD Review Date: After Phase 2 implementation