@git-stunts/git-warp 10.3.2 → 10.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -3
- package/SECURITY.md +89 -1
- package/bin/warp-graph.js +574 -208
- package/index.d.ts +55 -0
- package/index.js +4 -0
- package/package.json +8 -4
- package/src/domain/WarpGraph.js +334 -161
- package/src/domain/crdt/LWW.js +1 -1
- package/src/domain/crdt/ORSet.js +10 -6
- package/src/domain/crdt/VersionVector.js +5 -1
- package/src/domain/errors/EmptyMessageError.js +2 -4
- package/src/domain/errors/ForkError.js +4 -0
- package/src/domain/errors/IndexError.js +4 -0
- package/src/domain/errors/OperationAbortedError.js +4 -0
- package/src/domain/errors/QueryError.js +4 -0
- package/src/domain/errors/SchemaUnsupportedError.js +4 -0
- package/src/domain/errors/ShardCorruptionError.js +2 -6
- package/src/domain/errors/ShardLoadError.js +2 -6
- package/src/domain/errors/ShardValidationError.js +2 -7
- package/src/domain/errors/StorageError.js +2 -6
- package/src/domain/errors/SyncError.js +4 -0
- package/src/domain/errors/TraversalError.js +4 -0
- package/src/domain/errors/WarpError.js +2 -4
- package/src/domain/errors/WormholeError.js +4 -0
- package/src/domain/services/AnchorMessageCodec.js +1 -4
- package/src/domain/services/BitmapIndexBuilder.js +10 -6
- package/src/domain/services/BitmapIndexReader.js +27 -21
- package/src/domain/services/BoundaryTransitionRecord.js +22 -15
- package/src/domain/services/CheckpointMessageCodec.js +1 -7
- package/src/domain/services/CheckpointSerializerV5.js +20 -19
- package/src/domain/services/CheckpointService.js +18 -18
- package/src/domain/services/CommitDagTraversalService.js +13 -1
- package/src/domain/services/DagPathFinding.js +40 -18
- package/src/domain/services/DagTopology.js +7 -6
- package/src/domain/services/DagTraversal.js +5 -3
- package/src/domain/services/Frontier.js +7 -6
- package/src/domain/services/HealthCheckService.js +15 -14
- package/src/domain/services/HookInstaller.js +64 -13
- package/src/domain/services/HttpSyncServer.js +88 -19
- package/src/domain/services/IndexRebuildService.js +12 -12
- package/src/domain/services/IndexStalenessChecker.js +13 -6
- package/src/domain/services/JoinReducer.js +28 -27
- package/src/domain/services/LogicalTraversal.js +7 -6
- package/src/domain/services/MessageCodecInternal.js +2 -0
- package/src/domain/services/ObserverView.js +6 -6
- package/src/domain/services/PatchBuilderV2.js +9 -9
- package/src/domain/services/PatchMessageCodec.js +1 -7
- package/src/domain/services/ProvenanceIndex.js +6 -8
- package/src/domain/services/ProvenancePayload.js +1 -2
- package/src/domain/services/QueryBuilder.js +29 -23
- package/src/domain/services/StateDiff.js +7 -7
- package/src/domain/services/StateSerializerV5.js +8 -6
- package/src/domain/services/StreamingBitmapIndexBuilder.js +29 -23
- package/src/domain/services/SyncAuthService.js +396 -0
- package/src/domain/services/SyncProtocol.js +23 -26
- package/src/domain/services/TemporalQuery.js +4 -3
- package/src/domain/services/TranslationCost.js +4 -4
- package/src/domain/services/WormholeService.js +19 -15
- package/src/domain/types/TickReceipt.js +10 -6
- package/src/domain/types/WarpTypesV2.js +2 -3
- package/src/domain/utils/CachedValue.js +1 -1
- package/src/domain/utils/LRUCache.js +3 -3
- package/src/domain/utils/MinHeap.js +2 -2
- package/src/domain/utils/RefLayout.js +19 -0
- package/src/domain/utils/WriterId.js +2 -2
- package/src/domain/utils/defaultCodec.js +9 -2
- package/src/domain/utils/defaultCrypto.js +36 -0
- package/src/domain/utils/roaring.js +5 -5
- package/src/domain/utils/seekCacheKey.js +32 -0
- package/src/domain/warp/PatchSession.js +3 -3
- package/src/domain/warp/Writer.js +2 -2
- package/src/infrastructure/adapters/BunHttpAdapter.js +21 -8
- package/src/infrastructure/adapters/CasSeekCacheAdapter.js +311 -0
- package/src/infrastructure/adapters/ClockAdapter.js +2 -2
- package/src/infrastructure/adapters/DenoHttpAdapter.js +22 -9
- package/src/infrastructure/adapters/GitGraphAdapter.js +25 -83
- package/src/infrastructure/adapters/InMemoryGraphAdapter.js +488 -0
- package/src/infrastructure/adapters/NodeCryptoAdapter.js +16 -3
- package/src/infrastructure/adapters/NodeHttpAdapter.js +33 -11
- package/src/infrastructure/adapters/WebCryptoAdapter.js +21 -11
- package/src/infrastructure/adapters/adapterValidation.js +90 -0
- package/src/infrastructure/codecs/CborCodec.js +16 -8
- package/src/ports/BlobPort.js +2 -2
- package/src/ports/CodecPort.js +2 -2
- package/src/ports/CommitPort.js +8 -21
- package/src/ports/ConfigPort.js +3 -3
- package/src/ports/CryptoPort.js +7 -7
- package/src/ports/GraphPersistencePort.js +12 -14
- package/src/ports/HttpServerPort.js +1 -5
- package/src/ports/IndexStoragePort.js +1 -0
- package/src/ports/LoggerPort.js +9 -9
- package/src/ports/RefPort.js +5 -5
- package/src/ports/SeekCachePort.js +73 -0
- package/src/ports/TreePort.js +3 -3
- package/src/visualization/layouts/converters.js +14 -7
- package/src/visualization/layouts/elkAdapter.js +17 -4
- package/src/visualization/layouts/elkLayout.js +23 -7
- package/src/visualization/layouts/index.js +3 -3
- package/src/visualization/renderers/ascii/check.js +30 -17
- package/src/visualization/renderers/ascii/graph.js +92 -1
- package/src/visualization/renderers/ascii/history.js +28 -26
- package/src/visualization/renderers/ascii/info.js +9 -7
- package/src/visualization/renderers/ascii/materialize.js +20 -16
- package/src/visualization/renderers/ascii/opSummary.js +15 -7
- package/src/visualization/renderers/ascii/path.js +1 -1
- package/src/visualization/renderers/ascii/seek.js +187 -23
- package/src/visualization/renderers/ascii/table.js +1 -1
- package/src/visualization/renderers/svg/index.js +5 -1
|
@@ -2,6 +2,16 @@
|
|
|
2
2
|
* ELK adapter: converts normalised graph data into ELK JSON input.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* @typedef {{ id: string, label: string, props?: Record<string, any> }} GraphDataNode
|
|
7
|
+
* @typedef {{ from: string, to: string, label?: string }} GraphDataEdge
|
|
8
|
+
* @typedef {{ nodes: GraphDataNode[], edges: GraphDataEdge[] }} GraphData
|
|
9
|
+
* @typedef {{ text: string }} ElkLabel
|
|
10
|
+
* @typedef {{ id: string, sources: string[], targets: string[], labels?: ElkLabel[] }} ElkEdge
|
|
11
|
+
* @typedef {{ id: string, width: number, height: number, labels: ElkLabel[] }} ElkChild
|
|
12
|
+
* @typedef {{ id: string, layoutOptions: Record<string, string>, children: ElkChild[], edges: ElkEdge[] }} ElkGraph
|
|
13
|
+
*/
|
|
14
|
+
|
|
5
15
|
const LAYOUT_PRESETS = {
|
|
6
16
|
query: {
|
|
7
17
|
'elk.algorithm': 'layered',
|
|
@@ -29,7 +39,7 @@ const DEFAULT_PRESET = LAYOUT_PRESETS.query;
|
|
|
29
39
|
* Returns ELK layout options for a given visualisation type.
|
|
30
40
|
*
|
|
31
41
|
* @param {'query'|'path'|'slice'} type
|
|
32
|
-
* @returns {
|
|
42
|
+
* @returns {Record<string, string>} ELK layout options
|
|
33
43
|
*/
|
|
34
44
|
export function getDefaultLayoutOptions(type) {
|
|
35
45
|
return LAYOUT_PRESETS[type] ?? DEFAULT_PRESET;
|
|
@@ -38,6 +48,8 @@ export function getDefaultLayoutOptions(type) {
|
|
|
38
48
|
/**
|
|
39
49
|
* Estimates pixel width for a node label.
|
|
40
50
|
* Approximates monospace glyph width at ~9px with 24px padding.
|
|
51
|
+
* @param {string | undefined} label
|
|
52
|
+
* @returns {number}
|
|
41
53
|
*/
|
|
42
54
|
function estimateNodeWidth(label) {
|
|
43
55
|
const charWidth = 9;
|
|
@@ -51,9 +63,9 @@ const NODE_HEIGHT = 30;
|
|
|
51
63
|
/**
|
|
52
64
|
* Converts normalised graph data to an ELK graph JSON object.
|
|
53
65
|
*
|
|
54
|
-
* @param {
|
|
55
|
-
* @param {{ type?:
|
|
56
|
-
* @returns {
|
|
66
|
+
* @param {GraphData} graphData
|
|
67
|
+
* @param {{ type?: 'query'|'path'|'slice', layoutOptions?: Record<string, string> }} [options]
|
|
68
|
+
* @returns {ElkGraph} ELK-format graph
|
|
57
69
|
*/
|
|
58
70
|
export function toElkGraph(graphData, options = {}) {
|
|
59
71
|
const { type = 'query', layoutOptions } = options;
|
|
@@ -66,6 +78,7 @@ export function toElkGraph(graphData, options = {}) {
|
|
|
66
78
|
}));
|
|
67
79
|
|
|
68
80
|
const edges = (graphData.edges ?? []).map((e, i) => {
|
|
81
|
+
/** @type {ElkEdge} */
|
|
69
82
|
const edge = {
|
|
70
83
|
id: `e${i}`,
|
|
71
84
|
sources: [e.from],
|
|
@@ -5,11 +5,22 @@
|
|
|
5
5
|
* a layout is actually requested, keeping normal CLI startup fast.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
/**
|
|
9
|
+
* @typedef {{ id: string, x?: number, y?: number, width?: number, height?: number, labels?: Array<{ text: string }> }} ElkResultChild
|
|
10
|
+
* @typedef {{ id: string, sources?: string[], targets?: string[], labels?: Array<{ text: string }>, sections?: any[] }} ElkResultEdge
|
|
11
|
+
* @typedef {{ children?: ElkResultChild[], edges?: ElkResultEdge[], width?: number, height?: number }} ElkResult
|
|
12
|
+
* @typedef {{ id: string, x: number, y: number, width: number, height: number, label: string }} PosNode
|
|
13
|
+
* @typedef {{ id: string, source: string, target: string, label?: string, sections: any[] }} PosEdge
|
|
14
|
+
* @typedef {{ nodes: PosNode[], edges: PosEdge[], width: number, height: number }} PositionedGraph
|
|
15
|
+
* @typedef {{ id: string, children?: Array<{ id: string, width?: number, height?: number, labels?: Array<{ text: string }> }>, edges?: Array<{ id: string, sources?: string[], targets?: string[], labels?: Array<{ text: string }> }>, layoutOptions?: Record<string, string> }} ElkGraphInput
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/** @type {Promise<any> | null} */
|
|
8
19
|
let elkPromise = null;
|
|
9
20
|
|
|
10
21
|
/**
|
|
11
22
|
* Returns (or creates) a singleton ELK instance.
|
|
12
|
-
* @returns {Promise<
|
|
23
|
+
* @returns {Promise<any>} ELK instance
|
|
13
24
|
*/
|
|
14
25
|
function getElk() {
|
|
15
26
|
if (!elkPromise) {
|
|
@@ -21,10 +32,11 @@ function getElk() {
|
|
|
21
32
|
/**
|
|
22
33
|
* Runs ELK layout on a graph and returns a PositionedGraph.
|
|
23
34
|
*
|
|
24
|
-
* @param {
|
|
25
|
-
* @returns {Promise<
|
|
35
|
+
* @param {ElkGraphInput} elkGraph - ELK-format graph from toElkGraph()
|
|
36
|
+
* @returns {Promise<PositionedGraph>} PositionedGraph
|
|
26
37
|
*/
|
|
27
38
|
export async function runLayout(elkGraph) {
|
|
39
|
+
/** @type {ElkResult | undefined} */
|
|
28
40
|
let result;
|
|
29
41
|
try {
|
|
30
42
|
const elk = await getElk();
|
|
@@ -37,9 +49,11 @@ export async function runLayout(elkGraph) {
|
|
|
37
49
|
|
|
38
50
|
/**
|
|
39
51
|
* Converts ELK output to a PositionedGraph.
|
|
52
|
+
* @param {ElkResult | undefined} result
|
|
53
|
+
* @returns {PositionedGraph}
|
|
40
54
|
*/
|
|
41
55
|
function toPositionedGraph(result) {
|
|
42
|
-
const nodes = (result
|
|
56
|
+
const nodes = (result?.children ?? []).map((c) => ({
|
|
43
57
|
id: c.id,
|
|
44
58
|
x: c.x ?? 0,
|
|
45
59
|
y: c.y ?? 0,
|
|
@@ -48,7 +62,7 @@ function toPositionedGraph(result) {
|
|
|
48
62
|
label: c.labels?.[0]?.text ?? c.id,
|
|
49
63
|
}));
|
|
50
64
|
|
|
51
|
-
const edges = (result
|
|
65
|
+
const edges = (result?.edges ?? []).map((e) => ({
|
|
52
66
|
id: e.id,
|
|
53
67
|
source: e.sources?.[0] ?? '',
|
|
54
68
|
target: e.targets?.[0] ?? '',
|
|
@@ -59,13 +73,15 @@ function toPositionedGraph(result) {
|
|
|
59
73
|
return {
|
|
60
74
|
nodes,
|
|
61
75
|
edges,
|
|
62
|
-
width: result
|
|
63
|
-
height: result
|
|
76
|
+
width: result?.width ?? 0,
|
|
77
|
+
height: result?.height ?? 0,
|
|
64
78
|
};
|
|
65
79
|
}
|
|
66
80
|
|
|
67
81
|
/**
|
|
68
82
|
* Fallback: line nodes up horizontally when ELK fails.
|
|
83
|
+
* @param {ElkGraphInput} elkGraph
|
|
84
|
+
* @returns {PositionedGraph}
|
|
69
85
|
*/
|
|
70
86
|
function fallbackLayout(elkGraph) {
|
|
71
87
|
let x = 20;
|
|
@@ -19,9 +19,9 @@ import { runLayout } from './elkLayout.js';
|
|
|
19
19
|
/**
|
|
20
20
|
* Full pipeline: graphData → PositionedGraph.
|
|
21
21
|
*
|
|
22
|
-
* @param {{ nodes: Array, edges: Array }} graphData - Normalised graph data
|
|
23
|
-
* @param {{ type?:
|
|
24
|
-
* @returns {Promise<
|
|
22
|
+
* @param {{ nodes: Array<{ id: string, label: string }>, edges: Array<{ from: string, to: string, label?: string }> }} graphData - Normalised graph data
|
|
23
|
+
* @param {{ type?: 'query'|'path'|'slice', layoutOptions?: Record<string, string> }} [options]
|
|
24
|
+
* @returns {Promise<{ nodes: any[], edges: any[], width: number, height: number }>} PositionedGraph
|
|
25
25
|
*/
|
|
26
26
|
export async function layoutGraph(graphData, options = {}) {
|
|
27
27
|
const elkGraph = toElkGraph(graphData, options);
|
|
@@ -12,6 +12,18 @@ import { colors } from './colors.js';
|
|
|
12
12
|
import { padRight } from '../../utils/unicode.js';
|
|
13
13
|
import { formatAge } from './formatters.js';
|
|
14
14
|
|
|
15
|
+
/**
|
|
16
|
+
* @typedef {{ cachedState?: string, tombstoneRatio?: number, patchesSinceCheckpoint?: number }} CheckStatus
|
|
17
|
+
* @typedef {{ writerId?: string, sha?: string }} WriterHead
|
|
18
|
+
* @typedef {{ sha?: string, ageSeconds?: number | null }} CheckpointInfo
|
|
19
|
+
* @typedef {{ installed?: boolean, foreign?: boolean, current?: boolean, version?: string }} HookInfo
|
|
20
|
+
* @typedef {{ sha?: string, missingWriters?: string[] }} CoverageInfo
|
|
21
|
+
* @typedef {{ status?: string }} HealthInfo
|
|
22
|
+
* @typedef {{ tombstoneRatio?: number }} GCInfo
|
|
23
|
+
* @typedef {{ heads?: WriterHead[] }} WritersInfo
|
|
24
|
+
* @typedef {{ graph: string, health: HealthInfo, status: CheckStatus, writers: WritersInfo, checkpoint: CheckpointInfo, coverage: CoverageInfo, gc: GCInfo, hook: HookInfo | null }} CheckPayload
|
|
25
|
+
*/
|
|
26
|
+
|
|
15
27
|
// Health thresholds
|
|
16
28
|
const TOMBSTONE_HEALTHY_MAX = 0.15; // < 15% tombstones = healthy
|
|
17
29
|
const TOMBSTONE_WARNING_MAX = 0.30; // < 30% tombstones = warning
|
|
@@ -19,7 +31,7 @@ const CACHE_STALE_PENALTY = 20; // Reduce "freshness" score for stale ca
|
|
|
19
31
|
|
|
20
32
|
/**
|
|
21
33
|
* Get cache freshness percentage and state.
|
|
22
|
-
* @param {
|
|
34
|
+
* @param {CheckStatus | null} status - The status object from check payload
|
|
23
35
|
* @returns {{ percent: number, label: string }}
|
|
24
36
|
*/
|
|
25
37
|
function getCacheFreshness(status) {
|
|
@@ -78,7 +90,7 @@ function tombstoneBar(percent, width = 20) {
|
|
|
78
90
|
|
|
79
91
|
/**
|
|
80
92
|
* Format writer information for display.
|
|
81
|
-
* @param {
|
|
93
|
+
* @param {WriterHead[] | undefined} heads - Writer heads array
|
|
82
94
|
* @returns {string}
|
|
83
95
|
*/
|
|
84
96
|
function formatWriters(heads) {
|
|
@@ -94,7 +106,7 @@ function formatWriters(heads) {
|
|
|
94
106
|
|
|
95
107
|
/**
|
|
96
108
|
* Format checkpoint status line.
|
|
97
|
-
* @param {
|
|
109
|
+
* @param {CheckpointInfo | null} checkpoint - Checkpoint info
|
|
98
110
|
* @returns {string}
|
|
99
111
|
*/
|
|
100
112
|
function formatCheckpoint(checkpoint) {
|
|
@@ -103,13 +115,14 @@ function formatCheckpoint(checkpoint) {
|
|
|
103
115
|
}
|
|
104
116
|
|
|
105
117
|
const sha = colors.muted(checkpoint.sha.slice(0, 7));
|
|
106
|
-
const
|
|
118
|
+
const ageSeconds = checkpoint.ageSeconds ?? null;
|
|
119
|
+
const age = formatAge(ageSeconds);
|
|
107
120
|
|
|
108
121
|
// Add checkmark for recent checkpoints (< 5 min), warning for older
|
|
109
122
|
let status;
|
|
110
|
-
if (
|
|
123
|
+
if (ageSeconds !== null && ageSeconds < 300) {
|
|
111
124
|
status = colors.success('\u2713');
|
|
112
|
-
} else if (
|
|
125
|
+
} else if (ageSeconds !== null && ageSeconds < 3600) {
|
|
113
126
|
status = colors.warning('\u2713');
|
|
114
127
|
} else {
|
|
115
128
|
status = colors.muted('\u2713');
|
|
@@ -120,7 +133,7 @@ function formatCheckpoint(checkpoint) {
|
|
|
120
133
|
|
|
121
134
|
/**
|
|
122
135
|
* Format hook status line.
|
|
123
|
-
* @param {
|
|
136
|
+
* @param {HookInfo|null} hook - Hook status
|
|
124
137
|
* @returns {string}
|
|
125
138
|
*/
|
|
126
139
|
function formatHook(hook) {
|
|
@@ -145,7 +158,7 @@ function formatHook(hook) {
|
|
|
145
158
|
|
|
146
159
|
/**
|
|
147
160
|
* Format coverage status line.
|
|
148
|
-
* @param {
|
|
161
|
+
* @param {CoverageInfo | null} coverage - Coverage info
|
|
149
162
|
* @returns {string}
|
|
150
163
|
*/
|
|
151
164
|
function formatCoverage(coverage) {
|
|
@@ -164,7 +177,7 @@ function formatCoverage(coverage) {
|
|
|
164
177
|
|
|
165
178
|
/**
|
|
166
179
|
* Get overall health status with color and symbol.
|
|
167
|
-
* @param {
|
|
180
|
+
* @param {HealthInfo | null} health - Health object
|
|
168
181
|
* @returns {{ text: string, symbol: string, color: Function }}
|
|
169
182
|
*/
|
|
170
183
|
function getOverallHealth(health) {
|
|
@@ -190,8 +203,8 @@ function getOverallHealth(health) {
|
|
|
190
203
|
|
|
191
204
|
/**
|
|
192
205
|
* Build the state section lines (cache, tombstones, patches).
|
|
193
|
-
* @param {
|
|
194
|
-
* @param {
|
|
206
|
+
* @param {CheckStatus | null} status - Status object
|
|
207
|
+
* @param {GCInfo | null} gc - GC metrics
|
|
195
208
|
* @returns {string[]}
|
|
196
209
|
*/
|
|
197
210
|
function buildStateLines(status, gc) {
|
|
@@ -215,10 +228,10 @@ function buildStateLines(status, gc) {
|
|
|
215
228
|
/**
|
|
216
229
|
* Build the metadata section lines (writers, checkpoint, coverage, hooks).
|
|
217
230
|
* @param {Object} opts - Metadata options
|
|
218
|
-
* @param {
|
|
219
|
-
* @param {
|
|
220
|
-
* @param {
|
|
221
|
-
* @param {
|
|
231
|
+
* @param {WritersInfo} opts.writers - Writers info
|
|
232
|
+
* @param {CheckpointInfo} opts.checkpoint - Checkpoint info
|
|
233
|
+
* @param {CoverageInfo} opts.coverage - Coverage info
|
|
234
|
+
* @param {HookInfo | null} opts.hook - Hook status
|
|
222
235
|
* @returns {string[]}
|
|
223
236
|
*/
|
|
224
237
|
function buildMetadataLines({ writers, checkpoint, coverage, hook }) {
|
|
@@ -232,7 +245,7 @@ function buildMetadataLines({ writers, checkpoint, coverage, hook }) {
|
|
|
232
245
|
|
|
233
246
|
/**
|
|
234
247
|
* Determine border color based on health status.
|
|
235
|
-
* @param {
|
|
248
|
+
* @param {{ text: string, symbol: string, color: Function }} overall - Overall health info
|
|
236
249
|
* @returns {string}
|
|
237
250
|
*/
|
|
238
251
|
function getBorderColor(overall) {
|
|
@@ -243,7 +256,7 @@ function getBorderColor(overall) {
|
|
|
243
256
|
|
|
244
257
|
/**
|
|
245
258
|
* Render the check view dashboard.
|
|
246
|
-
* @param {
|
|
259
|
+
* @param {CheckPayload} payload - The check command payload
|
|
247
260
|
* @returns {string} Formatted dashboard string
|
|
248
261
|
*/
|
|
249
262
|
export function renderCheckView(payload) {
|
|
@@ -11,6 +11,14 @@ import { createBox } from './box.js';
|
|
|
11
11
|
import { colors } from './colors.js';
|
|
12
12
|
import { ARROW } from './symbols.js';
|
|
13
13
|
|
|
14
|
+
/**
|
|
15
|
+
* @typedef {{ x: number, y: number }} Point
|
|
16
|
+
* @typedef {{ startPoint?: Point, endPoint?: Point, bendPoints?: Point[] }} Section
|
|
17
|
+
* @typedef {{ id: string, x: number, y: number, width: number, height: number, label?: string }} PositionedNode
|
|
18
|
+
* @typedef {{ id: string, source: string, target: string, label?: string, sections?: Section[] }} PositionedEdge
|
|
19
|
+
* @typedef {{ nodes: PositionedNode[], edges: PositionedEdge[], width: number, height: number }} PositionedGraph
|
|
20
|
+
*/
|
|
21
|
+
|
|
14
22
|
// ── Scaling constants ────────────────────────────────────────────────────────
|
|
15
23
|
|
|
16
24
|
const CELL_W = 10;
|
|
@@ -30,23 +38,33 @@ const BOX = {
|
|
|
30
38
|
|
|
31
39
|
// ── Grid helpers ─────────────────────────────────────────────────────────────
|
|
32
40
|
|
|
41
|
+
/** @param {number} px */
|
|
33
42
|
function toCol(px) {
|
|
34
43
|
return Math.round(px / CELL_W) + MARGIN;
|
|
35
44
|
}
|
|
36
45
|
|
|
46
|
+
/** @param {number} px */
|
|
37
47
|
function toRow(px) {
|
|
38
48
|
return Math.round(px / CELL_H) + MARGIN;
|
|
39
49
|
}
|
|
40
50
|
|
|
51
|
+
/** @param {number} px */
|
|
41
52
|
function scaleW(px) {
|
|
42
53
|
return Math.round(px / CELL_W);
|
|
43
54
|
}
|
|
44
55
|
|
|
56
|
+
/** @param {number} px */
|
|
45
57
|
function scaleH(px) {
|
|
46
58
|
return Math.round(px / CELL_H);
|
|
47
59
|
}
|
|
48
60
|
|
|
61
|
+
/**
|
|
62
|
+
* @param {number} rows
|
|
63
|
+
* @param {number} cols
|
|
64
|
+
* @returns {string[][]}
|
|
65
|
+
*/
|
|
49
66
|
function createGrid(rows, cols) {
|
|
67
|
+
/** @type {string[][]} */
|
|
50
68
|
const grid = [];
|
|
51
69
|
for (let r = 0; r < rows; r++) {
|
|
52
70
|
grid.push(new Array(cols).fill(' '));
|
|
@@ -54,12 +72,24 @@ function createGrid(rows, cols) {
|
|
|
54
72
|
return grid;
|
|
55
73
|
}
|
|
56
74
|
|
|
75
|
+
/**
|
|
76
|
+
* @param {string[][]} grid
|
|
77
|
+
* @param {number} r
|
|
78
|
+
* @param {number} c
|
|
79
|
+
* @param {string} ch
|
|
80
|
+
*/
|
|
57
81
|
function writeChar(grid, r, c, ch) {
|
|
58
82
|
if (r >= 0 && r < grid.length && c >= 0 && c < grid[0].length) {
|
|
59
83
|
grid[r][c] = ch;
|
|
60
84
|
}
|
|
61
85
|
}
|
|
62
86
|
|
|
87
|
+
/**
|
|
88
|
+
* @param {string[][]} grid
|
|
89
|
+
* @param {number} r
|
|
90
|
+
* @param {number} c
|
|
91
|
+
* @returns {string}
|
|
92
|
+
*/
|
|
63
93
|
function readChar(grid, r, c) {
|
|
64
94
|
if (r >= 0 && r < grid.length && c >= 0 && c < grid[0].length) {
|
|
65
95
|
return grid[r][c];
|
|
@@ -67,6 +97,12 @@ function readChar(grid, r, c) {
|
|
|
67
97
|
return ' ';
|
|
68
98
|
}
|
|
69
99
|
|
|
100
|
+
/**
|
|
101
|
+
* @param {string[][]} grid
|
|
102
|
+
* @param {number} r
|
|
103
|
+
* @param {number} c
|
|
104
|
+
* @param {string} str
|
|
105
|
+
*/
|
|
70
106
|
function writeString(grid, r, c, str) {
|
|
71
107
|
for (let i = 0; i < str.length; i++) {
|
|
72
108
|
writeChar(grid, r, c + i, str[i]);
|
|
@@ -75,6 +111,10 @@ function writeString(grid, r, c, str) {
|
|
|
75
111
|
|
|
76
112
|
// ── Node stamping ────────────────────────────────────────────────────────────
|
|
77
113
|
|
|
114
|
+
/**
|
|
115
|
+
* @param {string[][]} grid
|
|
116
|
+
* @param {PositionedNode} node
|
|
117
|
+
*/
|
|
78
118
|
function stampNode(grid, node) {
|
|
79
119
|
const r = toRow(node.y);
|
|
80
120
|
const c = toCol(node.x);
|
|
@@ -112,6 +152,11 @@ function stampNode(grid, node) {
|
|
|
112
152
|
|
|
113
153
|
// ── Edge tracing ─────────────────────────────────────────────────────────────
|
|
114
154
|
|
|
155
|
+
/**
|
|
156
|
+
* @param {string[][]} grid
|
|
157
|
+
* @param {PositionedEdge} edge
|
|
158
|
+
* @param {Set<string>} nodeSet
|
|
159
|
+
*/
|
|
115
160
|
function traceEdge(grid, edge, nodeSet) {
|
|
116
161
|
const { sections } = edge;
|
|
117
162
|
if (!sections || sections.length === 0) {
|
|
@@ -136,6 +181,7 @@ function traceEdge(grid, edge, nodeSet) {
|
|
|
136
181
|
}
|
|
137
182
|
}
|
|
138
183
|
|
|
184
|
+
/** @param {Section} section @returns {Point[]} */
|
|
139
185
|
function buildPointList(section) {
|
|
140
186
|
const points = [];
|
|
141
187
|
if (section.startPoint) {
|
|
@@ -150,6 +196,11 @@ function buildPointList(section) {
|
|
|
150
196
|
return points;
|
|
151
197
|
}
|
|
152
198
|
|
|
199
|
+
/**
|
|
200
|
+
* @param {string[][]} grid
|
|
201
|
+
* @param {Point[]} points
|
|
202
|
+
* @param {Set<string>} nodeSet
|
|
203
|
+
*/
|
|
153
204
|
function drawSegments(grid, points, nodeSet) {
|
|
154
205
|
for (let i = 0; i < points.length - 1; i++) {
|
|
155
206
|
const r1 = toRow(points[i].y);
|
|
@@ -160,6 +211,14 @@ function drawSegments(grid, points, nodeSet) {
|
|
|
160
211
|
}
|
|
161
212
|
}
|
|
162
213
|
|
|
214
|
+
/**
|
|
215
|
+
* @param {string[][]} grid
|
|
216
|
+
* @param {number} r1
|
|
217
|
+
* @param {number} c1
|
|
218
|
+
* @param {number} r2
|
|
219
|
+
* @param {number} c2
|
|
220
|
+
* @param {Set<string>} nodeSet
|
|
221
|
+
*/
|
|
163
222
|
function drawLine(grid, r1, c1, r2, c2, nodeSet) {
|
|
164
223
|
if (r1 === r2) {
|
|
165
224
|
drawHorizontal(grid, r1, c1, c2, nodeSet);
|
|
@@ -172,6 +231,13 @@ function drawLine(grid, r1, c1, r2, c2, nodeSet) {
|
|
|
172
231
|
}
|
|
173
232
|
}
|
|
174
233
|
|
|
234
|
+
/**
|
|
235
|
+
* @param {string[][]} grid
|
|
236
|
+
* @param {number} row
|
|
237
|
+
* @param {number} c1
|
|
238
|
+
* @param {number} c2
|
|
239
|
+
* @param {Set<string>} nodeSet
|
|
240
|
+
*/
|
|
175
241
|
function drawHorizontal(grid, row, c1, c2, nodeSet) {
|
|
176
242
|
const start = Math.min(c1, c2);
|
|
177
243
|
const end = Math.max(c1, c2);
|
|
@@ -187,6 +253,13 @@ function drawHorizontal(grid, row, c1, c2, nodeSet) {
|
|
|
187
253
|
}
|
|
188
254
|
}
|
|
189
255
|
|
|
256
|
+
/**
|
|
257
|
+
* @param {string[][]} grid
|
|
258
|
+
* @param {number} col
|
|
259
|
+
* @param {number} r1
|
|
260
|
+
* @param {number} r2
|
|
261
|
+
* @param {Set<string>} nodeSet
|
|
262
|
+
*/
|
|
190
263
|
function drawVertical(grid, col, r1, r2, nodeSet) {
|
|
191
264
|
const start = Math.min(r1, r2);
|
|
192
265
|
const end = Math.max(r1, r2);
|
|
@@ -202,6 +275,11 @@ function drawVertical(grid, col, r1, r2, nodeSet) {
|
|
|
202
275
|
}
|
|
203
276
|
}
|
|
204
277
|
|
|
278
|
+
/**
|
|
279
|
+
* @param {string[][]} grid
|
|
280
|
+
* @param {Section} section
|
|
281
|
+
* @param {Set<string>} nodeSet
|
|
282
|
+
*/
|
|
205
283
|
function drawArrowhead(grid, section, nodeSet) {
|
|
206
284
|
const ep = section.endPoint;
|
|
207
285
|
if (!ep) {
|
|
@@ -250,6 +328,12 @@ function drawArrowhead(grid, section, nodeSet) {
|
|
|
250
328
|
}
|
|
251
329
|
}
|
|
252
330
|
|
|
331
|
+
/**
|
|
332
|
+
* @param {string[][]} grid
|
|
333
|
+
* @param {Section[]} sections
|
|
334
|
+
* @param {string} label
|
|
335
|
+
* @param {Set<string>} nodeSet
|
|
336
|
+
*/
|
|
253
337
|
function placeEdgeLabel(grid, sections, label, nodeSet) {
|
|
254
338
|
// Find midpoint of the full path
|
|
255
339
|
const allPoints = [];
|
|
@@ -292,6 +376,7 @@ function placeEdgeLabel(grid, sections, label, nodeSet) {
|
|
|
292
376
|
|
|
293
377
|
// ── Node occupancy set ───────────────────────────────────────────────────────
|
|
294
378
|
|
|
379
|
+
/** @param {PositionedNode[]} nodes @returns {Set<string>} */
|
|
295
380
|
function buildNodeSet(nodes) {
|
|
296
381
|
const set = new Set();
|
|
297
382
|
for (const node of nodes) {
|
|
@@ -308,6 +393,12 @@ function buildNodeSet(nodes) {
|
|
|
308
393
|
return set;
|
|
309
394
|
}
|
|
310
395
|
|
|
396
|
+
/**
|
|
397
|
+
* @param {Set<string>} nodeSet
|
|
398
|
+
* @param {number} r
|
|
399
|
+
* @param {number} c
|
|
400
|
+
* @returns {boolean}
|
|
401
|
+
*/
|
|
311
402
|
function isNodeCell(nodeSet, r, c) {
|
|
312
403
|
return nodeSet.has(`${r},${c}`);
|
|
313
404
|
}
|
|
@@ -317,7 +408,7 @@ function isNodeCell(nodeSet, r, c) {
|
|
|
317
408
|
/**
|
|
318
409
|
* Renders a PositionedGraph (from ELK) as an ASCII box-drawing string.
|
|
319
410
|
*
|
|
320
|
-
* @param {
|
|
411
|
+
* @param {PositionedGraph} positionedGraph - PositionedGraph from runLayout()
|
|
321
412
|
* @param {{ title?: string }} [options]
|
|
322
413
|
* @returns {string} Rendered ASCII art wrapped in a box
|
|
323
414
|
*/
|
|
@@ -9,13 +9,17 @@ import { padRight, padLeft } from '../../utils/unicode.js';
|
|
|
9
9
|
import { TIMELINE } from './symbols.js';
|
|
10
10
|
import { OP_DISPLAY, EMPTY_OP_SUMMARY, summarizeOps, formatOpSummary } from './opSummary.js';
|
|
11
11
|
|
|
12
|
+
/**
|
|
13
|
+
* @typedef {{ sha?: string, lamport?: number, writerId?: string, opSummary?: Record<string, number>, ops?: Array<{ type: string }> }} PatchEntry
|
|
14
|
+
*/
|
|
15
|
+
|
|
12
16
|
// Default pagination settings
|
|
13
17
|
const DEFAULT_PAGE_SIZE = 20;
|
|
14
18
|
|
|
15
19
|
/**
|
|
16
20
|
* Ensures entry has an opSummary, computing one if needed.
|
|
17
|
-
* @param {
|
|
18
|
-
* @returns {
|
|
21
|
+
* @param {PatchEntry} entry - Patch entry
|
|
22
|
+
* @returns {Record<string, number>} Operation summary
|
|
19
23
|
*/
|
|
20
24
|
function ensureOpSummary(entry) {
|
|
21
25
|
if (entry.opSummary) {
|
|
@@ -29,10 +33,10 @@ function ensureOpSummary(entry) {
|
|
|
29
33
|
|
|
30
34
|
/**
|
|
31
35
|
* Paginates entries, returning display entries and truncation info.
|
|
32
|
-
* @param {
|
|
36
|
+
* @param {PatchEntry[]} entries - All entries
|
|
33
37
|
* @param {number} pageSize - Page size
|
|
34
38
|
* @param {boolean} showAll - Whether to show all
|
|
35
|
-
* @returns {{displayEntries:
|
|
39
|
+
* @returns {{displayEntries: PatchEntry[], truncated: boolean, hiddenCount: number}}
|
|
36
40
|
*/
|
|
37
41
|
function paginateEntries(entries, pageSize, showAll) {
|
|
38
42
|
if (showAll || entries.length <= pageSize) {
|
|
@@ -64,17 +68,22 @@ function renderTruncationIndicator(truncated, hiddenCount) {
|
|
|
64
68
|
/**
|
|
65
69
|
* Renders a single patch entry line.
|
|
66
70
|
* @param {Object} params - Entry parameters
|
|
71
|
+
* @param {PatchEntry} params.entry - Patch entry
|
|
72
|
+
* @param {boolean} params.isLast - Whether this is the last entry
|
|
73
|
+
* @param {number} params.lamportWidth - Width for lamport timestamp padding
|
|
74
|
+
* @param {string} [params.writerStr] - Writer string
|
|
75
|
+
* @param {number} [params.maxWriterIdLen] - Max writer ID length for padding
|
|
67
76
|
* @returns {string} Formatted entry line
|
|
68
77
|
*/
|
|
69
78
|
function renderEntryLine({ entry, isLast, lamportWidth, writerStr, maxWriterIdLen }) {
|
|
70
79
|
const connector = isLast ? TIMELINE.end : TIMELINE.connector;
|
|
71
80
|
const shortSha = (entry.sha || '').slice(0, 7);
|
|
72
|
-
const lamportStr = padLeft(String(entry.lamport), lamportWidth);
|
|
81
|
+
const lamportStr = padLeft(String(entry.lamport ?? 0), lamportWidth);
|
|
73
82
|
const opSummary = ensureOpSummary(entry);
|
|
74
83
|
const opSummaryStr = formatOpSummary(opSummary, writerStr ? 30 : 40);
|
|
75
84
|
|
|
76
85
|
if (writerStr) {
|
|
77
|
-
const paddedWriter = padRight(writerStr, maxWriterIdLen);
|
|
86
|
+
const paddedWriter = padRight(writerStr, maxWriterIdLen ?? 6);
|
|
78
87
|
return ` ${connector}${TIMELINE.dot} ${colors.muted(`L${lamportStr}`)} ${colors.primary(paddedWriter)}:${colors.muted(shortSha)} ${opSummaryStr}`;
|
|
79
88
|
}
|
|
80
89
|
return ` ${connector}${TIMELINE.dot} ${colors.muted(`L${lamportStr}`)} ${colors.primary(shortSha)} ${opSummaryStr}`;
|
|
@@ -101,8 +110,8 @@ function renderSingleWriterFooter(totalCount) {
|
|
|
101
110
|
|
|
102
111
|
/**
|
|
103
112
|
* Renders single-writer timeline view.
|
|
104
|
-
* @param {
|
|
105
|
-
* @param {
|
|
113
|
+
* @param {{ entries: PatchEntry[], writer: string }} payload - History payload
|
|
114
|
+
* @param {{ pageSize?: number, showAll?: boolean }} options - Rendering options
|
|
106
115
|
* @returns {string[]} Lines for the timeline
|
|
107
116
|
*/
|
|
108
117
|
function renderSingleWriterTimeline(payload, options) {
|
|
@@ -121,7 +130,7 @@ function renderSingleWriterTimeline(payload, options) {
|
|
|
121
130
|
lines.push(colors.muted(' (no patches)'));
|
|
122
131
|
return lines;
|
|
123
132
|
}
|
|
124
|
-
const maxLamport = Math.max(...displayEntries.map((e) => e.lamport));
|
|
133
|
+
const maxLamport = Math.max(...displayEntries.map((e) => e.lamport ?? 0));
|
|
125
134
|
const lamportWidth = String(maxLamport).length;
|
|
126
135
|
|
|
127
136
|
lines.push(...renderTruncationIndicator(truncated, hiddenCount));
|
|
@@ -137,8 +146,8 @@ function renderSingleWriterTimeline(payload, options) {
|
|
|
137
146
|
|
|
138
147
|
/**
|
|
139
148
|
* Merges and sorts entries from all writers by lamport timestamp.
|
|
140
|
-
* @param {
|
|
141
|
-
* @returns {
|
|
149
|
+
* @param {Record<string, PatchEntry[]>} writers - Map of writerId to entries
|
|
150
|
+
* @returns {PatchEntry[]} Sorted entries with writerId attached
|
|
142
151
|
*/
|
|
143
152
|
function mergeWriterEntries(writers) {
|
|
144
153
|
const allEntries = [];
|
|
@@ -147,7 +156,7 @@ function mergeWriterEntries(writers) {
|
|
|
147
156
|
allEntries.push({ ...entry, writerId });
|
|
148
157
|
}
|
|
149
158
|
}
|
|
150
|
-
allEntries.sort((a, b) => a.lamport - b.lamport || a.writerId.localeCompare(b.writerId));
|
|
159
|
+
allEntries.sort((a, b) => (a.lamport ?? 0) - (b.lamport ?? 0) || (a.writerId ?? '').localeCompare(b.writerId ?? ''));
|
|
151
160
|
return allEntries;
|
|
152
161
|
}
|
|
153
162
|
|
|
@@ -178,8 +187,8 @@ function renderMultiWriterFooter(totalCount, writerCount) {
|
|
|
178
187
|
|
|
179
188
|
/**
|
|
180
189
|
* Renders multi-writer timeline view with parallel columns.
|
|
181
|
-
* @param {
|
|
182
|
-
* @param {
|
|
190
|
+
* @param {{ writers: Record<string, PatchEntry[]>, graph: string }} payload - History payload with allWriters data
|
|
191
|
+
* @param {{ pageSize?: number, showAll?: boolean }} options - Rendering options
|
|
183
192
|
* @returns {string[]} Lines for the timeline
|
|
184
193
|
*/
|
|
185
194
|
function renderMultiWriterTimeline(payload, options) {
|
|
@@ -206,7 +215,7 @@ function renderMultiWriterTimeline(payload, options) {
|
|
|
206
215
|
lines.push(colors.muted(' (no patches)'));
|
|
207
216
|
return lines;
|
|
208
217
|
}
|
|
209
|
-
const maxLamport = Math.max(...displayEntries.map((e) => e.lamport));
|
|
218
|
+
const maxLamport = Math.max(...displayEntries.map((e) => e.lamport ?? 0));
|
|
210
219
|
const lamportWidth = String(maxLamport).length;
|
|
211
220
|
const maxWriterIdLen = Math.max(...writerIds.map((id) => id.length), 6);
|
|
212
221
|
|
|
@@ -230,15 +239,8 @@ function renderMultiWriterTimeline(payload, options) {
|
|
|
230
239
|
|
|
231
240
|
/**
|
|
232
241
|
* Renders the history view with ASCII timeline.
|
|
233
|
-
* @param {
|
|
234
|
-
* @param {
|
|
235
|
-
* @param {string} [payload.writer] - Writer ID (single writer mode)
|
|
236
|
-
* @param {string|null} [payload.nodeFilter] - Node filter if applied
|
|
237
|
-
* @param {Object[]} [payload.entries] - Array of patch entries (single writer mode)
|
|
238
|
-
* @param {Object} [payload.writers] - Map of writerId to entries (multi-writer mode)
|
|
239
|
-
* @param {Object} [options] - Rendering options
|
|
240
|
-
* @param {number} [options.pageSize=20] - Number of patches to show per page
|
|
241
|
-
* @param {boolean} [options.showAll=false] - Show all patches (no pagination)
|
|
242
|
+
* @param {{ graph: string, writer?: string, nodeFilter?: string | null, entries?: PatchEntry[], writers?: Record<string, PatchEntry[]> }} payload - History payload from handleHistory
|
|
243
|
+
* @param {{ pageSize?: number, showAll?: boolean }} [options] - Rendering options
|
|
242
244
|
* @returns {string} Formatted ASCII output
|
|
243
245
|
*/
|
|
244
246
|
export function renderHistoryView(payload, options = {}) {
|
|
@@ -248,8 +250,8 @@ export function renderHistoryView(payload, options = {}) {
|
|
|
248
250
|
|
|
249
251
|
const isMultiWriter = payload.writers && typeof payload.writers === 'object';
|
|
250
252
|
const contentLines = isMultiWriter
|
|
251
|
-
? renderMultiWriterTimeline(payload, options)
|
|
252
|
-
: renderSingleWriterTimeline(payload, options);
|
|
253
|
+
? renderMultiWriterTimeline(/** @type {{ writers: Record<string, PatchEntry[]>, graph: string }} */ (payload), options)
|
|
254
|
+
: renderSingleWriterTimeline(/** @type {{ entries: PatchEntry[], writer: string }} */ (payload), options);
|
|
253
255
|
|
|
254
256
|
// Add node filter indicator if present
|
|
255
257
|
if (payload.nodeFilter) {
|