@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
|
@@ -10,6 +10,10 @@ import { padRight } from '../../utils/unicode.js';
|
|
|
10
10
|
import { timeAgo } from '../../utils/time.js';
|
|
11
11
|
import { TIMELINE } from './symbols.js';
|
|
12
12
|
|
|
13
|
+
/**
|
|
14
|
+
* @typedef {{ name: string, writers?: { count?: number, ids?: string[] }, checkpoint?: { sha?: string, date?: string | Date }, coverage?: { sha?: string }, writerPatches?: Record<string, number> }} GraphInfo
|
|
15
|
+
*/
|
|
16
|
+
|
|
13
17
|
// Box drawing characters (info.js uses verbose key names for card rendering)
|
|
14
18
|
const BOX = {
|
|
15
19
|
topLeft: '\u250C', // ┌
|
|
@@ -77,7 +81,7 @@ function formatWriterNames(writerIds) {
|
|
|
77
81
|
|
|
78
82
|
/**
|
|
79
83
|
* Renders the header lines for a graph card.
|
|
80
|
-
* @param {
|
|
84
|
+
* @param {GraphInfo} graph - Graph info object
|
|
81
85
|
* @param {number} contentWidth - Available content width
|
|
82
86
|
* @returns {string[]} Header lines
|
|
83
87
|
*/
|
|
@@ -102,7 +106,7 @@ function renderCardHeader(graph, contentWidth) {
|
|
|
102
106
|
|
|
103
107
|
/**
|
|
104
108
|
* Renders writer timeline lines for a graph card.
|
|
105
|
-
* @param {
|
|
109
|
+
* @param {Record<string, number> | undefined} writerPatches - Map of writerId to patch count
|
|
106
110
|
* @param {number} contentWidth - Available content width
|
|
107
111
|
* @returns {string[]} Timeline lines
|
|
108
112
|
*/
|
|
@@ -133,7 +137,7 @@ function renderWriterTimelines(writerPatches, contentWidth) {
|
|
|
133
137
|
|
|
134
138
|
/**
|
|
135
139
|
* Renders checkpoint and coverage lines for a graph card.
|
|
136
|
-
* @param {
|
|
140
|
+
* @param {GraphInfo} graph - Graph info object
|
|
137
141
|
* @param {number} contentWidth - Available content width
|
|
138
142
|
* @returns {string[]} Status lines
|
|
139
143
|
*/
|
|
@@ -169,7 +173,7 @@ function renderCardStatus(graph, contentWidth) {
|
|
|
169
173
|
|
|
170
174
|
/**
|
|
171
175
|
* Renders a single graph card.
|
|
172
|
-
* @param {
|
|
176
|
+
* @param {GraphInfo} graph - Graph info object
|
|
173
177
|
* @param {number} innerWidth - Available width inside the card
|
|
174
178
|
* @returns {string[]} Array of lines for this graph card
|
|
175
179
|
*/
|
|
@@ -186,9 +190,7 @@ function renderGraphCard(graph, innerWidth) {
|
|
|
186
190
|
|
|
187
191
|
/**
|
|
188
192
|
* Renders the info view with ASCII box art.
|
|
189
|
-
* @param {
|
|
190
|
-
* @param {string} data.repo - Repository path
|
|
191
|
-
* @param {Object[]} data.graphs - Array of graph info objects
|
|
193
|
+
* @param {{ repo?: string, graphs: GraphInfo[] }} data - Info payload from handleInfo
|
|
192
194
|
* @returns {string} Formatted ASCII output
|
|
193
195
|
*/
|
|
194
196
|
export function renderInfoView(data) {
|
|
@@ -12,6 +12,11 @@ import { padRight } from '../../utils/unicode.js';
|
|
|
12
12
|
import { truncate } from '../../utils/truncate.js';
|
|
13
13
|
import { formatNumber, formatSha } from './formatters.js';
|
|
14
14
|
|
|
15
|
+
/**
|
|
16
|
+
* @typedef {{ graph: string, error?: string, noOp?: boolean, patchCount?: number, nodes?: number, edges?: number, properties?: number, writers?: Record<string, number>, checkpoint?: string | null }} GraphResult
|
|
17
|
+
* @typedef {{ maxNodes: number, maxEdges: number, maxProps: number }} MaxValues
|
|
18
|
+
*/
|
|
19
|
+
|
|
15
20
|
// Bar chart settings
|
|
16
21
|
const BAR_WIDTH = 20;
|
|
17
22
|
const STAT_LABEL_WIDTH = 12;
|
|
@@ -55,15 +60,15 @@ function renderErrorState(errorMessage) {
|
|
|
55
60
|
|
|
56
61
|
/**
|
|
57
62
|
* Render no-op state (already materialized).
|
|
58
|
-
* @param {
|
|
63
|
+
* @param {GraphResult} graph - Graph data
|
|
59
64
|
* @returns {string[]} No-op state lines
|
|
60
65
|
*/
|
|
61
66
|
function renderNoOpState(graph) {
|
|
62
67
|
const lines = [
|
|
63
68
|
` ${colors.success('\u2713')} Already materialized (no new patches)`,
|
|
64
69
|
'',
|
|
65
|
-
` ${padRight('Nodes:', STAT_LABEL_WIDTH)} ${formatNumber(graph.nodes)}`,
|
|
66
|
-
` ${padRight('Edges:', STAT_LABEL_WIDTH)} ${formatNumber(graph.edges)}`,
|
|
70
|
+
` ${padRight('Nodes:', STAT_LABEL_WIDTH)} ${formatNumber(graph.nodes || 0)}`,
|
|
71
|
+
` ${padRight('Edges:', STAT_LABEL_WIDTH)} ${formatNumber(graph.edges || 0)}`,
|
|
67
72
|
];
|
|
68
73
|
if (typeof graph.properties === 'number') {
|
|
69
74
|
lines.push(` ${padRight('Properties:', STAT_LABEL_WIDTH)} ${formatNumber(graph.properties)}`);
|
|
@@ -73,7 +78,7 @@ function renderNoOpState(graph) {
|
|
|
73
78
|
|
|
74
79
|
/**
|
|
75
80
|
* Render empty graph state (0 patches).
|
|
76
|
-
* @param {
|
|
81
|
+
* @param {GraphResult} graph - Graph data
|
|
77
82
|
* @returns {string[]} Empty state lines
|
|
78
83
|
*/
|
|
79
84
|
function renderEmptyState(graph) {
|
|
@@ -86,7 +91,7 @@ function renderEmptyState(graph) {
|
|
|
86
91
|
|
|
87
92
|
/**
|
|
88
93
|
* Render writer progress section.
|
|
89
|
-
* @param {
|
|
94
|
+
* @param {Record<string, number> | undefined} writers - Writer patch counts
|
|
90
95
|
* @returns {string[]} Writer lines
|
|
91
96
|
*/
|
|
92
97
|
function renderWriterSection(writers) {
|
|
@@ -108,15 +113,15 @@ function renderWriterSection(writers) {
|
|
|
108
113
|
|
|
109
114
|
/**
|
|
110
115
|
* Render statistics section with bar charts.
|
|
111
|
-
* @param {
|
|
112
|
-
* @param {
|
|
116
|
+
* @param {GraphResult} graph - Graph data
|
|
117
|
+
* @param {MaxValues} maxValues - Max values for scaling
|
|
113
118
|
* @returns {string[]} Statistics lines
|
|
114
119
|
*/
|
|
115
120
|
function renderStatsSection(graph, { maxNodes, maxEdges, maxProps }) {
|
|
116
121
|
const lines = [
|
|
117
122
|
` ${colors.dim('Statistics:')}`,
|
|
118
|
-
` ${padRight('Nodes:', STAT_LABEL_WIDTH)} ${statBar(graph.nodes, maxNodes)} ${formatNumber(graph.nodes)}`,
|
|
119
|
-
` ${padRight('Edges:', STAT_LABEL_WIDTH)} ${statBar(graph.edges, maxEdges)} ${formatNumber(graph.edges)}`,
|
|
123
|
+
` ${padRight('Nodes:', STAT_LABEL_WIDTH)} ${statBar(graph.nodes || 0, maxNodes)} ${formatNumber(graph.nodes || 0)}`,
|
|
124
|
+
` ${padRight('Edges:', STAT_LABEL_WIDTH)} ${statBar(graph.edges || 0, maxEdges)} ${formatNumber(graph.edges || 0)}`,
|
|
120
125
|
];
|
|
121
126
|
if (typeof graph.properties === 'number') {
|
|
122
127
|
lines.push(` ${padRight('Properties:', STAT_LABEL_WIDTH)} ${statBar(graph.properties, maxProps)} ${formatNumber(graph.properties)}`);
|
|
@@ -139,8 +144,8 @@ function renderCheckpointInfo(checkpoint) {
|
|
|
139
144
|
|
|
140
145
|
/**
|
|
141
146
|
* Render a single graph's materialization result.
|
|
142
|
-
* @param {
|
|
143
|
-
* @param {
|
|
147
|
+
* @param {GraphResult} graph - Graph result from materialize
|
|
148
|
+
* @param {MaxValues} maxValues - Max values for scaling bars
|
|
144
149
|
* @returns {string[]} Array of lines for this graph
|
|
145
150
|
*/
|
|
146
151
|
function renderGraphResult(graph, maxValues) {
|
|
@@ -158,14 +163,14 @@ function renderGraphResult(graph, maxValues) {
|
|
|
158
163
|
|
|
159
164
|
lines.push(...renderWriterSection(graph.writers));
|
|
160
165
|
lines.push(...renderStatsSection(graph, maxValues));
|
|
161
|
-
lines.push(...renderCheckpointInfo(graph.checkpoint));
|
|
166
|
+
lines.push(...renderCheckpointInfo(graph.checkpoint ?? null));
|
|
162
167
|
return lines;
|
|
163
168
|
}
|
|
164
169
|
|
|
165
170
|
/**
|
|
166
171
|
* Calculate max values for scaling bar charts.
|
|
167
|
-
* @param {
|
|
168
|
-
* @returns {
|
|
172
|
+
* @param {GraphResult[]} graphs - Array of graph results
|
|
173
|
+
* @returns {MaxValues} Max values object
|
|
169
174
|
*/
|
|
170
175
|
function calculateMaxValues(graphs) {
|
|
171
176
|
const successfulGraphs = graphs.filter((g) => !g.error);
|
|
@@ -212,8 +217,7 @@ function getBorderColor(successCount, errorCount) {
|
|
|
212
217
|
|
|
213
218
|
/**
|
|
214
219
|
* Render the materialize view dashboard.
|
|
215
|
-
* @param {
|
|
216
|
-
* @param {Object[]} payload.graphs - Array of graph results
|
|
220
|
+
* @param {{ graphs: GraphResult[] }} payload - The materialize command payload
|
|
217
221
|
* @returns {string} Formatted dashboard string
|
|
218
222
|
*/
|
|
219
223
|
export function renderMaterializeView(payload) {
|
|
@@ -8,6 +8,11 @@
|
|
|
8
8
|
import { colors } from './colors.js';
|
|
9
9
|
import { truncate } from '../../utils/truncate.js';
|
|
10
10
|
|
|
11
|
+
/**
|
|
12
|
+
* @typedef {'NodeAdd' | 'EdgeAdd' | 'PropSet' | 'NodeTombstone' | 'EdgeTombstone' | 'BlobValue'} OpType
|
|
13
|
+
* @typedef {Record<OpType, number>} OpSummary
|
|
14
|
+
*/
|
|
15
|
+
|
|
11
16
|
// Operation type to display info mapping
|
|
12
17
|
export const OP_DISPLAY = Object.freeze({
|
|
13
18
|
NodeAdd: { symbol: '+', label: 'node', color: colors.success },
|
|
@@ -30,14 +35,16 @@ export const EMPTY_OP_SUMMARY = Object.freeze({
|
|
|
30
35
|
|
|
31
36
|
/**
|
|
32
37
|
* Summarizes operations in a patch.
|
|
33
|
-
* @param {
|
|
34
|
-
* @returns {
|
|
38
|
+
* @param {Array<{ type: string }>} ops - Array of patch operations
|
|
39
|
+
* @returns {OpSummary} Summary with counts by operation type
|
|
35
40
|
*/
|
|
36
41
|
export function summarizeOps(ops) {
|
|
42
|
+
/** @type {OpSummary} */
|
|
37
43
|
const summary = { ...EMPTY_OP_SUMMARY };
|
|
38
44
|
for (const op of ops) {
|
|
39
|
-
|
|
40
|
-
|
|
45
|
+
const t = /** @type {OpType} */ (op.type);
|
|
46
|
+
if (t && summary[t] !== undefined) {
|
|
47
|
+
summary[t]++;
|
|
41
48
|
}
|
|
42
49
|
}
|
|
43
50
|
return summary;
|
|
@@ -45,17 +52,18 @@ export function summarizeOps(ops) {
|
|
|
45
52
|
|
|
46
53
|
/**
|
|
47
54
|
* Formats operation summary as a colored string.
|
|
48
|
-
* @param {
|
|
55
|
+
* @param {OpSummary | Record<string, number>} summary - Operation counts by type
|
|
49
56
|
* @param {number} maxWidth - Maximum width for the summary string
|
|
50
57
|
* @returns {string} Formatted summary string
|
|
51
58
|
*/
|
|
52
59
|
export function formatOpSummary(summary, maxWidth = 40) {
|
|
60
|
+
/** @type {OpType[]} */
|
|
53
61
|
const order = ['NodeAdd', 'EdgeAdd', 'PropSet', 'NodeTombstone', 'EdgeTombstone', 'BlobValue'];
|
|
54
62
|
const parts = order
|
|
55
|
-
.filter((opType) => summary[opType] > 0)
|
|
63
|
+
.filter((opType) => (/** @type {Record<string, number>} */ (summary))[opType] > 0)
|
|
56
64
|
.map((opType) => {
|
|
57
65
|
const display = OP_DISPLAY[opType];
|
|
58
|
-
return { text: `${display.symbol}${summary[opType]}${display.label}`, color: display.color };
|
|
66
|
+
return { text: `${display.symbol}${(/** @type {Record<string, number>} */ (summary))[opType]}${display.label}`, color: display.color };
|
|
59
67
|
});
|
|
60
68
|
|
|
61
69
|
if (parts.length === 0) {
|
|
@@ -75,7 +75,7 @@ function createPathSegment({ nodeId, index, pathLength, edges }) {
|
|
|
75
75
|
* Builds path segments that fit within the terminal width.
|
|
76
76
|
* Wraps long paths to multiple lines.
|
|
77
77
|
* @param {string[]} path - Array of node IDs
|
|
78
|
-
* @param {string[]}
|
|
78
|
+
* @param {string[] | undefined} edges - Optional array of edge labels (one fewer than nodes)
|
|
79
79
|
* @param {number} maxWidth - Maximum line width
|
|
80
80
|
* @returns {string[]} Array of line strings
|
|
81
81
|
*/
|
|
@@ -15,6 +15,30 @@ import { formatSha, formatWriterName } from './formatters.js';
|
|
|
15
15
|
import { TIMELINE } from './symbols.js';
|
|
16
16
|
import { formatOpSummary } from './opSummary.js';
|
|
17
17
|
|
|
18
|
+
/**
|
|
19
|
+
* @typedef {{ ticks: number[], tipSha?: string, tickShas?: Record<number, string> }} WriterInfo
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @typedef {Object} SeekPayload
|
|
24
|
+
* @property {string} graph
|
|
25
|
+
* @property {number} tick
|
|
26
|
+
* @property {number} maxTick
|
|
27
|
+
* @property {number[]} ticks
|
|
28
|
+
* @property {number} nodes
|
|
29
|
+
* @property {number} edges
|
|
30
|
+
* @property {number} patchCount
|
|
31
|
+
* @property {Map<string, WriterInfo> | Record<string, WriterInfo>} perWriter
|
|
32
|
+
* @property {{ nodes?: number, edges?: number }} [diff]
|
|
33
|
+
* @property {Record<string, any>} [tickReceipt]
|
|
34
|
+
* @property {import('../../../domain/services/StateDiff.js').StateDiffResult | null} [structuralDiff]
|
|
35
|
+
* @property {string} [diffBaseline]
|
|
36
|
+
* @property {number | null} [baselineTick]
|
|
37
|
+
* @property {boolean} [truncated]
|
|
38
|
+
* @property {number} [totalChanges]
|
|
39
|
+
* @property {number} [shownChanges]
|
|
40
|
+
*/
|
|
41
|
+
|
|
18
42
|
/** Maximum number of tick columns shown in the windowed view. */
|
|
19
43
|
const MAX_COLS = 9;
|
|
20
44
|
|
|
@@ -30,6 +54,7 @@ const DOT_MID = '\u00B7'; // ·
|
|
|
30
54
|
/** Open circle used for excluded-zone patch markers. */
|
|
31
55
|
const CIRCLE_OPEN = '\u25CB'; // ○
|
|
32
56
|
|
|
57
|
+
/** @param {number} n @returns {string} */
|
|
33
58
|
function formatDelta(n) {
|
|
34
59
|
if (typeof n !== 'number' || !Number.isFinite(n) || n === 0) {
|
|
35
60
|
return '';
|
|
@@ -38,10 +63,17 @@ function formatDelta(n) {
|
|
|
38
63
|
return ` (${sign}${n})`;
|
|
39
64
|
}
|
|
40
65
|
|
|
66
|
+
/**
|
|
67
|
+
* @param {number} n
|
|
68
|
+
* @param {string} singular
|
|
69
|
+
* @param {string} plural
|
|
70
|
+
* @returns {string}
|
|
71
|
+
*/
|
|
41
72
|
function pluralize(n, singular, plural) {
|
|
42
73
|
return n === 1 ? singular : plural;
|
|
43
74
|
}
|
|
44
75
|
|
|
76
|
+
/** @param {Record<string, any> | undefined} tickReceipt @returns {string[]} */
|
|
45
77
|
function buildReceiptLines(tickReceipt) {
|
|
46
78
|
if (!tickReceipt || typeof tickReceipt !== 'object') {
|
|
47
79
|
return [];
|
|
@@ -200,7 +232,7 @@ function buildLane(patchSet, points, currentTick) {
|
|
|
200
232
|
*
|
|
201
233
|
* @param {Object} opts
|
|
202
234
|
* @param {string} opts.writerId
|
|
203
|
-
* @param {
|
|
235
|
+
* @param {WriterInfo} opts.writerInfo - `{ ticks, tipSha, tickShas }`
|
|
204
236
|
* @param {{ points: number[] }} opts.win - Computed window
|
|
205
237
|
* @param {number} opts.currentTick - Active seek cursor tick
|
|
206
238
|
* @returns {string} Formatted, indented swimlane line
|
|
@@ -252,14 +284,45 @@ function buildTickPoints(ticks, tick) {
|
|
|
252
284
|
return { allPoints, currentIdx };
|
|
253
285
|
}
|
|
254
286
|
|
|
287
|
+
// ============================================================================
|
|
288
|
+
// Structural Diff
|
|
289
|
+
// ============================================================================
|
|
290
|
+
|
|
291
|
+
/** Maximum structural diff lines shown in ASCII view. */
|
|
292
|
+
const MAX_DIFF_LINES = 20;
|
|
293
|
+
|
|
255
294
|
/**
|
|
256
|
-
* Builds the
|
|
257
|
-
*
|
|
258
|
-
* @
|
|
259
|
-
* @returns {string[]} Lines for the box body
|
|
295
|
+
* Builds the state summary, receipt, and structural diff footer lines.
|
|
296
|
+
* @param {SeekPayload} payload
|
|
297
|
+
* @returns {string[]}
|
|
260
298
|
*/
|
|
299
|
+
function buildFooterLines(payload) {
|
|
300
|
+
const { tick, nodes, edges, patchCount, diff, tickReceipt } = payload;
|
|
301
|
+
const lines = [];
|
|
302
|
+
lines.push('');
|
|
303
|
+
const nodesStr = `${nodes} ${pluralize(nodes, 'node', 'nodes')}${formatDelta(diff?.nodes ?? 0)}`;
|
|
304
|
+
const edgesStr = `${edges} ${pluralize(edges, 'edge', 'edges')}${formatDelta(diff?.edges ?? 0)}`;
|
|
305
|
+
lines.push(` ${colors.bold('State:')} ${nodesStr}, ${edgesStr}, ${patchCount} ${pluralize(patchCount, 'patch', 'patches')}`);
|
|
306
|
+
|
|
307
|
+
const receiptLines = buildReceiptLines(tickReceipt);
|
|
308
|
+
if (receiptLines.length > 0) {
|
|
309
|
+
lines.push('');
|
|
310
|
+
lines.push(` ${colors.bold(`Tick ${tick}:`)}`);
|
|
311
|
+
lines.push(...receiptLines);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const sdLines = buildStructuralDiffLines(payload, MAX_DIFF_LINES);
|
|
315
|
+
if (sdLines.length > 0) {
|
|
316
|
+
lines.push('');
|
|
317
|
+
lines.push(...sdLines);
|
|
318
|
+
}
|
|
319
|
+
lines.push('');
|
|
320
|
+
return lines;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/** @param {SeekPayload} payload @returns {string[]} */
|
|
261
324
|
function buildSeekBodyLines(payload) {
|
|
262
|
-
const { graph, tick, maxTick, ticks,
|
|
325
|
+
const { graph, tick, maxTick, ticks, perWriter } = payload;
|
|
263
326
|
const lines = [];
|
|
264
327
|
|
|
265
328
|
lines.push('');
|
|
@@ -272,11 +335,9 @@ function buildSeekBodyLines(payload) {
|
|
|
272
335
|
} else {
|
|
273
336
|
const { allPoints, currentIdx } = buildTickPoints(ticks, tick);
|
|
274
337
|
const win = computeWindow(allPoints, currentIdx);
|
|
275
|
-
|
|
276
|
-
// Column headers with relative offsets
|
|
277
338
|
lines.push(buildHeaderRow(win));
|
|
278
339
|
|
|
279
|
-
|
|
340
|
+
/** @type {Array<[string, WriterInfo]>} */
|
|
280
341
|
const writerEntries = perWriter instanceof Map
|
|
281
342
|
? [...perWriter.entries()]
|
|
282
343
|
: Object.entries(perWriter).map(([k, v]) => [k, v]);
|
|
@@ -286,34 +347,137 @@ function buildSeekBodyLines(payload) {
|
|
|
286
347
|
}
|
|
287
348
|
}
|
|
288
349
|
|
|
289
|
-
lines.push(
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
const patchLabel = pluralize(patchCount, 'patch', 'patches');
|
|
350
|
+
lines.push(...buildFooterLines(payload));
|
|
351
|
+
return lines;
|
|
352
|
+
}
|
|
293
353
|
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
354
|
+
/**
|
|
355
|
+
* Builds a truncation hint line when entries exceed the display or data limit.
|
|
356
|
+
* @param {{totalEntries: number, shown: number, maxLines: number, truncated?: boolean, totalChanges?: number, shownChanges?: number}} opts
|
|
357
|
+
* @returns {string|null}
|
|
358
|
+
*/
|
|
359
|
+
function buildTruncationHint(opts) {
|
|
360
|
+
const { totalEntries, shown, maxLines, truncated, totalChanges, shownChanges } = opts;
|
|
361
|
+
if (totalEntries > maxLines && truncated) {
|
|
362
|
+
const remaining = Math.max(0, (totalChanges || 0) - shown);
|
|
363
|
+
return `... and ${remaining} more changes (${totalChanges} total, use --diff-limit to increase)`;
|
|
364
|
+
}
|
|
365
|
+
if (totalEntries > maxLines) {
|
|
366
|
+
return `... and ${Math.max(0, totalEntries - maxLines)} more changes`;
|
|
367
|
+
}
|
|
368
|
+
if (truncated) {
|
|
369
|
+
return `... and ${Math.max(0, (totalChanges || 0) - (shownChanges || 0))} more changes (use --diff-limit to increase)`;
|
|
370
|
+
}
|
|
371
|
+
return null;
|
|
372
|
+
}
|
|
297
373
|
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
374
|
+
/**
|
|
375
|
+
* @param {SeekPayload} payload
|
|
376
|
+
* @param {number} maxLines
|
|
377
|
+
* @returns {string[]}
|
|
378
|
+
*/
|
|
379
|
+
function buildStructuralDiffLines(payload, maxLines) {
|
|
380
|
+
const { structuralDiff, diffBaseline, baselineTick, truncated, totalChanges, shownChanges } = payload;
|
|
381
|
+
if (!structuralDiff) {
|
|
382
|
+
return [];
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const lines = [];
|
|
386
|
+
const baselineLabel = diffBaseline === 'tick'
|
|
387
|
+
? `baseline: tick ${baselineTick}`
|
|
388
|
+
: 'baseline: empty';
|
|
389
|
+
|
|
390
|
+
lines.push(` ${colors.bold(`Changes (${baselineLabel}):`)}`);
|
|
391
|
+
|
|
392
|
+
let shown = 0;
|
|
393
|
+
const entries = collectDiffEntries(structuralDiff);
|
|
394
|
+
|
|
395
|
+
for (const entry of entries) {
|
|
396
|
+
if (shown >= maxLines) {
|
|
397
|
+
break;
|
|
398
|
+
}
|
|
399
|
+
lines.push(` ${entry}`);
|
|
400
|
+
shown++;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
const hint = buildTruncationHint({ totalEntries: entries.length, shown, maxLines, truncated, totalChanges, shownChanges });
|
|
404
|
+
if (hint) {
|
|
405
|
+
lines.push(` ${colors.muted(hint)}`);
|
|
303
406
|
}
|
|
304
|
-
lines.push('');
|
|
305
407
|
|
|
306
408
|
return lines;
|
|
307
409
|
}
|
|
308
410
|
|
|
411
|
+
/**
|
|
412
|
+
* Collects formatted diff entries from a structural diff result.
|
|
413
|
+
*
|
|
414
|
+
* @param {import('../../../domain/services/StateDiff.js').StateDiffResult} diff
|
|
415
|
+
* @returns {string[]} Formatted entries with +/-/~ prefixes
|
|
416
|
+
*/
|
|
417
|
+
function collectDiffEntries(diff) {
|
|
418
|
+
const entries = [];
|
|
419
|
+
|
|
420
|
+
for (const nodeId of diff.nodes.added) {
|
|
421
|
+
entries.push(colors.success(`+ node ${nodeId}`));
|
|
422
|
+
}
|
|
423
|
+
for (const nodeId of diff.nodes.removed) {
|
|
424
|
+
entries.push(colors.error(`- node ${nodeId}`));
|
|
425
|
+
}
|
|
426
|
+
for (const edge of diff.edges.added) {
|
|
427
|
+
entries.push(colors.success(`+ edge ${edge.from} -[${edge.label}]-> ${edge.to}`));
|
|
428
|
+
}
|
|
429
|
+
for (const edge of diff.edges.removed) {
|
|
430
|
+
entries.push(colors.error(`- edge ${edge.from} -[${edge.label}]-> ${edge.to}`));
|
|
431
|
+
}
|
|
432
|
+
for (const prop of diff.props.set) {
|
|
433
|
+
const old = prop.oldValue !== undefined ? formatPropValue(prop.oldValue) : null;
|
|
434
|
+
const arrow = old !== null ? `${old} -> ${formatPropValue(prop.newValue)}` : formatPropValue(prop.newValue);
|
|
435
|
+
entries.push(colors.warning(`~ ${prop.nodeId}.${prop.propKey}: ${arrow}`));
|
|
436
|
+
}
|
|
437
|
+
for (const prop of diff.props.removed) {
|
|
438
|
+
entries.push(colors.error(`- ${prop.nodeId}.${prop.propKey}: ${formatPropValue(prop.oldValue)}`));
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
return entries;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* Formats a property value for display (truncated if too long).
|
|
446
|
+
* @param {*} value
|
|
447
|
+
* @returns {string}
|
|
448
|
+
*/
|
|
449
|
+
function formatPropValue(value) {
|
|
450
|
+
if (value === undefined) {
|
|
451
|
+
return 'undefined';
|
|
452
|
+
}
|
|
453
|
+
const s = typeof value === 'string' ? `"${value}"` : String(value);
|
|
454
|
+
return s.length > 40 ? `${s.slice(0, 37)}...` : s;
|
|
455
|
+
}
|
|
456
|
+
|
|
309
457
|
// ============================================================================
|
|
310
458
|
// Public API
|
|
311
459
|
// ============================================================================
|
|
312
460
|
|
|
461
|
+
/**
|
|
462
|
+
* Formats a structural diff as a plain-text string (no boxen).
|
|
463
|
+
*
|
|
464
|
+
* Used by the non-view renderSeek() path in the CLI.
|
|
465
|
+
*
|
|
466
|
+
* @param {SeekPayload} payload - Seek payload containing structuralDiff
|
|
467
|
+
* @returns {string} Formatted diff section, or empty string if no diff
|
|
468
|
+
*/
|
|
469
|
+
export function formatStructuralDiff(payload) {
|
|
470
|
+
const lines = buildStructuralDiffLines(payload, MAX_DIFF_LINES);
|
|
471
|
+
if (lines.length === 0) {
|
|
472
|
+
return '';
|
|
473
|
+
}
|
|
474
|
+
return `${lines.join('\n')}\n`;
|
|
475
|
+
}
|
|
476
|
+
|
|
313
477
|
/**
|
|
314
478
|
* Renders the seek view dashboard inside a double-bordered box.
|
|
315
479
|
*
|
|
316
|
-
* @param {
|
|
480
|
+
* @param {SeekPayload} payload - Seek payload from the CLI handler
|
|
317
481
|
* @returns {string} Boxen-wrapped ASCII dashboard with trailing newline
|
|
318
482
|
*/
|
|
319
483
|
export function renderSeekView(payload) {
|
|
@@ -6,7 +6,7 @@ import Table from 'cli-table3';
|
|
|
6
6
|
* @param {Object} [options] - Options forwarded to cli-table3 constructor
|
|
7
7
|
* @param {string[]} [options.head] - Column headers
|
|
8
8
|
* @param {Object} [options.style] - Style overrides (defaults: head=cyan, border=gray)
|
|
9
|
-
* @returns {
|
|
9
|
+
* @returns {InstanceType<typeof Table>} A cli-table3 instance
|
|
10
10
|
*/
|
|
11
11
|
export function createTable(options = {}) {
|
|
12
12
|
const defaultStyle = { head: ['cyan'], border: ['gray'] };
|
|
@@ -16,6 +16,7 @@ const PALETTE = {
|
|
|
16
16
|
arrowFill: '#a6adc8',
|
|
17
17
|
};
|
|
18
18
|
|
|
19
|
+
/** @param {string} str @returns {string} */
|
|
19
20
|
function escapeXml(str) {
|
|
20
21
|
return String(str)
|
|
21
22
|
.replace(/&/g, '&')
|
|
@@ -46,6 +47,7 @@ function renderStyle() {
|
|
|
46
47
|
].join('\n');
|
|
47
48
|
}
|
|
48
49
|
|
|
50
|
+
/** @param {{ id: string, x: number, y: number, width: number, height: number, label?: string }} node @returns {string} */
|
|
49
51
|
function renderNode(node) {
|
|
50
52
|
const { x, y, width, height } = node;
|
|
51
53
|
const label = escapeXml(node.label ?? node.id);
|
|
@@ -59,6 +61,7 @@ function renderNode(node) {
|
|
|
59
61
|
].join('\n');
|
|
60
62
|
}
|
|
61
63
|
|
|
64
|
+
/** @param {{ startPoint?: { x: number, y: number }, bendPoints?: Array<{ x: number, y: number }>, endPoint?: { x: number, y: number } }} section @returns {Array<{ x: number, y: number }>} */
|
|
62
65
|
function sectionToPoints(section) {
|
|
63
66
|
const pts = [];
|
|
64
67
|
if (section.startPoint) {
|
|
@@ -73,6 +76,7 @@ function sectionToPoints(section) {
|
|
|
73
76
|
return pts;
|
|
74
77
|
}
|
|
75
78
|
|
|
79
|
+
/** @param {{ sections?: Array<{ startPoint?: { x: number, y: number }, bendPoints?: Array<{ x: number, y: number }>, endPoint?: { x: number, y: number } }>, label?: string }} edge @returns {string} */
|
|
76
80
|
function renderEdge(edge) {
|
|
77
81
|
const { sections } = edge;
|
|
78
82
|
if (!sections || sections.length === 0) {
|
|
@@ -115,7 +119,7 @@ function renderEdge(edge) {
|
|
|
115
119
|
/**
|
|
116
120
|
* Renders a PositionedGraph as an SVG string.
|
|
117
121
|
*
|
|
118
|
-
* @param {
|
|
122
|
+
* @param {{ nodes?: Array<{ id: string, x: number, y: number, width: number, height: number, label?: string }>, edges?: Array<{ sections?: Array<any>, label?: string }>, width?: number, height?: number }} positionedGraph - PositionedGraph from runLayout()
|
|
119
123
|
* @param {{ title?: string }} [options]
|
|
120
124
|
* @returns {string} Complete SVG markup
|
|
121
125
|
*/
|