## 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

- Logseq Block Format
- Full Design Document
- Implementation Tracking

## 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