Design proposal for making the graph the single source of truth, eliminating markdown files.

Motivation

Current workflow:

Markdown files → Parse frontmatter → Extract entities → Sync to graph

Problems:

  1. Two sources of truth (files + graph)
  2. Sync step required after every change
  3. Entity extraction is a separate manual step
  4. Claude Code must write files, then run sync

Proposed workflow:

Claude Code → CLI → Graph (direct)

Benefits:

  • Single source of truth
  • No sync step needed
  • Immediate consistency
  • Simpler mental model

Schema Changes

Current Schema (v1.0.3)

Lattice uses a unified nodes table for all entity types (including Documents):

-- Single table for all node types (graph.service.ts:136-148)
CREATE TABLE nodes (
label VARCHAR NOT NULL, -- 'Document', 'Technology', 'Concept', etc.
name VARCHAR NOT NULL, -- File path (for docs) or entity name
properties JSON, -- { title, summary, topic, description, ... }
embedding FLOAT[512], -- Voyage voyage-3-lite embeddings
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW(),
PRIMARY KEY(label, name) -- Composite key
);
-- Relationships between nodes (graph.service.ts:150-162)
CREATE TABLE relationships (
source_label VARCHAR NOT NULL,
source_name VARCHAR NOT NULL,
relation_type VARCHAR NOT NULL, -- 'REFERENCES', 'APPEARS_IN'
target_label VARCHAR NOT NULL,
target_name VARCHAR NOT NULL,
properties JSON, -- { documentPath, ... }
created_at TIMESTAMP DEFAULT NOW(),
PRIMARY KEY(source_label, source_name, relation_type, target_label, target_name)
);

Current document storage:

  • label = 'Document'
  • name = file path (e.g., /home/uptown/.lattice/docs/lattice/README.md)
  • properties = { title, summary, topic, status, ... }
  • Content lives in markdown files, NOT in the graph

Proposed Schema (Graph-Only)

Option A: Add content to properties JSON (minimal change)

-- No schema change needed!
-- Just store content in the existing properties JSON:
properties = {
"title": "Research Title",
"summary": "Brief summary",
"topic": "databases",
"content": "Full research content here..." -- NEW
}

Option B: Add dedicated content column (cleaner)

ALTER TABLE nodes ADD COLUMN content TEXT;
-- Then for Document nodes:
-- label = 'Document'
-- name = UUID or slug (not file path)
-- content = full research text
-- properties = { title, summary, topic }

Key changes either way:

  1. name changes from file path → UUID/slug for documents
  2. Content stored in graph instead of files
  3. Remove content_hash, frontmatter_hash from manifest (no files to track)

Recommendation

Option A (properties JSON) for Phase 1:

  • No schema migration needed
  • DuckDB handles large JSON values well
  • Can extract to dedicated column later if needed
// GraphService addition
async createDocument(doc: { title: string; content: string; ... }): Promise<string> {
const id = crypto.randomUUID();
await this.upsertNode('Document', {
name: id,
title: doc.title,
summary: doc.summary,
content: doc.content, // Store in properties
topic: doc.topic,
});
return id;
}

New CLI Commands

lattice create

Create a new document directly in the graph.

Terminal window
# Basic usage
lattice create --title "Topic Name" --content "Research content..."
# With entities (JSON format)
lattice create \
--title "DuckDB Performance Analysis" \
--content "Full research content here..." \
--summary "Analysis of DuckDB query performance" \
--topic "databases" \
--entities '[{"name": "DuckDB", "type": "Technology", "description": "Embedded OLAP database"}]' \
--relationships '[{"source": "DuckDB", "relation": "IMPLEMENTS", "target": "OLAP"}]'
# From stdin (for piping)
echo "Research content" | lattice create --title "Topic" --stdin
# Returns: document ID
# Output: Created document abc123-def456-...

Implementation (following existing NestJS patterns in src/commands/):

@Injectable()
@Command({ name: 'create', description: 'Create a new document in the graph' })
export class CreateCommand extends CommandRunner {
constructor(
private readonly graphService: GraphService,
private readonly embeddingService: EmbeddingService,
) { super(); }
async run(params: string[], options: CreateOptions): Promise<void> {
const id = crypto.randomUUID();
// Upsert document node (uses existing GraphService.upsertNode)
await this.graphService.upsertNode('Document', {
name: id,
title: options.title,
summary: options.summary,
content: options.content || await this.readStdin(),
topic: options.topic,
});
// Generate embedding (uses existing EmbeddingService)
const embeddingText = `${options.title}\n\n${options.summary || ''}`;
const embedding = await this.embeddingService.embed(embeddingText);
await this.graphService.updateNodeEmbedding('Document', id, embedding);
// Create entity relationships if provided
if (options.entities) {
const entities = JSON.parse(options.entities);
for (const entity of entities) {
await this.graphService.upsertNode(entity.type, {
name: entity.name,
description: entity.description,
});
await this.graphService.upsertRelationship(
entity.type, entity.name,
'APPEARS_IN',
'Document', id,
);
}
}
console.log(`Created document ${id}`);
process.exit(0);
}
}

lattice update

Update an existing document.

Terminal window
# Update content
lattice update <id> --content "Updated content..."
# Update specific fields
lattice update <id> --title "New Title" --summary "New summary"
# Add entities to existing document
lattice update <id> --add-entities '[{"name": "Redis", "type": "Technology"}]'
# From stdin
echo "New content" | lattice update <id> --stdin

