@pipefy/pipefy-process-coder 0.1.0 → 0.1.1

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 (159) hide show
  1. package/.agents/skills/ppc-pipefy-flow-authoring/SKILL.md +262 -262
  2. package/.agents/skills/ppc-pipefy-flow-authoring/examples/01-main-2-danfe-consulta.json +247 -247
  3. package/.agents/skills/ppc-pipefy-flow-authoring/examples/01-main-2-webhook-retorno-consulta.json +589 -589
  4. package/.agents/skills/ppc-pipefy-flow-authoring/examples/01-main-4-recebimento-barramento.json +1391 -1391
  5. package/.agents/skills/ppc-pipefy-flow-authoring/examples/02-subflow-2-danfe-retorno.json +623 -623
  6. package/.agents/skills/ppc-pipefy-flow-authoring/examples/02-subflow-4-criacao-operacao.json +636 -636
  7. package/.agents/skills/ppc-pipefy-flow-authoring/examples/03-subflow-4-criacao-titulo.json +3642 -3642
  8. package/.agents/skills/ppc-pipefy-flow-authoring/examples/04-subflow-4-criacao-cedente.json +863 -863
  9. package/.agents/skills/ppc-pipefy-flow-authoring/examples/04-subflow-4-criacao-sacado.json +799 -799
  10. package/.agents/skills/ppc-pipefy-flow-authoring/examples/README.md +42 -42
  11. package/.agents/skills/ppc-pipefy-flow-authoring/examples/hml-01-subflow-1-titulo-acompanhamento-cobranca.json +581 -581
  12. package/.agents/skills/ppc-pipefy-flow-authoring/examples/hml-01-subflow-1-titulo-nfe-monitoramento.json +503 -503
  13. package/.agents/skills/ppc-pipefy-flow-authoring/examples/hml-01-subflow-1-titulo-retorno-bancario.json +562 -562
  14. package/.agents/skills/ppc-pipefy-flow-authoring/examples/hml-02-subflow-2-retorno-consulta-cedente.json +557 -557
  15. package/.agents/skills/ppc-pipefy-flow-authoring/examples/hml-02-subflow-2-retorno-consulta-sacado.json +609 -609
  16. package/.agents/skills/ppc-pipefy-pipe-authoring/SKILL.md +146 -146
  17. package/.agents/skills/ppc-pipefy-process-design/SKILL.md +127 -127
  18. package/.agents/skills/ppc-pipefy-workspace/SKILL.md +91 -91
  19. package/AGENTS.md +465 -456
  20. package/README.md +825 -807
  21. package/bin/pipe.js +25 -4
  22. package/dist/apply/adopt.js +128 -0
  23. package/dist/apply/agentops.js +42 -0
  24. package/dist/apply/compile.js +722 -0
  25. package/dist/apply/execute.js +277 -0
  26. package/dist/apply/flowops.js +293 -0
  27. package/dist/apply/idmap.js +233 -0
  28. package/dist/apply/mutations.js +704 -0
  29. package/dist/apply/registry.js +348 -0
  30. package/dist/apply/types.js +18 -0
  31. package/dist/cli/args.js +79 -0
  32. package/dist/cli.js +187 -0
  33. package/dist/codec/flow.js +164 -0
  34. package/dist/codec/pack.js +88 -0
  35. package/dist/codec/roundtrip.js +86 -0
  36. package/dist/codec/unpack.js +181 -0
  37. package/dist/commands/agents.js +145 -0
  38. package/dist/commands/apply.js +982 -0
  39. package/dist/commands/context.js +79 -0
  40. package/dist/commands/create.js +63 -0
  41. package/dist/commands/diff.js +294 -0
  42. package/dist/commands/flows.js +348 -0
  43. package/dist/commands/misc.js +528 -0
  44. package/dist/commands/plan.js +301 -0
  45. package/dist/commands/pull.js +511 -0
  46. package/dist/commands/runs.js +74 -0
  47. package/dist/commands/skills.js +131 -0
  48. package/dist/commands/verify.js +205 -0
  49. package/dist/config.js +125 -0
  50. package/dist/diff/agents.js +80 -0
  51. package/dist/diff/diff.js +983 -0
  52. package/dist/diff/flow.js +258 -0
  53. package/{src/diff/html.ts → dist/diff/html.js} +274 -322
  54. package/dist/diff/render.js +90 -0
  55. package/dist/model/payload.js +84 -0
  56. package/dist/model/tree.js +7 -0
  57. package/dist/model/volatile.js +54 -0
  58. package/dist/pipefy/agents.js +122 -0
  59. package/dist/pipefy/automations.js +155 -0
  60. package/dist/pipefy/capability.js +84 -0
  61. package/dist/pipefy/client.js +204 -0
  62. package/dist/pipefy/discovery.js +148 -0
  63. package/dist/pipefy/internal.js +315 -0
  64. package/dist/pipefy/ipaas.js +269 -0
  65. package/dist/pipefy/reconstruct.js +674 -0
  66. package/dist/pipefy/reference.js +172 -0
  67. package/dist/pipefy/snapshot.js +185 -0
  68. package/dist/pipefy/toolkit.js +158 -0
  69. package/dist/report/integrations.js +165 -0
  70. package/dist/report/run.js +355 -0
  71. package/dist/util/fsx.js +50 -0
  72. package/dist/util/git.js +25 -0
  73. package/dist/util/json.js +56 -0
  74. package/dist/util/log.js +69 -0
  75. package/dist/util/pool.js +42 -0
  76. package/dist/util/slug.js +23 -0
  77. package/dist/util/tui.js +287 -0
  78. package/dist/validate/index.js +86 -0
  79. package/dist/validate/integrity.js +370 -0
  80. package/dist/validate/reference.js +120 -0
  81. package/dist/validate/schema.js +278 -0
  82. package/dist/workspace/agents.js +245 -0
  83. package/{src/workspace/docs.ts → dist/workspace/docs.js} +367 -407
  84. package/dist/workspace/flows.js +149 -0
  85. package/dist/workspace/layout.js +146 -0
  86. package/dist/workspace/lock.js +83 -0
  87. package/dist/workspace/read.js +168 -0
  88. package/dist/workspace/reference.js +24 -0
  89. package/dist/workspace/stamp.js +234 -0
  90. package/dist/workspace/write.js +194 -0
  91. package/package.json +37 -35
  92. package/src/apply/adopt.ts +0 -150
  93. package/src/apply/agentops.ts +0 -71
  94. package/src/apply/compile.ts +0 -875
  95. package/src/apply/execute.ts +0 -336
  96. package/src/apply/flowops.ts +0 -399
  97. package/src/apply/idmap.ts +0 -241
  98. package/src/apply/mutations.ts +0 -955
  99. package/src/apply/registry.ts +0 -430
  100. package/src/apply/types.ts +0 -134
  101. package/src/cli/args.ts +0 -88
  102. package/src/cli.ts +0 -211
  103. package/src/codec/flow.ts +0 -199
  104. package/src/codec/pack.ts +0 -103
  105. package/src/codec/roundtrip.ts +0 -94
  106. package/src/codec/unpack.ts +0 -198
  107. package/src/commands/agents.ts +0 -184
  108. package/src/commands/apply.ts +0 -1144
  109. package/src/commands/context.ts +0 -119
  110. package/src/commands/create.ts +0 -79
  111. package/src/commands/diff.ts +0 -314
  112. package/src/commands/flows.ts +0 -414
  113. package/src/commands/misc.ts +0 -644
  114. package/src/commands/plan.ts +0 -331
  115. package/src/commands/pull.ts +0 -567
  116. package/src/commands/runs.ts +0 -83
  117. package/src/commands/skills.ts +0 -137
  118. package/src/commands/verify.ts +0 -253
  119. package/src/config.ts +0 -168
  120. package/src/diff/agents.ts +0 -122
  121. package/src/diff/diff.ts +0 -1130
  122. package/src/diff/flow.ts +0 -318
  123. package/src/diff/render.ts +0 -101
  124. package/src/model/payload.ts +0 -154
  125. package/src/model/tree.ts +0 -99
  126. package/src/model/volatile.ts +0 -55
  127. package/src/pipefy/agents.ts +0 -165
  128. package/src/pipefy/automations.ts +0 -219
  129. package/src/pipefy/capability.ts +0 -119
  130. package/src/pipefy/client.ts +0 -267
  131. package/src/pipefy/discovery.ts +0 -209
  132. package/src/pipefy/internal.ts +0 -380
  133. package/src/pipefy/ipaas.ts +0 -365
  134. package/src/pipefy/reconstruct.ts +0 -775
  135. package/src/pipefy/reference.ts +0 -251
  136. package/src/pipefy/snapshot.ts +0 -245
  137. package/src/pipefy/toolkit.ts +0 -200
  138. package/src/report/integrations.ts +0 -231
  139. package/src/report/run.ts +0 -475
  140. package/src/util/fsx.ts +0 -45
  141. package/src/util/git.ts +0 -32
  142. package/src/util/json.ts +0 -55
  143. package/src/util/log.ts +0 -76
  144. package/src/util/pool.ts +0 -48
  145. package/src/util/slug.ts +0 -26
  146. package/src/util/tui.ts +0 -335
  147. package/src/validate/index.ts +0 -123
  148. package/src/validate/integrity.ts +0 -387
  149. package/src/validate/reference.ts +0 -136
  150. package/src/validate/schema.ts +0 -328
  151. package/src/workspace/agents.ts +0 -290
  152. package/src/workspace/flows.ts +0 -191
  153. package/src/workspace/layout.ts +0 -165
  154. package/src/workspace/lock.ts +0 -148
  155. package/src/workspace/read.ts +0 -165
  156. package/src/workspace/reference.ts +0 -24
  157. package/src/workspace/stamp.ts +0 -301
  158. package/src/workspace/write.ts +0 -225
  159. /package/{src → dist}/pipefy/toolkit_bearer.py +0 -0
