@git-stunts/git-warp 10.1.2 → 10.4.2
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 +31 -4
- package/bin/warp-graph.js +1242 -59
- package/index.d.ts +31 -0
- package/index.js +4 -0
- package/package.json +13 -3
- package/src/domain/WarpGraph.js +487 -140
- 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 +15 -14
- 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/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 +106 -15
- 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/parseCursorBlob.js +51 -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 +16 -27
- 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/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 +24 -11
- 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 +122 -16
- package/src/visualization/renderers/ascii/history.js +29 -90
- package/src/visualization/renderers/ascii/index.js +1 -1
- 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 +81 -0
- package/src/visualization/renderers/ascii/path.js +1 -1
- package/src/visualization/renderers/ascii/seek.js +344 -0
- package/src/visualization/renderers/ascii/table.js +1 -1
- package/src/visualization/renderers/svg/index.js +5 -1
|
@@ -2,24 +2,34 @@
|
|
|
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',
|
|
8
18
|
'elk.direction': 'DOWN',
|
|
9
|
-
'elk.spacing.nodeNode': '
|
|
10
|
-
'elk.layered.spacing.nodeNodeBetweenLayers': '
|
|
19
|
+
'elk.spacing.nodeNode': '30',
|
|
20
|
+
'elk.layered.spacing.nodeNodeBetweenLayers': '40',
|
|
11
21
|
},
|
|
12
22
|
path: {
|
|
13
23
|
'elk.algorithm': 'layered',
|
|
14
24
|
'elk.direction': 'RIGHT',
|
|
15
|
-
'elk.spacing.nodeNode': '
|
|
16
|
-
'elk.layered.spacing.nodeNodeBetweenLayers': '
|
|
25
|
+
'elk.spacing.nodeNode': '30',
|
|
26
|
+
'elk.layered.spacing.nodeNodeBetweenLayers': '40',
|
|
17
27
|
},
|
|
18
28
|
slice: {
|
|
19
29
|
'elk.algorithm': 'layered',
|
|
20
30
|
'elk.direction': 'DOWN',
|
|
21
|
-
'elk.spacing.nodeNode': '
|
|
22
|
-
'elk.layered.spacing.nodeNodeBetweenLayers': '
|
|
31
|
+
'elk.spacing.nodeNode': '30',
|
|
32
|
+
'elk.layered.spacing.nodeNodeBetweenLayers': '40',
|
|
23
33
|
},
|
|
24
34
|
};
|
|
25
35
|
|
|
@@ -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;
|
|
@@ -46,14 +58,14 @@ function estimateNodeWidth(label) {
|
|
|
46
58
|
return Math.max((label?.length ?? 0) * charWidth + padding, minWidth);
|
|
47
59
|
}
|
|
48
60
|
|
|
49
|
-
const NODE_HEIGHT =
|
|
61
|
+
const NODE_HEIGHT = 30;
|
|
50
62
|
|
|
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) {
|
|
@@ -2,17 +2,27 @@
|
|
|
2
2
|
* ASCII graph renderer: maps ELK-positioned nodes and edges onto a character grid.
|
|
3
3
|
*
|
|
4
4
|
* Pixel-to-character scaling:
|
|
5
|
-
* cellW =
|
|
5
|
+
* cellW = 10, cellH = 10
|
|
6
|
+
* ELK uses NODE_HEIGHT=40, nodeNode=40, betweenLayers=60.
|
|
7
|
+
* At cellH=10: 40px → 4 rows, compact 3-row nodes fit with natural gaps.
|
|
6
8
|
*/
|
|
7
9
|
|
|
8
10
|
import { createBox } from './box.js';
|
|
9
11
|
import { colors } from './colors.js';
|
|
10
12
|
import { ARROW } from './symbols.js';
|
|
11
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
|
+
|
|
12
22
|
// ── Scaling constants ────────────────────────────────────────────────────────
|
|
13
23
|
|
|
14
|
-
const CELL_W =
|
|
15
|
-
const CELL_H =
|
|
24
|
+
const CELL_W = 10;
|
|
25
|
+
const CELL_H = 10;
|
|
16
26
|
const MARGIN = 2;
|
|
17
27
|
|
|
18
28
|
// ── Box-drawing characters (short keys for tight grid-stamping loops) ───────
|
|
@@ -28,23 +38,33 @@ const BOX = {
|
|
|
28
38
|
|
|
29
39
|
// ── Grid helpers ─────────────────────────────────────────────────────────────
|
|
30
40
|
|
|
41
|
+
/** @param {number} px */
|
|
31
42
|
function toCol(px) {
|
|
32
43
|
return Math.round(px / CELL_W) + MARGIN;
|
|
33
44
|
}
|
|
34
45
|
|
|
46
|
+
/** @param {number} px */
|
|
35
47
|
function toRow(px) {
|
|
36
48
|
return Math.round(px / CELL_H) + MARGIN;
|
|
37
49
|
}
|
|
38
50
|
|
|
51
|
+
/** @param {number} px */
|
|
39
52
|
function scaleW(px) {
|
|
40
53
|
return Math.round(px / CELL_W);
|
|
41
54
|
}
|
|
42
55
|
|
|
56
|
+
/** @param {number} px */
|
|
43
57
|
function scaleH(px) {
|
|
44
58
|
return Math.round(px / CELL_H);
|
|
45
59
|
}
|
|
46
60
|
|
|
61
|
+
/**
|
|
62
|
+
* @param {number} rows
|
|
63
|
+
* @param {number} cols
|
|
64
|
+
* @returns {string[][]}
|
|
65
|
+
*/
|
|
47
66
|
function createGrid(rows, cols) {
|
|
67
|
+
/** @type {string[][]} */
|
|
48
68
|
const grid = [];
|
|
49
69
|
for (let r = 0; r < rows; r++) {
|
|
50
70
|
grid.push(new Array(cols).fill(' '));
|
|
@@ -52,12 +72,24 @@ function createGrid(rows, cols) {
|
|
|
52
72
|
return grid;
|
|
53
73
|
}
|
|
54
74
|
|
|
75
|
+
/**
|
|
76
|
+
* @param {string[][]} grid
|
|
77
|
+
* @param {number} r
|
|
78
|
+
* @param {number} c
|
|
79
|
+
* @param {string} ch
|
|
80
|
+
*/
|
|
55
81
|
function writeChar(grid, r, c, ch) {
|
|
56
82
|
if (r >= 0 && r < grid.length && c >= 0 && c < grid[0].length) {
|
|
57
83
|
grid[r][c] = ch;
|
|
58
84
|
}
|
|
59
85
|
}
|
|
60
86
|
|
|
87
|
+
/**
|
|
88
|
+
* @param {string[][]} grid
|
|
89
|
+
* @param {number} r
|
|
90
|
+
* @param {number} c
|
|
91
|
+
* @returns {string}
|
|
92
|
+
*/
|
|
61
93
|
function readChar(grid, r, c) {
|
|
62
94
|
if (r >= 0 && r < grid.length && c >= 0 && c < grid[0].length) {
|
|
63
95
|
return grid[r][c];
|
|
@@ -65,6 +97,12 @@ function readChar(grid, r, c) {
|
|
|
65
97
|
return ' ';
|
|
66
98
|
}
|
|
67
99
|
|
|
100
|
+
/**
|
|
101
|
+
* @param {string[][]} grid
|
|
102
|
+
* @param {number} r
|
|
103
|
+
* @param {number} c
|
|
104
|
+
* @param {string} str
|
|
105
|
+
*/
|
|
68
106
|
function writeString(grid, r, c, str) {
|
|
69
107
|
for (let i = 0; i < str.length; i++) {
|
|
70
108
|
writeChar(grid, r, c + i, str[i]);
|
|
@@ -73,11 +111,15 @@ function writeString(grid, r, c, str) {
|
|
|
73
111
|
|
|
74
112
|
// ── Node stamping ────────────────────────────────────────────────────────────
|
|
75
113
|
|
|
114
|
+
/**
|
|
115
|
+
* @param {string[][]} grid
|
|
116
|
+
* @param {PositionedNode} node
|
|
117
|
+
*/
|
|
76
118
|
function stampNode(grid, node) {
|
|
77
119
|
const r = toRow(node.y);
|
|
78
120
|
const c = toCol(node.x);
|
|
79
|
-
const w = Math.max(
|
|
80
|
-
const h =
|
|
121
|
+
const w = Math.max(toCol(node.width), 4);
|
|
122
|
+
const h = 3; // Always: border + label + border
|
|
81
123
|
|
|
82
124
|
// Top border
|
|
83
125
|
writeChar(grid, r, c, BOX.tl);
|
|
@@ -87,10 +129,8 @@ function stampNode(grid, node) {
|
|
|
87
129
|
writeChar(grid, r, c + w - 1, BOX.tr);
|
|
88
130
|
|
|
89
131
|
// Side borders
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
writeChar(grid, r + j, c + w - 1, BOX.v);
|
|
93
|
-
}
|
|
132
|
+
writeChar(grid, r + 1, c, BOX.v);
|
|
133
|
+
writeChar(grid, r + 1, c + w - 1, BOX.v);
|
|
94
134
|
|
|
95
135
|
// Bottom border
|
|
96
136
|
writeChar(grid, r + h - 1, c, BOX.bl);
|
|
@@ -99,19 +139,24 @@ function stampNode(grid, node) {
|
|
|
99
139
|
}
|
|
100
140
|
writeChar(grid, r + h - 1, c + w - 1, BOX.br);
|
|
101
141
|
|
|
102
|
-
// Label (
|
|
142
|
+
// Label (always on row 1)
|
|
103
143
|
const label = node.label ?? node.id;
|
|
104
144
|
const maxLabel = w - 4;
|
|
105
145
|
const truncated = label.length > maxLabel
|
|
106
146
|
? `${label.slice(0, Math.max(maxLabel - 1, 1))}\u2026`
|
|
107
147
|
: label;
|
|
108
|
-
const labelRow = r +
|
|
148
|
+
const labelRow = r + 1;
|
|
109
149
|
const labelCol = c + Math.max(1, Math.floor((w - truncated.length) / 2));
|
|
110
150
|
writeString(grid, labelRow, labelCol, truncated);
|
|
111
151
|
}
|
|
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) {
|
|
@@ -220,6 +298,8 @@ function drawArrowhead(grid, section, nodeSet) {
|
|
|
220
298
|
const pc = toCol(prev.x);
|
|
221
299
|
|
|
222
300
|
let arrow;
|
|
301
|
+
let ar = er;
|
|
302
|
+
let ac = ec;
|
|
223
303
|
if (er > pr) {
|
|
224
304
|
arrow = ARROW.down;
|
|
225
305
|
} else if (er < pr) {
|
|
@@ -230,11 +310,30 @@ function drawArrowhead(grid, section, nodeSet) {
|
|
|
230
310
|
arrow = ARROW.left;
|
|
231
311
|
}
|
|
232
312
|
|
|
233
|
-
|
|
234
|
-
|
|
313
|
+
// If the endpoint is inside a node box, step back one cell into free space
|
|
314
|
+
if (isNodeCell(nodeSet, ar, ac)) {
|
|
315
|
+
if (er > pr) {
|
|
316
|
+
ar = er - 1;
|
|
317
|
+
} else if (er < pr) {
|
|
318
|
+
ar = er + 1;
|
|
319
|
+
} else if (ec > pc) {
|
|
320
|
+
ac = ec - 1;
|
|
321
|
+
} else {
|
|
322
|
+
ac = ec + 1;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
if (!isNodeCell(nodeSet, ar, ac)) {
|
|
327
|
+
writeChar(grid, ar, ac, arrow);
|
|
235
328
|
}
|
|
236
329
|
}
|
|
237
330
|
|
|
331
|
+
/**
|
|
332
|
+
* @param {string[][]} grid
|
|
333
|
+
* @param {Section[]} sections
|
|
334
|
+
* @param {string} label
|
|
335
|
+
* @param {Set<string>} nodeSet
|
|
336
|
+
*/
|
|
238
337
|
function placeEdgeLabel(grid, sections, label, nodeSet) {
|
|
239
338
|
// Find midpoint of the full path
|
|
240
339
|
const allPoints = [];
|
|
@@ -277,13 +376,14 @@ function placeEdgeLabel(grid, sections, label, nodeSet) {
|
|
|
277
376
|
|
|
278
377
|
// ── Node occupancy set ───────────────────────────────────────────────────────
|
|
279
378
|
|
|
379
|
+
/** @param {PositionedNode[]} nodes @returns {Set<string>} */
|
|
280
380
|
function buildNodeSet(nodes) {
|
|
281
381
|
const set = new Set();
|
|
282
382
|
for (const node of nodes) {
|
|
283
383
|
const r = toRow(node.y);
|
|
284
384
|
const c = toCol(node.x);
|
|
285
|
-
const w = Math.max(
|
|
286
|
-
const h =
|
|
385
|
+
const w = Math.max(toCol(node.width), 4);
|
|
386
|
+
const h = 3; // Match compact node height
|
|
287
387
|
for (let dr = 0; dr < h; dr++) {
|
|
288
388
|
for (let dc = 0; dc < w; dc++) {
|
|
289
389
|
set.add(`${r + dr},${c + dc}`);
|
|
@@ -293,6 +393,12 @@ function buildNodeSet(nodes) {
|
|
|
293
393
|
return set;
|
|
294
394
|
}
|
|
295
395
|
|
|
396
|
+
/**
|
|
397
|
+
* @param {Set<string>} nodeSet
|
|
398
|
+
* @param {number} r
|
|
399
|
+
* @param {number} c
|
|
400
|
+
* @returns {boolean}
|
|
401
|
+
*/
|
|
296
402
|
function isNodeCell(nodeSet, r, c) {
|
|
297
403
|
return nodeSet.has(`${r},${c}`);
|
|
298
404
|
}
|
|
@@ -302,7 +408,7 @@ function isNodeCell(nodeSet, r, c) {
|
|
|
302
408
|
/**
|
|
303
409
|
* Renders a PositionedGraph (from ELK) as an ASCII box-drawing string.
|
|
304
410
|
*
|
|
305
|
-
* @param {
|
|
411
|
+
* @param {PositionedGraph} positionedGraph - PositionedGraph from runLayout()
|
|
306
412
|
* @param {{ title?: string }} [options]
|
|
307
413
|
* @returns {string} Rendered ASCII art wrapped in a box
|
|
308
414
|
*/
|