Implementation:

@Command({ name: 'update', description: 'Update an existing document' })
export class UpdateCommand extends CommandRunner {
async run([id]: string[], options: UpdateOptions): Promise<void> {
const updates: Partial<Document> = {};
if (options.content) updates.content = options.content;
if (options.title) updates.title = options.title;
if (options.summary) updates.summary = options.summary;
if (options.stdin) updates.content = await this.readStdin();
updates.updated = new Date();
await this.graphService.updateDocument(id, updates);
// Re-generate embedding if content changed
if (updates.content) {
const doc = await this.graphService.getDocument(id);
const embedding = await this.embeddingService.embed(
composeDocumentEmbeddingText(doc)
);
await this.graphService.updateEmbedding(id, embedding);
}
console.log(`Updated document ${id}`);
}
}

lattice get

Retrieve a document’s full content.

Terminal window
# Get full document as JSON
lattice get <id>
# Get just content (for piping)
lattice get <id> --content-only
# Get with related entities
lattice get <id> --with-entities
# Output format
lattice get <id> --format json|markdown|text

Output example:

{
"id": "abc123-def456",
"title": "DuckDB Performance Analysis",
"content": "Full research content here...",
"summary": "Analysis of DuckDB query performance",
"topic": "databases",
"created": "2025-12-07T15:00:00Z",
"updated": "2025-12-07T15:30:00Z",
"entities": [
{"name": "DuckDB", "type": "Technology", "description": "..."}
],
"relationships": [
{"source": "DuckDB", "relation": "IMPLEMENTS", "target": "OLAP"}
]
}

lattice delete

Remove a document from the graph.

Terminal window
lattice delete <id>
lattice delete <id> --force # Skip confirmation

lattice list

List documents with optional filtering.

Terminal window
lattice list # All documents
lattice list --topic databases # Filter by topic
lattice list --recent 10 # Last 10 modified
lattice list --format table|json

Updated /research Workflow

The /research slash command changes to:

## Process
### Step 1: Search Existing Research
lattice search "<query>" --limit 10
### Step 2: Review Results
- If found: Display document content
- Ask user if existing research answers their question
### Step 3: Create New (if needed)
If user wants new research:
1. Perform web research
2. Create document directly:
lattice create \
--title "Research Title" \
--content "$(cat <<'EOF'
# Research Title
## Summary
...
## Key Findings
...
## Sources
...
EOF
)" \
--summary "Brief summary for search" \
--entities '[...]' \
--relationships '[...]'
### Step 4: Entity Extraction
Entities are provided at creation time via --entities flag.
No separate extraction step needed.

Migration Path

Phase 1: Add New Commands (Non-Breaking)

  1. Add content column to existing schema
  2. Implement create, update, get, delete commands
  3. Keep existing sync command working
  4. Test with Claude Code

Phase 2: Migrate Existing Data

-- Migrate content from files to graph
-- Run once for existing documents
UPDATE documents
SET content = (SELECT read_text(path))
WHERE content IS NULL;

Or via CLI:

Terminal window
lattice migrate-content # Reads files, populates content column

Phase 3: Deprecate File-Based Commands

  1. Mark sync, status as deprecated
  2. Update /research command to use new workflow
  3. Remove file watching

Phase 4: Remove File Support

  1. Remove sync.command.ts
  2. Remove document-parser.service.ts
  3. Remove manifest.service.ts
  4. Remove path-based operations
  5. Update schema to remove path column

Trade-offs

What We Lose

FeatureCurrentProposedMitigation
Git historyFull file historyNo automatic versioningAdd document_versions table?
Human-readableEdit in any editorCLI-only editinglattice get --format markdown
CollaborationGit merge/conflict resolutionSingle-writerOut of scope for personal KB
BackupCopy fileslattice exportAdd export command
Grep/findUnix tools workMust use CLIlattice search is better anyway

What We Gain

FeatureCurrentProposed
Single source of truthFiles + GraphGraph only
Immediate consistencyRequires syncAlways consistent
Simpler mental modelWrite → Sync → SearchWrite → Search
Claude Code integrationWrite file + run syncSingle CLI call
Entity handlingSeparate extraction stepInline at creation

Open Questions

  1. Versioning: Do we need document version history? Could add document_versions table.

  2. Backup/Export: Should lattice export dump to markdown for backup?

  3. Bulk import: Keep a lattice import <file.md> for one-time migrations?

  4. Entity auto-extraction: Should create auto-extract entities from content, or require explicit --entities?


Next Steps

  1. Implement content column in schema
  2. Implement lattice create command
  3. Implement lattice get command
  4. Implement lattice update command
  5. Update /research slash command
  6. Test end-to-end with Claude Code
  7. Deprecate sync command
  8. Remove file-based code

Sources

Lattice codebase (~/Projects/zabaca/lattice/):

  • Graph schema: src/graph/graph.service.ts:136-162
  • Node operations: src/graph/graph.service.ts:203-232 (upsertNode)
  • Vector search: src/graph/graph.service.ts:464-506
  • Sync workflow: src/sync/sync.service.ts
  • Document parser: src/sync/document-parser.service.ts
  • Path utilities: src/utils/paths.ts
  • Example command: src/commands/init.command.ts

External docs: