This document defines the wire format and data structures for the Gantt-Sync Protocol. It is intended for developers implementing a client or server in any language (Rust, Python, Go, TypeScript, etc.).
The HLC is the backbone of the protocol's causality. All timestamps transmitted over the wire MUST adhere to this format.
The canonical string representation of an HLC timestamp is:
<ISO-8601-Time>-<Counter-Hex>-<NodeId>
- ISO-8601-Time: UTC timestamp with millisecond precision (e.g.,
2023-10-27T10:00:00.123Z). - Counter-Hex: 4-digit hexadecimal counter (e.g.,
0000,000A,FFFF). Used to distinguish events occurring within the same millisecond. - NodeId: A standard string identifier for the node/device generating the timestamp. Should not contain dashes if possible, though parsers should handle it.
2023-10-27T10:00:00.123Z-0000-deviceA2023-10-27T10:00:00.123Z-0001-deviceA(Same ms, subsequent event)
To compare two HLCs, A and B:
- Compare
ISO-8601-Time. Higher value wins. - If equal due to millisecond collision, compare
Counter-Hex. Higher value wins. - If equal, compare
NodeIdlexicographically (arbitrary tie-breaker).
The synchronization stream consists of an ordered sequence of JSON Operations.
{
"type": "string", // The type of operation
"schemaVersion": 1, // Protocol version (default: 1)
"timestamp": "string", // HLC string (see 1.1)
"actorId": "string", // The ID of the user/system performing the action
"data": { // The payload (depends on type)
...
}
}
Upserts a task. Fields present in data will be merged; missing fields are ignored.
{
"type": "UPDATE_TASK",
"data": {
"id": "task-uuid-123",
"name": "New Task Name",
"completion": 0.5,
"start": "2023-11-01T09:00:00.000Z"
}
// ... metadata fields ...
}Logically deletes a task.
{
"type": "DELETE_TASK",
"data": {
"id": "task-uuid-123"
}
}Carries a list of operations to be applied atomically.
{
"type": "BATCH_UPDATE",
"data": {
"operations": [
{ "type": "INSERT_TASK", ... },
{ "type": "UPDATE_TASK", ... }
]
}
}Creates an immutable snapshot of the current state.
{
"type": "CREATE_TAG",
"data": {
"id": "tag-uuid-123",
"name": "Release 1.0",
"merkleRoot": "hash-abc-123",
"timestamp": "2023-11-01T09:00:00.000Z-0000-node",
"metadata": {}
}
}Deletes a tag.
{
"type": "DELETE_TAG",
"data": {
"id": "tag-uuid-123"
}
}These models define the "data" payload within operations or the state snapshot.
| Field | Type | Description |
|---|---|---|
id |
String | Unique UUID. |
rowId |
String | Logic row identifier for UI. |
start |
ISO-8601 | Start time (UTC). |
end |
ISO-8601 | End time (UTC). |
name |
String | Task label. |
completion |
Double | Progress (0.0 to 1.0). |
isSummary |
Boolean | If true, computed from children. |
isMilestone |
Boolean | Zero-duration event. |
resourceId |
String? | ID of assigned resource. |
parentId |
String? | ID of parent task (for hierarchy). |
isDeleted |
Boolean | Tombstone flag. |
metadata |
Map | Flexible standard JSON map for extra fields. |
Example:
{
"id": "t1",
"name": "Planning",
"start": "2023-01-01T09:00:00.000Z",
"end": "2023-01-05T17:00:00.000Z",
"isDeleted": false,
"metadata": {
"color": "#FF0000"
}
}| Field | Type | Description |
|---|---|---|
predecessorTaskId |
String | The task that comes first. |
successorTaskId |
String | The task that follows. |
type |
String | Enum: finishToStart, startToStart, finishToFinish, startToFinish. |
lag |
Int? | Lag in milliseconds (optional). |
Example:
{
"predecessorTaskId": "t1",
"successorTaskId": "t2",
"type": "finishToStart",
"lag": 3600000 // 1 hour
}| Field | Type | Description |
|---|---|---|
id |
String | Unique UUID. |
name |
String | Resource name (e.g., "Alice"). |
type |
String | "person", "machine", etc. |
| Field | Type | Description |
|---|---|---|
id |
String | Unique UUID. |
name |
String | Tag label (e.g., "Baseline Q1"). |
merkleRoot |
String | The Merkle Root of the state at the time of tagging. |
timestamp |
HLC | When the tag was created. |
actorId |
String? | Who created the tag. |
isDeleted |
Boolean | Tombstone flag. |
metadata |
Map | Extra fields. |
To verify state consistency, clients compute a Merkle Root.
The content hash of a Task/Resource/Dependency is the SHA-256 hash of its deterministic JSON representation.
- Create a JSON object containing all relevant fields.
- Sort keys alphabetically to ensure deterministic serialization (canonical JSON).
- Compute SHA-256 of the UTF-8 bytes.
- Collect all content hashes for active (non-deleted) items.
- Sort hashes alphabetically.
- Combine them into a Merkle Tree structure (implementation specific, but standard binary tree).
- The root hash is the "Version State".
While the data format is transport-agnostic, the standard real-time implementation uses WebSockets.
- Connect: Client connects to the WebSocket endpoint (e.g.,
ws://server/). - Subscribe: Client sends a subscription message to join a specific channel (tenant/project).
{
"type": "subscribe",
"channel": "tenant-123"
}- Confirmation: Server responds with
SUBSCRIBE_SUCCESS.- CRITICAL: This response MUST NOT include
timestamporactorIdfields. The presence of these fields causes clients to interpret the message as a CRDT operation rather than a control message.
- CRITICAL: This response MUST NOT include
{
"type": "SUBSCRIBE_SUCCESS",
"channel": "tenant-123"
}- GET_MERKLE_ROOT: Client requests the current server-side Merkle Root.
{ "type": "GET_MERKLE_ROOT" }- MERKLE_ROOT: Server response containing the root hash.
{
"type": "MERKLE_ROOT",
"data": {
"root": "hash-abc-123"
},
"timestamp": "...",
"actorId": "server"
}