reslop 0.2.5 → 0.2.7

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.
Files changed (114) hide show
  1. package/CHANGELOG.md +22 -1
  2. package/LICENSE +1 -1
  3. package/README.md +11 -11
  4. package/bin/reslop.js +3 -8
  5. package/lib/ansi.js +72 -19
  6. package/lib/api.js +1 -1
  7. package/lib/capabilities.js +8 -8
  8. package/lib/cli.js +61 -75
  9. package/lib/clipboard.js +39 -47
  10. package/lib/dashboard/git.js +128 -0
  11. package/lib/dashboard/model.js +357 -0
  12. package/lib/dashboard/npm.js +290 -0
  13. package/lib/dashboard/tree.js +278 -0
  14. package/lib/deps-diff.js +197 -402
  15. package/lib/deps.js +113 -240
  16. package/lib/detect.js +8 -18
  17. package/lib/diff/align.js +41 -113
  18. package/lib/diff/diff.js +12 -43
  19. package/lib/diff/display.js +25 -36
  20. package/lib/diff/edit.js +6 -20
  21. package/lib/diff/inline.js +76 -144
  22. package/lib/diff/items.js +33 -47
  23. package/lib/diff/parse.js +28 -56
  24. package/lib/diff/patch.js +12 -33
  25. package/lib/diff/unit.js +79 -178
  26. package/lib/editor.js +52 -69
  27. package/lib/files.js +79 -91
  28. package/lib/find.js +61 -0
  29. package/lib/git-branches.js +254 -416
  30. package/lib/git-deps.js +148 -272
  31. package/lib/git-reword-todo.js +5 -13
  32. package/lib/git-worktree.js +161 -214
  33. package/lib/git.js +21 -49
  34. package/lib/github.js +67 -181
  35. package/lib/gitlab.js +40 -90
  36. package/lib/highlight/bash.js +55 -177
  37. package/lib/highlight/core.js +155 -141
  38. package/lib/highlight/css.js +25 -58
  39. package/lib/highlight/csv.js +19 -37
  40. package/lib/highlight/dart.js +72 -213
  41. package/lib/highlight/dot.js +14 -14
  42. package/lib/highlight/html.js +42 -63
  43. package/lib/highlight/js.js +177 -506
  44. package/lib/highlight/json.js +17 -37
  45. package/lib/highlight/log.js +19 -25
  46. package/lib/highlight/txt.js +7 -10
  47. package/lib/highlight.js +17 -46
  48. package/lib/keys.js +75 -83
  49. package/lib/manifest.js +67 -126
  50. package/lib/npm-commands.js +120 -169
  51. package/lib/remote.js +140 -190
  52. package/lib/render/branches.js +63 -61
  53. package/lib/render/chrome.js +412 -347
  54. package/lib/render/commits.js +160 -361
  55. package/lib/render/dash-activity.js +212 -0
  56. package/lib/render/dash-blocks.js +496 -0
  57. package/lib/render/dash-table.js +295 -0
  58. package/lib/render/dashboard.js +62 -0
  59. package/lib/render/diff.js +123 -258
  60. package/lib/render/files.js +60 -86
  61. package/lib/render/frame.js +82 -117
  62. package/lib/render/notes.js +150 -210
  63. package/lib/render/npm.js +213 -177
  64. package/lib/render/packages.js +93 -0
  65. package/lib/render/present.js +4 -11
  66. package/lib/render/primitives.js +77 -172
  67. package/lib/render/render.js +5 -14
  68. package/lib/render/report.js +43 -0
  69. package/lib/render/repos.js +226 -0
  70. package/lib/render/search.js +25 -0
  71. package/lib/render/tiles.js +355 -0
  72. package/lib/render/unit.js +15 -31
  73. package/lib/report-model.js +110 -33
  74. package/lib/report-parse.js +56 -37
  75. package/lib/report-render.js +102 -59
  76. package/lib/report-run.js +87 -77
  77. package/lib/report-test.js +221 -99
  78. package/lib/report-tools.js +96 -154
  79. package/lib/review-merge.js +33 -41
  80. package/lib/review.js +69 -129
  81. package/lib/runs.js +225 -0
  82. package/lib/select.js +53 -91
  83. package/lib/session/accessors.js +14 -21
  84. package/lib/session/actions.js +189 -65
  85. package/lib/session/branches.js +164 -222
  86. package/lib/session/change-actions.js +49 -119
  87. package/lib/session/commit.js +298 -352
  88. package/lib/session/compose.js +572 -715
  89. package/lib/session/dashboard.js +530 -0
  90. package/lib/session/dispatch.js +197 -171
  91. package/lib/session/files-pane.js +92 -45
  92. package/lib/session/find.js +94 -0
  93. package/lib/session/items.js +6 -17
  94. package/lib/session/lifecycle.js +420 -466
  95. package/lib/session/load.js +1 -14
  96. package/lib/session/navigation.js +4 -2
  97. package/lib/session/npm.js +403 -420
  98. package/lib/session/ops.js +5 -13
  99. package/lib/session/packages.js +143 -0
  100. package/lib/session/pointer.js +30 -14
  101. package/lib/session/progress.js +39 -30
  102. package/lib/session/review.js +1 -3
  103. package/lib/session/tasks.js +277 -236
  104. package/lib/session/templates.js +51 -59
  105. package/lib/session/terminal.js +85 -132
  106. package/lib/session/view.js +59 -80
  107. package/lib/session/watch.js +201 -212
  108. package/lib/session/workspace.js +455 -0
  109. package/lib/session.js +95 -161
  110. package/lib/update.js +44 -111
  111. package/lib/utilities.js +46 -88
  112. package/lib/workspace.js +166 -0
  113. package/lib/wrap.js +29 -55
  114. package/package.json +1 -1