@@ -0,0 +1,287 @@
1
+ import { bold, claimLogSink, dim, green, red, yellow, blue, magenta } from "./log.js";
2
+ /**
3
+ * A small live progress display for the long commands — pull, apply, dry-run.
4
+ *
5
+ * Two renderers behind one interface, chosen by whether stderr is a terminal:
6
+ *
7
+ * - **live**: redraws a block of lines in place, with a spinner and elapsed
8
+ * times, so a 63-second snapshot wait looks like work rather than a hang.
9
+ * - **plain**: one line per state change, no cursor control, no repainting.
10
+ * This is what CI, `2>file`, and a backgrounded run get — and it is not an
11
+ * afterthought, since that is how this tool is most often watched.
12
+ *
13
+ * Everything goes to stderr, leaving stdout clean for `--json`.
14
+ */
15
+ const ESC = '\x1b[';
16
+ const HIDE_CURSOR = `${ESC}?25l`;
17
+ const SHOW_CURSOR = `${ESC}?25h`;
18
+ const CLEAR_LINE = `${ESC}2K`;
19
+ const cursorUp = (n) => (n > 0 ? `${ESC}${n}A` : '');
20
+ /** Braille spinner: 8 frames, reads as motion even at 80ms. */
21
+ const FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧'];
22
+ const MARK = {
23
+ pending: dim,
24
+ running: blue,
25
+ done: green,
26
+ failed: red,
27
+ skipped: yellow,
28
+ };
29
+ const GLYPH = {
30
+ pending: '·',
31
+ running: '',
32
+ done: '+',
33
+ failed: 'x',
34
+ skipped: '-',
35
+ };
36
+ const secs = (ms) => `${(ms / 1000).toFixed(ms < 10_000 ? 1 : 0)}s`;
37
+ export class Progress {
38
+ tasks = [];
39
+ byId = new Map();
40
+ title;
41
+ stream;
42
+ live;
43
+ frame = 0;
44
+ timer = null;
45
+ linesDrawn = 0;
46
+ startedAt = Date.now();
47
+ finished = false;
48
+ /** Extra lines under the task list: warnings that must survive a repaint. */
49
+ notes = [];
50
+ cleanup = null;
51
+ /**
52
+ * Renders are coalesced onto the spinner tick rather than run on every state
53
+ * change. Building a 200-step plan calls add() 200 times, and repainting the
54
+ * whole block each time is both wasted work and visible flicker; a 90ms delay
55
+ * before a change appears is not perceptible.
56
+ */
57
+ dirty = false;
58
+ constructor(opts) {
59
+ this.title = opts.title;
60
+ this.stream = opts.stream ?? process.stderr;
61
+ this.live = !opts.plain && Boolean(this.stream.isTTY) && !process.env.PPC_NO_TUI && !process.env.NO_TUI;
62
+ }
63
+ /** Begin painting. Safe to call once; a second call is ignored. */
64
+ start() {
65
+ if (this.timer || this.finished)
66
+ return;
67
+ /**
68
+ * Claimed in both modes, so the semantics do not depend on whether stderr
69
+ * is a terminal. Live, it stops an interleaved write garbling a repaint;
70
+ * plain, it stops the task list and the old progress chatter saying the same
71
+ * thing twice.
72
+ */
73
+ claimLogSink({ note: (text) => this.note(text) });
74
+ if (!this.live) {
75
+ this.stream.write(bold(this.title) + '\n');
76
+ return;
77
+ }
78
+ this.stream.write(HIDE_CURSOR);
79
+ // Leaving the cursor hidden after a Ctrl-C would break the user's terminal.
80
+ const restore = () => this.stop();
81
+ process.once('SIGINT', restore);
82
+ process.once('SIGTERM', restore);
83
+ process.once('exit', restore);
84
+ this.cleanup = () => {
85
+ process.off('SIGINT', restore);
86
+ process.off('SIGTERM', restore);
87
+ process.off('exit', restore);
88
+ };
89
+ this.timer = setInterval(() => {
90
+ this.frame++;
91
+ // The spinner has to move even when nothing changed, so a running task
92
+ // always repaints; an idle list repaints only when it is stale.
93
+ if (this.dirty || this.tasks.some((t) => t.state === 'running'))
94
+ this.render();
95
+ }, 90);
96
+ this.timer.unref?.();
97
+ this.render();
98
+ }
99
+ add(task) {
100
+ const t = { state: 'pending', ...task };
101
+ this.tasks.push(t);
102
+ this.byId.set(t.id, t);
103
+ this.touch();
104
+ return t;
105
+ }
106
+ update(id, patch) {
107
+ const t = this.byId.get(id);
108
+ if (!t)
109
+ return;
110
+ const before = t.state;
111
+ Object.assign(t, patch);
112
+ if (patch.state === 'running' && !t.startedAt) {
113
+ t.startedAt = Date.now();
114
+ // Seeded so the first heartbeat is one interval away, not immediate:
115
+ // otherwise every task announces its first stage and fast work is noise.
116
+ this.lastHeartbeat.set(t.id, t.startedAt);
117
+ }
118
+ if (patch.state && patch.state !== 'running' && !t.endedAt)
119
+ t.endedAt = Date.now();
120
+ if (!this.live) {
121
+ if (patch.state && patch.state !== before)
122
+ this.onPlainTransition(t);
123
+ else if (patch.detail !== undefined)
124
+ this.plainHeartbeat(t);
125
+ }
126
+ else {
127
+ this.touch();
128
+ }
129
+ }
130
+ /**
131
+ * One line per task in plain mode, plus a heartbeat for slow work.
132
+ *
133
+ * Every task reports its outcome, so nothing starts and then vanishes from the
134
+ * log. Work that announces a stage while running — a snapshot poll, a download
135
+ * — also reports that, throttled, because a log that goes silent for 63
136
+ * seconds is indistinguishable from a hang. A 200-step plan of fast calls is
137
+ * 200 lines.
138
+ */
139
+ static HEARTBEAT_MS = 15_000;
140
+ lastHeartbeat = new Map();
141
+ onPlainTransition(t) {
142
+ // Terminal states always speak.
143
+ if (t.state === 'done' || t.state === 'failed' || t.state === 'skipped')
144
+ this.emitPlain(t);
145
+ }
146
+ plainHeartbeat(t) {
147
+ if (this.live || t.state !== 'running')
148
+ return;
149
+ const now = Date.now();
150
+ const last = this.lastHeartbeat.get(t.id) ?? 0;
151
+ if (now - last < Progress.HEARTBEAT_MS)
152
+ return;
153
+ this.lastHeartbeat.set(t.id, now);
154
+ this.emitPlain(t);
155
+ }
156
+ /**
157
+ * Mark the frame stale. Before start() there is no timer, so paint at once —
158
+ * that is what makes a short-lived Progress still show something.
159
+ */
160
+ touch() {
161
+ if (!this.live)
162
+ return;
163
+ this.dirty = true;
164
+ if (!this.timer)
165
+ this.render();
166
+ }
167
+ /** Whether a task with this id already exists. */
168
+ has(id) {
169
+ return this.byId.has(id);
170
+ }
171
+ /** The last few only: a frame that grows without bound scrolls the terminal. */
172
+ static MAX_NOTES = 5;
173
+ /** A line that stays visible under the task list. */
174
+ note(text) {
175
+ this.notes.push(text);
176
+ if (this.notes.length > Progress.MAX_NOTES)
177
+ this.notes.splice(0, this.notes.length - Progress.MAX_NOTES);
178
+ if (!this.live)
179
+ this.stream.write(text + '\n');
180
+ else
181
+ this.render();
182
+ }
183
+ /** Stop painting and leave the final frame on screen. */
184
+ stop(summary) {
185
+ if (this.finished)
186
+ return;
187
+ this.finished = true;
188
+ if (this.timer)
189
+ clearInterval(this.timer);
190
+ this.timer = null;
191
+ this.cleanup?.();
192
+ claimLogSink(null);
193
+ if (this.live) {
194
+ this.render(true);
195
+ this.stream.write(SHOW_CURSOR);
196
+ }
197
+ if (summary)
198
+ this.stream.write(summary + '\n');
199
+ }
200
+ get elapsed() {
201
+ return Date.now() - this.startedAt;
202
+ }
203
+ // ── rendering ─────────────────────────────────────────────────────────────
204
+ emitPlain(t) {
205
+ const paint = MARK[t.state];
206
+ const indent = ' '.repeat(t.depth ?? 0);
207
+ const tail = t.detail ? ` ${dim(t.detail)}` : '';
208
+ const took = t.startedAt && t.endedAt ? ` ${dim(secs(t.endedAt - t.startedAt))}` : '';
209
+ this.stream.write(`${indent}${paint(GLYPH[t.state] || '>')} ${t.label}${tail}${took}\n`);
210
+ }
211
+ line(t) {
212
+ const paint = MARK[t.state];
213
+ const glyph = t.state === 'running' ? blue(FRAMES[this.frame % FRAMES.length]) : paint(GLYPH[t.state]);
214
+ const indent = ' '.repeat(t.depth ?? 0);
215
+ const label = t.state === 'pending' ? dim(t.label) : t.label;
216
+ const tags = t.tags?.length ? ' ' + t.tags.map((g) => dim(`[${g}]`)).join(' ') : '';
217
+ let timing = '';
218
+ if (t.state === 'running' && t.startedAt)
219
+ timing = ` ${dim(secs(Date.now() - t.startedAt))}`;
220
+ else if (t.startedAt && t.endedAt)
221
+ timing = ` ${dim(secs(t.endedAt - t.startedAt))}`;
222
+ const detail = t.detail ? ` ${dim('— ' + t.detail)}` : '';
223
+ return `${indent} ${glyph} ${label}${tags}${detail}${timing}`;
224
+ }
225
+ render(final = false) {
226
+ if (!this.live)
227
+ return;
228
+ const counts = this.tasks.reduce((a, t) => {
229
+ a[t.state]++;
230
+ return a;
231
+ }, { pending: 0, running: 0, done: 0, failed: 0, skipped: 0 });
232
+ const head = bold(this.title) +
233
+ dim(` · ${counts.done}/${this.tasks.length}` +
234
+ (counts.failed ? ` · ${counts.failed} failed` : '') +
235
+ (counts.skipped ? ` · ${counts.skipped} skipped` : '') +
236
+ ` · ${secs(this.elapsed)}`);
237
+ /**
238
+ * Only the tail is kept when the list is long: a 200-step plan must not
239
+ * scroll the terminal on every frame. Finished work above the window is
240
+ * summarised in the header, which is the number people actually watch.
241
+ */
242
+ const MAX = 14;
243
+ const visible = this.tasks.length <= MAX
244
+ ? this.tasks
245
+ : (() => {
246
+ const active = this.tasks.findIndex((t) => t.state === 'running' || t.state === 'pending');
247
+ const from = Math.max(0, Math.min(active === -1 ? this.tasks.length - MAX : active - 3, this.tasks.length - MAX));
248
+ return this.tasks.slice(from, from + MAX);
249
+ })();
250
+ const hiddenAbove = this.tasks.indexOf(visible[0]);
251
+ const hiddenBelow = this.tasks.length - (hiddenAbove + visible.length);
252
+ const body = [
253
+ head,
254
+ ...(hiddenAbove > 0 ? [dim(` … ${hiddenAbove} earlier`)] : []),
255
+ ...visible.map((t) => this.line(t)),
256
+ ...(hiddenBelow > 0 ? [dim(` … ${hiddenBelow} more`)] : []),
257
+ ...this.notes.map((n) => ` ${n}`),
258
+ ];
259
+ // Repaint in place: up N lines, then clear-and-write each one.
260
+ const out = cursorUp(this.linesDrawn) + body.map((l) => CLEAR_LINE + l).join('\n') + '\n';
261
+ this.stream.write(out);
262
+ this.linesDrawn = final ? 0 : body.length;
263
+ }
264
+ }
265
+ /**
266
+ * A single long wait with no sub-steps — a snapshot poll, a download.
267
+ * Returns a function to finish it.
268
+ */
269
+ export const spinner = (title, opts = {}) => {
270
+ const p = new Progress({ title, plain: opts.plain });
271
+ const id = 'only';
272
+ p.add({ id, label: title, state: 'running', startedAt: Date.now() });
273
+ p.start();
274
+ return {
275
+ detail: (text) => p.update(id, { detail: text }),
276
+ done: (label) => {
277
+ p.update(id, { state: 'done', ...(label ? { label } : {}) });
278
+ p.stop();
279
+ },
280
+ fail: (label) => {
281
+ p.update(id, { state: 'failed', ...(label ? { label } : {}) });
282
+ p.stop();
283
+ },
284
+ };
285
+ };
286
+ export const isLive = () => Boolean(process.stderr.isTTY) && !process.env.PPC_NO_TUI && !process.env.NO_TUI;
287
+ export { magenta };
@@ -0,0 +1,86 @@
1
+ import { green, red, yellow, dim, bold } from "../util/log.js";
2
+ import { validateSchema } from "./schema.js";
3
+ import { validateIntegrity, summarise } from "./integrity.js";
4
+ import { validateAgainstReference } from "./reference.js";
5
+ export { summarise } from "./integrity.js";
6
+ /**
7
+ * Entity plus message, deliberately *not* including the file.
8
+ *
9
+ * Renaming a phase changes the file label of every finding inside it, so a
10
+ * file-keyed fingerprint would report a pre-existing problem as newly
11
+ * introduced the moment the FDE renames something nearby.
12
+ */
13
+ const fingerprint = (f) => `${f.entity}::${f.message}`;
14
+ export const validateTree = (tree, opts = {}) => {
15
+ const findings = [
16
+ ...validateSchema(tree),
17
+ ...validateIntegrity(tree, { knownRepoIds: opts.knownRepoIds, agents: opts.agents }),
18
+ ...validateAgainstReference(tree, opts.reference ?? null),
19
+ ];
20
+ if (!opts.baseline)
21
+ return findings;
22
+ /**
23
+ * A multiset, not a set: if the baseline had one dangling reference of a kind
24
+ * and the edit introduced a second, the second must still be reported.
25
+ */
26
+ const budget = new Map();
27
+ for (const f of [
28
+ ...validateSchema(opts.baseline),
29
+ ...validateIntegrity(opts.baseline, { knownRepoIds: opts.knownRepoIds, agents: opts.agents }),
30
+ ...validateAgainstReference(opts.baseline, opts.reference ?? null),
31
+ ]) {
32
+ const key = fingerprint(f);
33
+ budget.set(key, (budget.get(key) ?? 0) + 1);
34
+ }
35
+ return findings.map((f) => {
36
+ const key = fingerprint(f);
37
+ const left = budget.get(key) ?? 0;
38
+ if (left <= 0)
39
+ return f;
40
+ budget.set(key, left - 1);
41
+ return { ...f, severity: 'warning', preExisting: true };
42
+ });
43
+ };
44
+ /** Errors the edit introduced — what plan and apply actually gate on. */
45
+ export const blockingErrors = (findings) => findings.filter((f) => f.severity === 'error' && !f.preExisting);
46
+ export const renderFindings = (findings, label) => {
47
+ const introduced = findings.filter((f) => !f.preExisting);
48
+ const preExisting = findings.filter((f) => f.preExisting);
49
+ const { errors, warnings } = summarise(introduced);
50
+ const lines = [];
51
+ if (!findings.length) {
52
+ return green(`+ ${label}: valid`);
53
+ }
54
+ lines.push(bold(label));
55
+ const group = (items) => {
56
+ const byFile = new Map();
57
+ for (const f of items) {
58
+ const key = f.file ?? f.entity;
59
+ const arr = byFile.get(key) ?? [];
60
+ arr.push(f);
61
+ byFile.set(key, arr);
62
+ }
63
+ for (const [file, items2] of byFile) {
64
+ lines.push(` ${dim(file)}`);
65
+ for (const f of items2) {
66
+ const mark = f.severity === 'error' ? red('x') : yellow('!');
67
+ lines.push(` ${mark} ${f.message}`);
68
+ if (f.fix)
69
+ lines.push(` ${dim('-> ' + f.fix)}`);
70
+ }
71
+ }
72
+ };
73
+ if (introduced.length)
74
+ group(introduced);
75
+ if (preExisting.length) {
76
+ lines.push('');
77
+ lines.push(dim(` ${preExisting.length} pre-existing issue${preExisting.length === 1 ? '' : 's'} — present in the last pull, not caused by these edits:`));
78
+ for (const f of preExisting)
79
+ lines.push(` ${dim('·')} ${dim(`${f.entity}: ${f.message}`)}`);
80
+ }
81
+ lines.push('');
82
+ lines.push(`${errors ? red(`${errors} error${errors === 1 ? '' : 's'}`) : green('0 errors')}, ` +
83
+ `${warnings ? yellow(`${warnings} warning${warnings === 1 ? '' : 's'}`) : '0 warnings'}` +
84
+ `${preExisting.length ? dim(`, ${preExisting.length} pre-existing`) : ''}`);
85
+ return lines.join('\n');
86
+ };