The SyncService provides JSONL-based import/export operations for syncing Stoneforge data between SQLite and git-tracked files. It implements the dual storage model where JSONL is the source of truth and SQLite is the cache.
Overview
The SyncService is implemented in packages/quarry/src/sync/service.ts and provides:
Full and incremental export to JSONL
Import with automatic merge conflict resolution
Dirty tracking for efficient incremental exports
Dependency import/export
Cycle-safe element ordering
See also: Storage Model for sync concepts and dual storage architecture.
Installation
import { SyncService } from '@stoneforge/quarry' ;
import { createStorageBackend } from '@stoneforge/storage' ;
const backend = await createStorageBackend ({ dbPath: '.stoneforge/stoneforge.db' });
const syncService = new SyncService ( backend );
Export Operations
export()
Exports elements to JSONL files (async).
Full export (true) or incremental (false)
Include ephemeral workflows and tasks (default: false)
Elements filename (default: ‘elements.jsonl’)
Dependencies filename (default: ‘dependencies.jsonl’)
Number of elements exported
Number of dependencies exported
Whether this was an incremental export
Full path to elements file
Full path to dependencies file
// Full export
const result = await syncService . export ({
outputDir: '.stoneforge' ,
full: true ,
includeEphemeral: false ,
});
console . log ( `Exported ${ result . elementsExported } elements` );
console . log ( `Exported ${ result . dependenciesExported } dependencies` );
// Incremental export (only dirty elements)
const incrementalResult = await syncService . export ({
outputDir: '.stoneforge' ,
full: false ,
});
console . log ( `Incremental: ${ incrementalResult . elementsExported } elements` );
exportSync()
Synchronous version of export (useful for CLI).
// Synchronous export
const result = syncService . exportSync ({
outputDir: '.stoneforge' ,
full: true ,
});
exportToString()
Exports to in-memory strings (for API use).
Include ephemeral elements (default: false)
Include dependencies (default: true)
Dependencies JSONL content (if included)
const { elements , dependencies } = syncService . exportToString ({
includeEphemeral: false ,
includeDependencies: true ,
});
// Send over HTTP or save to custom location
response . setHeader ( 'Content-Type' , 'application/x-ndjson' );
response . send ( elements );
Import Operations
import()
Imports elements from JSONL files with merge conflict resolution (async).
Preview changes without applying (default: false)
Force overwrite local changes (default: false)
Elements filename (default: ‘elements.jsonl’)
Dependencies filename (default: ‘dependencies.jsonl’)
Elements skipped (no changes)
Merge conflicts detected Show ConflictRecord properties
resolution
'local' | 'remote' | 'merged'
How conflict was resolved
dependencyConflicts
DependencyConflictRecord[]
Dependency conflicts
Parse/validation errors Show ImportError properties
Line number (if applicable)
// Preview import
const dryRun = await syncService . import ({
inputDir: '.stoneforge' ,
dryRun: true ,
});
console . log ( `Would import ${ dryRun . elementsImported } elements` );
if ( dryRun . conflicts . length > 0 ) {
console . log ( 'Conflicts detected:' );
dryRun . conflicts . forEach ( c => {
console . log ( ` ${ c . elementId } : ${ c . field } ( ${ c . resolution } )` );
});
}
// Actual import with automatic merge
const result = await syncService . import ({
inputDir: '.stoneforge' ,
dryRun: false ,
});
console . log ( `Imported ${ result . elementsImported } elements` );
console . log ( `Skipped ${ result . elementsSkipped } (no changes)` );
if ( result . errors . length > 0 ) {
console . error ( ` ${ result . errors . length } errors:` );
result . errors . forEach ( e => {
console . error ( ` Line ${ e . line } : ${ e . message } ` );
});
}
// Force import (overwrite local changes)
const forced = await syncService . import ({
inputDir: '.stoneforge' ,
force: true ,
});
importSync()
Synchronous version of import (useful for CLI).
// Synchronous import
const result = syncService . importSync ({
inputDir: '.stoneforge' ,
});
importFromStrings()
Imports from in-memory JSONL strings (for API use).
Dependencies JSONL content
Import options (dryRun, force)
// Import from HTTP request body
const elementsContent = await request . text ();
const dependenciesContent = '' ;
const result = syncService . importFromStrings (
elementsContent ,
dependenciesContent ,
{ dryRun: false }
);
console . log ( `Imported ${ result . elementsImported } elements from API` );
Merge Conflict Resolution
The SyncService automatically resolves merge conflicts using these rules:
Element Merge Strategy
Closed/Tombstone Always Wins : If either version is closed or tombstoned, that status takes precedence
Latest Timestamp Wins : For other conflicts, the element with the later updatedAt timestamp is used
Field-Level Merging : Some fields are merged intelligently:
Tags: Union of both tag sets
Metadata: Merged with remote values taking precedence
// Example conflict scenario:
// Local: { title: 'Fix bug', status: 'open', updatedAt: '2026-03-01T10:00:00Z' }
// Remote: { title: 'Fix bug', status: 'closed', updatedAt: '2026-03-01T09:00:00Z' }
// Result: status='closed' (closed always wins), other fields from local (newer)
const result = await syncService . import ({
inputDir: '.stoneforge' ,
});
result . conflicts . forEach ( conflict => {
console . log ( `Conflict in ${ conflict . elementId } . ${ conflict . field } ` );
console . log ( ` Local: ${ JSON . stringify ( conflict . localValue ) } ` );
console . log ( ` Remote: ${ JSON . stringify ( conflict . remoteValue ) } ` );
console . log ( ` Resolution: ${ conflict . resolution } ` );
});
Dependency Merge Strategy
Additive : Remote dependencies not in local are added
Removal Detection : Local dependencies not in remote are removed
Referential Integrity : Dependencies with dangling references (missing elements) are skipped
// Dependencies are compared by (blockedId, blockerId, type) composite key
const result = await syncService . import ({
inputDir: '.stoneforge' ,
});
console . log ( `Added ${ result . dependenciesImported } dependencies` );
if ( result . dependencyConflicts . length > 0 ) {
console . log ( 'Dependency conflicts:' );
result . dependencyConflicts . forEach ( c => {
console . log ( ` ${ c . blockedId } → ${ c . blockerId } ( ${ c . type } )` );
});
}
Elements are serialized with entities first (for referential integrity), then sorted by creation time:
{ "id" : "el-0000" , "type" : "entity" , "name" : "operator" , "createdAt" : "2026-03-01T00:00:00.000Z" , "updatedAt" : "2026-03-01T00:00:00.000Z" , "createdBy" : "el-0000" , "tags" :[], "metadata" :{}}
{ "id" : "el-1234" , "type" : "task" , "title" : "Fix bug" , "status" : "open" , "priority" : 3 , "createdAt" : "2026-03-01T10:00:00.000Z" , "updatedAt" : "2026-03-01T10:00:00.000Z" , "createdBy" : "el-0000" , "tags" :[ "bug" ], "metadata" :{}}
Dependencies are sorted by creation time:
{ "blockedId" : "el-1234" , "blockerId" : "el-5678" , "type" : "blocks" , "createdAt" : "2026-03-01T10:05:00.000Z" , "createdBy" : "el-0000" , "metadata" :{}}
Dirty Tracking
The SQLite backend tracks which elements have been modified since the last export:
// Make some changes
await api . update ( taskId , { status: 'in_progress' });
await api . create ( newTask );
// Incremental export only exports changed elements
const result = await syncService . export ({
outputDir: '.stoneforge' ,
full: false , // Only dirty elements
});
console . log ( `Exported ${ result . elementsExported } changed elements` );
// Dirty tracking is cleared after successful export
Element Ordering
Elements are exported in a specific order to maintain referential integrity during import:
Entities - Must exist before they’re referenced in other elements
Other Elements - Sorted by createdAt timestamp
This ensures that when importing, entities exist before tasks/documents that reference them as createdBy or assignee.
// Export order example:
// 1. el-0000 (entity: operator)
// 2. el-0001 (entity: director)
// 3. el-1234 (task created by el-0000)
// 4. el-5678 (document created by el-0001)
Error Handling
Parse Errors
Invalid JSONL lines are reported but don’t stop the import:
const result = await syncService . import ({
inputDir: '.stoneforge' ,
});
if ( result . errors . length > 0 ) {
result . errors . forEach ( error => {
console . error ( `Line ${ error . line } in ${ error . file } : ${ error . message } ` );
console . error ( `Content: ${ error . content } ` );
});
}
Referential Integrity
Dependencies with missing elements are automatically skipped:
// If elements.jsonl is missing el-5678, but dependencies.jsonl has:
// {"blockedId":"el-1234","blockerId":"el-5678","type":"blocks",...}
// This dependency will be skipped with a warning
const result = await syncService . import ({
inputDir: '.stoneforge' ,
});
console . log ( `Skipped ${ result . dependenciesSkipped } dependencies (missing elements)` );
CLI Usage
The SyncService powers the sf export and sf import CLI commands:
# Full export
sf export --full
# Incremental export
sf export
# Import with dry run
sf import --dry-run
# Import with conflict preview
sf import
# Force import (overwrite local)
sf import --force