@@ -0,0 +1,355 @@
1
+ 'use strict';
2
+
3
+ const ansi = require('../ansi.js');
4
+ const { THEME, paint, visibleWidth, truncateVisible } = ansi;
5
+
6
+ const GAP_X = 2;
7
+ const GAP_Y = 1;
8
+ const MIN_W = 34;
9
+ const MIN_H = 6;
10
+ const MAX_COLS = 4;
11
+ const RULE = '─';
12
+ const ELLIPSIS = '…';
13
+ const BAR_ON = '█';
14
+ const BAR_OFF = '░';
15
+ const SPARK = '▁▂▃▄▅▆▇█';
16
+ const SPINNER = '⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏';
17
+
18
+ const seg = (text, tone = 'text', bold = false) => ({
19
+ text: `${text}`,
20
+ tone,
21
+ bold,
22
+ bg: null,
23
+ });
24
+
25
+ const toneColors = () => ({
26
+ text: THEME.chromeFg,
27
+ muted: THEME.mutedFg,
28
+ hot: THEME.buttonHotFg,
29
+ add: THEME.addLineFg,
30
+ del: THEME.delLineFg,
31
+ warn: THEME.warnFg,
32
+ error: THEME.errorFg,
33
+ sha: THEME.shaFg,
34
+ dep: THEME.pkgDepFg,
35
+ dev: THEME.pkgDevFg,
36
+ pkg: THEME.pkgFg,
37
+ chain: THEME.pkgChainFg,
38
+ heat: THEME.heatFg,
39
+ blink: THEME.heatBlinkFg,
40
+ key: THEME.dashKeyFg,
41
+ });
42
+
43
+ const headColors = () => ({
44
+ ...toneColors(),
45
+ muted: THEME.dashHeadMuted,
46
+ del: THEME.dashDelFg,
47
+ });
48
+
49
+ const segmentsWidth = (segs) => {
50
+ let width = 0;
51
+ for (const item of segs) width += visibleWidth(item.text);
52
+ return width;
53
+ };
54
+
55
+ const clipSegments = (segs, width) => {
56
+ if (segmentsWidth(segs) <= width) {
57
+ return segs.map((item) => ({ ...item }));
58
+ }
59
+ const markW = visibleWidth(ELLIPSIS);
60
+ const budget = markW <= width ? width - markW : width;
61
+ const out = [];
62
+ let used = 0;
63
+ for (const item of segs) {
64
+ const room = budget - used;
65
+ if (room <= 0) break;
66
+ const text = truncateVisible(item.text, room);
67
+ if (!text) continue;
68
+ used += visibleWidth(text);
69
+ out.push({ ...item, text });
70
+ if (visibleWidth(text) < visibleWidth(item.text)) break;
71
+ }
72
+ if (markW > width) return out;
73
+ if (!out.length) return [seg(ELLIPSIS)];
74
+ out[out.length - 1].text += ELLIPSIS;
75
+ return out;
76
+ };
77
+
78
+ const paintSegments = (segs, width, bg, color, fill = ' ', colors = null) => {
79
+ const clipped = clipSegments(segs, width);
80
+ const pad = Math.max(0, width - segmentsWidth(clipped));
81
+ const ink = colors ?? toneColors();
82
+ if (!color) {
83
+ return clipped.map((item) => item.text).join('') + fill.repeat(pad);
84
+ }
85
+ let out = '';
86
+ for (const item of clipped) {
87
+ const pieceBg = item.bg ?? bg;
88
+ out += paint(item.text, ink[item.tone], pieceBg, color, item.bold);
89
+ }
90
+ return out + paint(fill.repeat(pad), ink.muted, bg, color);
91
+ };
92
+
93
+ const barSegments = (ratio, width, tone = 'add') => {
94
+ const clamped = Math.min(1, Math.max(0, Number.isFinite(ratio) ? ratio : 0));
95
+ const on = Math.round(clamped * width);
96
+ return [
97
+ seg(BAR_ON.repeat(on), tone),
98
+ seg(BAR_OFF.repeat(width - on), 'muted'),
99
+ ];
100
+ };
101
+
102
+ const sparkline = (values, width) => {
103
+ const count = values.length;
104
+ if (!count || width < 1) return '';
105
+ const max = Math.max(...values);
106
+ const top = SPARK.length - 1;
107
+ let out = '';
108
+ for (let i = 0; i < width; i++) {
109
+ const span = width <= 1 ? count - 1 : (i * (count - 1)) / (width - 1);
110
+ const at = Math.round(span);
111
+ const level = max > 0 ? Math.round((values[at] / max) * top) : 0;
112
+ out += SPARK[level];
113
+ }
114
+ return out;
115
+ };
116
+
117
+ const spinner = (frame) => SPINNER[Math.abs(frame ?? 0) % SPINNER.length];
118
+
119
+ const spread = (total, parts) => {
120
+ const base = Math.floor(total / parts);
121
+ const extra = total % parts;
122
+ return Array.from({ length: parts }, (_, i) => base + (i < extra ? 1 : 0));
123
+ };
124
+
125
+ const placeBand = (shown, from, size, width) => {
126
+ const room = Math.max(0, width - (size - 1) * GAP_X);
127
+ const widths = spread(room, size);
128
+ const tiles = [];
129
+ let x = 0;
130
+ for (let i = 0; i < size; i++) {
131
+ tiles.push({ id: shown[from + i].id, x, w: widths[i] });
132
+ x += widths[i] + GAP_X;
133
+ }
134
+ return tiles;
135
+ };
136
+
137
+ const placeTiles = (shown, cols, width, height) => {
138
+ const rows = Math.ceil(shown.length / cols);
139
+ const sizes = spread(shown.length, rows);
140
+ const heights = spread(height - (rows - 1) * GAP_Y, rows);
141
+ const bands = [];
142
+ let from = 0;
143
+ let y = 0;
144
+ for (let row = 0; row < rows; row++) {
145
+ const tiles = placeBand(shown, from, sizes[row], width);
146
+ bands.push({ y, h: Math.max(1, heights[row]), tiles });
147
+ from += sizes[row];
148
+ y += heights[row] + GAP_Y;
149
+ }
150
+ return bands;
151
+ };
152
+
153
+ const layoutTiles = (width, height, tiles) => {
154
+ const cap = Math.floor((width + GAP_X) / (MIN_W + GAP_X));
155
+ const maxCols = Math.max(1, Math.min(MAX_COLS, cap));
156
+ const ranked = [...tiles].sort((left, right) => left.rank - right.rank);
157
+ for (let count = tiles.length; count >= 1; count--) {
158
+ const keep = new Set(ranked.slice(0, count).map((tile) => tile.id));
159
+ const shown = tiles.filter((tile) => keep.has(tile.id));
160
+ const cols = Math.min(maxCols, count);
161
+ const rows = Math.ceil(count / cols);
162
+ const rowH = Math.floor((height - (rows - 1) * GAP_Y) / rows);
163
+ if (rowH >= MIN_H || count === 1) {
164
+ return placeTiles(shown, cols, width, height);
165
+ }
166
+ }
167
+ return [];
168
+ };
169
+
170
+ const titleSegments = (tile, badge = []) => {
171
+ const keyed = tile.title.startsWith(tile.key);
172
+ const rest = keyed ? tile.title.slice(tile.key.length) : ` ${tile.title}`;
173
+ const segs = [seg(tile.key, 'key', true), seg(rest)];
174
+ return badge.length ? [...segs, seg(' '), ...badge] : segs;
175
+ };
176
+
177
+ const paintFramed = (line, width, bg, color, fill = ' ', colors = null) => {
178
+ const edge = paintSegments([seg(' ')], 1, bg, color);
179
+ const inner = Math.max(0, width - 2);
180
+ return edge + paintSegments(line, inner, bg, color, fill, colors) + edge;
181
+ };
182
+
183
+ const paintFlushRight = (line, width, bg, color) => {
184
+ const edge = paintSegments([seg(' ')], 1, bg, color);
185
+ const inner = Math.max(0, width - 1);
186
+ return edge + paintSegments(line, inner, bg, color);
187
+ };
188
+
189
+ const paintHead = (tile, content, width, color, headBg) => {
190
+ const fill = color ? ' ' : RULE;
191
+ const bg = headBg;
192
+ const colors = headColors();
193
+ if (content.titleLine) {
194
+ return paintFramed(content.titleLine, width, bg, color, fill, colors);
195
+ }
196
+ const segs = [seg(' '), ...titleSegments(tile, content.badge)];
197
+ return paintSegments(segs, width, bg, color, fill, colors);
198
+ };
199
+
200
+ const rowBackground = (index, filled, backs) => {
201
+ if (filled && index % 2 === 1) return backs.row;
202
+ return backs.tile;
203
+ };
204
+
205
+ const paintTile = (tile, content, width, height, color, backs = null) => {
206
+ const shade = backs ?? {
207
+ head: THEME.dashHeadBg,
208
+ tile: THEME.dashTileBg,
209
+ row: THEME.dashRowBg,
210
+ };
211
+ const { lines, footer } = content;
212
+ const rows = [paintHead(tile, content, width, color, shade.head)];
213
+ const bodyRoom = footer ? height - 2 : height - 1;
214
+ for (let i = 0; i < bodyRoom; i++) {
215
+ const bg = rowBackground(i, i < lines.length, shade);
216
+ rows.push(paintFramed(lines[i] ?? [], width, bg, color));
217
+ }
218
+ if (footer && height > 1) {
219
+ const bg = rowBackground(bodyRoom, true, shade);
220
+ const paintRow = content.footerFlush ? paintFlushRight : paintFramed;
221
+ rows.push(paintRow(footer, width, bg, color));
222
+ }
223
+ return rows;
224
+ };
225
+
226
+ const pageFill = (width, color) =>
227
+ paint(' '.repeat(Math.max(0, width)), THEME.chromeFg, THEME.dashBg, color);
228
+
229
+ const composeBands = (bands, painted, width, height, color) => {
230
+ const rows = [];
231
+ const blank = pageFill(width, color);
232
+ for (const band of bands) {
233
+ if (rows.length) {
234
+ for (let gap = 0; gap < GAP_Y; gap++) rows.push(blank);
235
+ }
236
+ const last = band.tiles.at(-1);
237
+ const rest = pageFill(width - last.x - last.w, color);
238
+ for (let line = 0; line < band.h; line++) {
239
+ let row = '';
240
+ for (let i = 0; i < band.tiles.length; i++) {
241
+ if (i) row += pageFill(GAP_X, color);
242
+ row += painted.get(band.tiles[i].id)[line];
243
+ }
244
+ rows.push(row + rest);
245
+ }
246
+ }
247
+ while (rows.length < height) rows.push(blank);
248
+ return rows.slice(0, height);
249
+ };
250
+
251
+ const gridSize = (width, height, cols, rows) => ({
252
+ tileW: Math.floor((width - (cols - 1) * GAP_X) / cols),
253
+ tileH: Math.floor((height - (rows - 1) * GAP_Y) / rows),
254
+ });
255
+
256
+ const equalGrid = (width, height, count, minW = MIN_W, minH = MIN_H) => {
257
+ const safe = Math.max(1, count);
258
+ const colCap = Math.max(
259
+ 1,
260
+ Math.min(safe, Math.floor((width + GAP_X) / (minW + GAP_X)) || 1),
261
+ );
262
+ const rowCap = Math.max(
263
+ 1,
264
+ Math.min(safe, Math.floor((height + GAP_Y) / (minH + GAP_Y)) || 1),
265
+ );
266
+ let best = null;
267
+ for (let cols = 1; cols <= colCap; cols++) {
268
+ for (let rows = 1; rows <= rowCap; rows++) {
269
+ const size = gridSize(width, height, cols, rows);
270
+ if (size.tileW < minW || size.tileH < minH) continue;
271
+ const cap = cols * rows;
272
+ const shown = Math.min(safe, cap);
273
+ const score = shown * 1000000 + size.tileW * size.tileH;
274
+ if (!best || score > best.score) {
275
+ best = { cols, rows, cap, ...size, score };
276
+ }
277
+ }
278
+ }
279
+ if (best) return best;
280
+ return {
281
+ cols: 1,
282
+ rows: 1,
283
+ cap: 1,
284
+ tileW: Math.max(1, width),
285
+ tileH: Math.max(1, height),
286
+ score: 0,
287
+ };
288
+ };
289
+
290
+ const pageWindow = (count, cursor, cap) => {
291
+ const room = Math.max(1, cap);
292
+ if (count <= room) return { start: 0, end: count };
293
+ const at = Math.min(Math.max(0, cursor), count - 1);
294
+ let start = Math.floor(at / room) * room;
295
+ if (start + room > count) start = count - room;
296
+ return { start, end: start + room };
297
+ };
298
+
299
+ const placeEqualTiles = (tiles, grid) => {
300
+ const bands = [];
301
+ const cols = Math.max(1, grid.cols);
302
+ const rows = Math.ceil(tiles.length / cols);
303
+ for (let row = 0; row < rows; row++) {
304
+ const slice = tiles.slice(row * cols, (row + 1) * cols);
305
+ if (!slice.length) break;
306
+ const placed = [];
307
+ for (let i = 0; i < slice.length; i++) {
308
+ placed.push({
309
+ id: slice[i].id,
310
+ x: i * (grid.tileW + GAP_X),
311
+ w: grid.tileW,
312
+ });
313
+ }
314
+ bands.push({
315
+ y: row * (grid.tileH + GAP_Y),
316
+ h: grid.tileH,
317
+ tiles: placed,
318
+ });
319
+ }
320
+ return bands;
321
+ };
322
+
323
+ const tileHits = (bands, headerLines) => {
324
+ const hits = [];
325
+ for (const band of bands) {
326
+ for (const tile of band.tiles) {
327
+ for (let line = 0; line < band.h; line++) {
328
+ const y = headerLines + band.y + line + 1;
329
+ hits.push({ y, x0: tile.x, x1: tile.x + tile.w, cursor: tile.id });
330
+ }
331
+ }
332
+ }
333
+ return hits;
334
+ };
335
+
336
+ module.exports = {
337
+ GAP_X,
338
+ GAP_Y,
339
+ seg,
340
+ segmentsWidth,
341
+ clipSegments,
342
+ barSegments,
343
+ sparkline,
344
+ spinner,
345
+ titleSegments,
346
+ layoutTiles,
347
+ equalGrid,
348
+ pageWindow,
349
+ placeEqualTiles,
350
+ paintSegments,
351
+ paintTile,
352
+ composeBands,
353
+ tileHits,
354
+ pageFill,
355
+ };
@@ -1,47 +1,31 @@
1
1
  'use strict';
2
2
 
3
3
  const detect = require('../detect.js');
4
- const { detectLang } = detect;
4
+ const diffLib = require('../diff/diff.js');
5
5
  const primitives = require('./primitives.js');
6
6
  const diff = require('./diff.js');
7
7
 
8
- const { paneResult } = primitives;
9
- const { paintDiffLine, codeCursorInRows, codeEditHits } = diff;
10
- const { expandSoftRows } = diff;
11
- const diffLib = require('../diff/diff.js');
8
+ const { detectLang } = detect;
12
9
  const { sameHunk } = diffLib;
10
+ const { paneResult } = primitives;
11
+ const { paintDiffLine, expandSoftRows, codeHits } = diff;
13
12
 
14
13
  const paintBodyUnit = (view, width, color) => {
15
- const lines = view.unitLines ?? [];
16
- const item = view.item;
14
+ const { item, codeOverlay: overlay } = view;
17
15
  const lang = detectLang(view.reviewPath ?? '');
18
- const overlay = view.codeOverlay;
19
16
  const editing =
20
- view.mode === 'compose' && overlay && typeof overlay.cursor === 'number';
21
- const picked = editing ? overlay.scrollCol : 0;
22
- const scrollCol = picked ?? 0;
17
+ view.mode === 'compose' && typeof overlay?.cursor === 'number';
18
+ const scrollCol = (editing ? overlay.scrollCol : 0) ?? 0;
19
+ const lines = view.unitLines ?? [];
23
20
  const source = editing ? lines : expandSoftRows(lines, width, false, true);
24
- const { length } = source;
25
- const body = new Array(length);
26
- for (let i = 0; i < length; i++) {
27
- const line = source[i];
28
- const isBlock = item && sameHunk(line.item, item);
29
- const row = { ...line };
30
- if (editing) {
31
- row.mark = ' ';
32
- } else if ((row.type === 'add' || row.type === 'del') && !isBlock) {
33
- row.mark = ' ';
34
- }
35
- body[i] = paintDiffLine(row, width, color, row.origin, lang, scrollCol);
36
- }
37
- const edit = overlay ? overlay.cursor : null;
38
- const cursor = codeCursorInRows(source, false, width, edit, scrollCol);
39
- const editHits = editing ? codeEditHits(source, false, width, scrollCol) : [];
40
- return paneResult({
41
- body,
42
- cursor,
43
- editHits,
21
+ const body = source.map((line) => {
22
+ const changed = line.type === 'add' || line.type === 'del';
23
+ const outside = changed && !(item && sameHunk(line.item, item));
24
+ const row = editing || outside ? { ...line, mark: ' ' } : line;
25
+ return paintDiffLine(row, width, color, row.origin, lang, scrollCol);
44
26
  });
27
+ const hits = codeHits(source, false, width, overlay, editing, scrollCol);
28
+ return paneResult({ body, ...hits });
45
29
  };
46
30
 
47
31
  module.exports = { paintBodyUnit };
@@ -4,7 +4,7 @@ const ansi = require('./ansi.js');
4
4
  const { stripAnsi } = ansi;
5
5
  const commands = require('./npm-commands.js');
6
6
 
7
- const { withoutNodeFrame, indentStack, relativize } = commands;
7
+ const { traceLine, relativize } = commands;
8
8
 
9
9
  const summary = () => ({
10
10
  tests: null,
@@ -23,8 +23,10 @@ const problem = (fields = {}) => {
23
23
  severity: fields.severity || 'error',
24
24
  name: fields.name || '',
25
25
  message: fields.message || '',
26
+ key: fields.key || '',
26
27
  trace,
27
28
  traces,
29
+ related: fields.related || [],
28
30
  duration: fields.duration || '',
29
31
  code: fields.code || '',
30
32
  operator: fields.operator || '',
@@ -44,15 +46,16 @@ const report = (tool, fields = {}) => ({
44
46
  });
45
47
 
46
48
  const userTrace = (text, root) => {
47
- const lines = stripAnsi(`${text ?? ''}`).split(/\r?\n/);
48
49
  const kept = [];
49
- for (const line of lines) {
50
- const next = withoutNodeFrame(line);
51
- if (next === null) continue;
52
- kept.push(relativize(indentStack(next), root).trimEnd());
50
+ for (const line of stripAnsi(`${text ?? ''}`).split(/\r?\n/)) {
51
+ const next = traceLine(line, root);
52
+ if (next !== null) kept.push(next.trimEnd());
53
53
  }
54
- while (kept.length && kept[kept.length - 1] === '') kept.pop();
55
- return kept.join('\n').trim();
54
+ let start = 0;
55
+ while (start < kept.length && kept[start].trim() === '') start += 1;
56
+ let end = kept.length;
57
+ while (end > start && kept[end - 1] === '') end -= 1;
58
+ return kept.slice(start, end).join('\n');
56
59
  };
57
60
 
58
61
  const normalizeMessage = (message) => {
@@ -60,33 +63,106 @@ const normalizeMessage = (message) => {
60
63
  return text.replace(/\s+\S+:\d+:\d+$/, '');
61
64
  };
62
65
 
63
- const groupKey = (item) =>
64
- [
66
+ // Build identity from raw diagnostics, before userTrace removes Node frames.
67
+ const errorKey = (fields, stack, root) => {
68
+ const text = stripAnsi(fields.message || '');
69
+ const header = /^([\w.]*Error)(?: \[[^\]]+\])?:\s*/.exec(text);
70
+ const type = fields.type || (header ? header[1] : '');
71
+ const message = header ? text.slice(header[0].length) : text;
72
+ let origin = '';
73
+ for (const row of stripAnsi(stack).split(/\r?\n/)) {
74
+ const line = row.trim().replace(/^at\s+/, '');
75
+ if (/^[\w.]*Error(?: \[[^\]]+\])?:/.test(line)) continue;
76
+ if (line.includes('node:') || !/:\d+:\d+\)?$/.test(line)) continue;
77
+ const location = line.replace(/^.*\((.*:\d+:\d+)\)$/, '$1');
78
+ origin = relativize(location.replace(/file:\/\//g, ''), root).replace(
79
+ /\\/g,
80
+ '/',
81
+ );
82
+ break;
83
+ }
84
+ return JSON.stringify([
85
+ fields.severity || 'error',
86
+ type,
87
+ fields.code || '',
88
+ fields.operator || '',
89
+ message.replace(/\r\n/g, '\n').trim(),
90
+ fields.expected || '',
91
+ fields.actual || '',
92
+ origin,
93
+ ]);
94
+ };
95
+
96
+ const normalizeTrace = (trace) => {
97
+ if (!trace) return '';
98
+ return `${trace}`
99
+ .trim()
100
+ .split(/\r?\n/)
101
+ .map((line) => line.trim())
102
+ .filter(Boolean)
103
+ .map((line) => line.replace(/\\/g, '/').replace(/:\d+(?::\d+)?/g, ':X:Y'))
104
+ .join('\n');
105
+ };
106
+
107
+ const groupKey = (item) => {
108
+ if (item.key) return item.key;
109
+ const trace = normalizeTrace(item.trace);
110
+ return [
65
111
  item.severity,
66
112
  item.code,
67
113
  item.operator,
68
- item.expected,
69
- item.actual,
70
- normalizeMessage(item.message),
114
+ trace || normalizeMessage(item.message),
71
115
  ].join('\0');
116
+ };
117
+
118
+ const relatedProblem = (item, group) => {
119
+ const frames = item.trace.split(/\r?\n/).map((line) => line.trim());
120
+ const sample = (group.traces[0] || '')
121
+ .split(/\r?\n/)
122
+ .map((line) => line.trim());
123
+ let index = 0;
124
+ while (index < frames.length && frames[index] === sample[index]) index++;
125
+ if (index === frames.length && item.name === group.name) return '';
126
+ const rows = [item.name];
127
+ const branch = frames[index];
128
+ const last = frames.at(-1) || '';
129
+ let location = '';
130
+ if (/:\d+:\d+\)?$/.test(last)) {
131
+ const inner = last.replace(/^.*\((.*:\d+:\d+)\)$/, '$1');
132
+ location = inner.replace(/^at\s+/, '');
133
+ }
134
+ if (branch) {
135
+ let frame = branch.replace(/^at\s+/, '');
136
+ if (index === frames.length - 1 && location) frame = location;
137
+ rows.push(frame);
138
+ }
139
+ if (index < frames.length - 1 && location) rows.push(location);
140
+ return rows.filter(Boolean).join(' | ');
141
+ };
72
142
 
73
143
  const groupProblems = (problems) => {
74
144
  const groups = new Map();
75
- const order = [];
76
145
  for (const item of problems) {
77
146
  const key = groupKey(item);
78
147
  let group = groups.get(key);
79
148
  if (!group) {
80
- group = problem({ ...item, count: 0, traces: [], omitted: 0 });
149
+ group = problem({ ...item, count: 0, traces: [], related: [] });
81
150
  groups.set(key, group);
82
- order.push(group);
83
151
  }
84
152
  group.count += 1;
85
- if (item.trace && group.traces.length < 3) group.traces.push(item.trace);
153
+ if (item.trace && !group.traces.length) {
154
+ group.traces.push(item.trace);
155
+ } else if (group.count > 1 && group.related.length < 2) {
156
+ const related = relatedProblem(item, group);
157
+ if (related && !group.related.includes(related)) {
158
+ group.related.push(related);
159
+ }
160
+ }
86
161
  }
162
+ const order = [...groups.values()];
87
163
  for (const group of order) {
88
164
  group.trace = group.traces[0] || '';
89
- group.omitted = Math.max(0, group.count - group.traces.length);
165
+ group.omitted = Math.max(0, group.count - 1 - group.related.length);
90
166
  }
91
167
  return order;
92
168
  };
@@ -98,23 +174,25 @@ const groupReport = (item) =>
98
174
  problems: groupProblems(item.problems),
99
175
  });
100
176
 
101
- const reduceStderr = (text, root) => userTrace(text, root);
177
+ const toolFailed = (item) => {
178
+ const stats = item.summary;
179
+ if (stats.failed || stats.errors) return true;
180
+ return item.problems.some((entry) => entry.severity !== 'warning');
181
+ };
102
182
 
103
- const shortTail = (text) => {
104
- const lines = `${text ?? ''}`.trim().split(/\r?\n/);
105
- if (lines.length <= 20) return lines.join('\n');
106
- return lines.slice(-20).join('\n');
183
+ const keepReport = (item) => {
184
+ const stats = item.summary;
185
+ if (stats.tests || stats.passed || stats.failed || stats.skipped) return true;
186
+ return !!(stats.errors || stats.warnings) || item.problems.length > 0;
107
187
  };
108
188
 
109
189
  const envelope = (reports, exit, stderr) => {
110
- const status = exit === null || exit === undefined ? 1 : exit;
111
- const ok = status === 0;
112
- const items = new Array(reports.length);
113
- for (let i = 0; i < reports.length; i++) {
114
- const item = reports[i];
115
- items[i] = report(item.tool, { ...item, ok });
116
- }
117
- return { ok, exit: status, reports: items, stderr: stderr || '' };
190
+ const status = exit ?? 1;
191
+ const kept = reports.filter(keepReport);
192
+ const items = kept.map((item) =>
193
+ report(item.tool, { ...item, ok: !toolFailed(item) }),
194
+ );
195
+ return { ok: status === 0, exit: status, reports: items, stderr };
118
196
  };
119
197
 
120
198
  module.exports = {
@@ -122,8 +200,7 @@ module.exports = {
122
200
  problem,
123
201
  report,
124
202
  userTrace,
203
+ errorKey,
125
204
  groupReport,
126
- reduceStderr,
127
- shortTail,
128
205
  envelope,
129
206
  };