@pipefy/pipefy-process-coder 0.1.0 → 0.1.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.
Files changed (162) 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 +476 -456
  20. package/LICENSE +202 -0
  21. package/NOTICE +11 -0
  22. package/README.md +830 -808
  23. package/bin/pipe.js +25 -4
  24. package/dist/apply/adopt.js +128 -0
  25. package/dist/apply/agentops.js +42 -0
  26. package/dist/apply/compile.js +722 -0
  27. package/dist/apply/execute.js +277 -0
  28. package/dist/apply/flowops.js +293 -0
  29. package/dist/apply/idmap.js +233 -0
  30. package/dist/apply/mutations.js +704 -0
  31. package/dist/apply/registry.js +348 -0
  32. package/dist/apply/types.js +18 -0
  33. package/dist/cli/args.js +79 -0
  34. package/dist/cli.js +196 -0
  35. package/dist/codec/flow.js +164 -0
  36. package/dist/codec/pack.js +88 -0
  37. package/dist/codec/roundtrip.js +86 -0
  38. package/dist/codec/unpack.js +181 -0
  39. package/dist/commands/agents.js +145 -0
  40. package/dist/commands/apply.js +982 -0
  41. package/dist/commands/context.js +79 -0
  42. package/dist/commands/create.js +63 -0
  43. package/dist/commands/diff.js +294 -0
  44. package/dist/commands/flows.js +348 -0
  45. package/dist/commands/misc.js +562 -0
  46. package/dist/commands/plan.js +301 -0
  47. package/dist/commands/pull.js +549 -0
  48. package/dist/commands/runs.js +74 -0
  49. package/dist/commands/skills.js +131 -0
  50. package/dist/commands/verify.js +205 -0
  51. package/dist/config.js +125 -0
  52. package/dist/diff/agents.js +80 -0
  53. package/dist/diff/diff.js +983 -0
  54. package/dist/diff/flow.js +258 -0
  55. package/{src/diff/html.ts → dist/diff/html.js} +274 -322
  56. package/dist/diff/render.js +90 -0
  57. package/dist/model/payload.js +84 -0
  58. package/dist/model/tree.js +7 -0
  59. package/dist/model/volatile.js +54 -0
  60. package/dist/pipefy/agents.js +131 -0
  61. package/dist/pipefy/automations.js +155 -0
  62. package/dist/pipefy/capability.js +84 -0
  63. package/dist/pipefy/client.js +204 -0
  64. package/dist/pipefy/discovery.js +148 -0
  65. package/dist/pipefy/internal.js +406 -0
  66. package/dist/pipefy/ipaas.js +269 -0
  67. package/dist/pipefy/reconstruct.js +676 -0
  68. package/dist/pipefy/reference.js +172 -0
  69. package/dist/pipefy/snapshot.js +185 -0
  70. package/dist/pipefy/toolkit.js +174 -0
  71. package/{src → dist}/pipefy/toolkit_bearer.py +60 -14
  72. package/dist/report/integrations.js +165 -0
  73. package/dist/report/run.js +356 -0
  74. package/dist/util/fsx.js +50 -0
  75. package/dist/util/git.js +25 -0
  76. package/dist/util/json.js +56 -0
  77. package/dist/util/log.js +69 -0
  78. package/dist/util/pool.js +42 -0
  79. package/dist/util/slug.js +23 -0
  80. package/dist/util/tui.js +287 -0
  81. package/dist/validate/index.js +86 -0
  82. package/dist/validate/integrity.js +370 -0
  83. package/dist/validate/reference.js +120 -0
  84. package/dist/validate/schema.js +278 -0
  85. package/dist/version.js +18 -0
  86. package/dist/workspace/agents.js +245 -0
  87. package/{src/workspace/docs.ts → dist/workspace/docs.js} +367 -407
  88. package/dist/workspace/flows.js +149 -0
  89. package/dist/workspace/layout.js +146 -0
  90. package/dist/workspace/lock.js +83 -0
  91. package/dist/workspace/read.js +168 -0
  92. package/dist/workspace/reference.js +24 -0
  93. package/dist/workspace/stamp.js +234 -0
  94. package/dist/workspace/write.js +194 -0
  95. package/package.json +38 -35
  96. package/src/apply/adopt.ts +0 -150
  97. package/src/apply/agentops.ts +0 -71
  98. package/src/apply/compile.ts +0 -875
  99. package/src/apply/execute.ts +0 -336
  100. package/src/apply/flowops.ts +0 -399
  101. package/src/apply/idmap.ts +0 -241
  102. package/src/apply/mutations.ts +0 -955
  103. package/src/apply/registry.ts +0 -430
  104. package/src/apply/types.ts +0 -134
  105. package/src/cli/args.ts +0 -88
  106. package/src/cli.ts +0 -211
  107. package/src/codec/flow.ts +0 -199
  108. package/src/codec/pack.ts +0 -103
  109. package/src/codec/roundtrip.ts +0 -94
  110. package/src/codec/unpack.ts +0 -198
  111. package/src/commands/agents.ts +0 -184
  112. package/src/commands/apply.ts +0 -1144
  113. package/src/commands/context.ts +0 -119
  114. package/src/commands/create.ts +0 -79
  115. package/src/commands/diff.ts +0 -314
  116. package/src/commands/flows.ts +0 -414
  117. package/src/commands/misc.ts +0 -644
  118. package/src/commands/plan.ts +0 -331
  119. package/src/commands/pull.ts +0 -567
  120. package/src/commands/runs.ts +0 -83
  121. package/src/commands/skills.ts +0 -137
  122. package/src/commands/verify.ts +0 -253
  123. package/src/config.ts +0 -168
  124. package/src/diff/agents.ts +0 -122
  125. package/src/diff/diff.ts +0 -1130
  126. package/src/diff/flow.ts +0 -318
  127. package/src/diff/render.ts +0 -101
  128. package/src/model/payload.ts +0 -154
  129. package/src/model/tree.ts +0 -99
  130. package/src/model/volatile.ts +0 -55
  131. package/src/pipefy/agents.ts +0 -165
  132. package/src/pipefy/automations.ts +0 -219
  133. package/src/pipefy/capability.ts +0 -119
  134. package/src/pipefy/client.ts +0 -267
  135. package/src/pipefy/discovery.ts +0 -209
  136. package/src/pipefy/internal.ts +0 -380
  137. package/src/pipefy/ipaas.ts +0 -365
  138. package/src/pipefy/reconstruct.ts +0 -775
  139. package/src/pipefy/reference.ts +0 -251
  140. package/src/pipefy/snapshot.ts +0 -245
  141. package/src/pipefy/toolkit.ts +0 -200
  142. package/src/report/integrations.ts +0 -231
  143. package/src/report/run.ts +0 -475
  144. package/src/util/fsx.ts +0 -45
  145. package/src/util/git.ts +0 -32
  146. package/src/util/json.ts +0 -55
  147. package/src/util/log.ts +0 -76
  148. package/src/util/pool.ts +0 -48
  149. package/src/util/slug.ts +0 -26
  150. package/src/util/tui.ts +0 -335
  151. package/src/validate/index.ts +0 -123
  152. package/src/validate/integrity.ts +0 -387
  153. package/src/validate/reference.ts +0 -136
  154. package/src/validate/schema.ts +0 -328
  155. package/src/workspace/agents.ts +0 -290
  156. package/src/workspace/flows.ts +0 -191
  157. package/src/workspace/layout.ts +0 -165
  158. package/src/workspace/lock.ts +0 -148
  159. package/src/workspace/read.ts +0 -165
  160. package/src/workspace/reference.ts +0 -24
  161. package/src/workspace/stamp.ts +0 -301
  162. package/src/workspace/write.ts +0 -225
@@ -0,0 +1,348 @@
1
+ import { dim, info, warn } from "../util/log.js";
2
+ import { Progress } from "../util/tui.js";
3
+ import { assertFlowRoundTrip, unlinkFlow } from "../codec/flow.js";
4
+ import { readFlows, writeFlows, writeFlowBaseline, readFlowBaseline, unknownFlows, flowIdOf, } from "../workspace/flows.js";
5
+ import { diffFlows } from "../diff/flow.js";
6
+ import { buildFlowOperations, compileFlows } from "../apply/flowops.js";
7
+ import { IpaasClient, IpaasUnavailable } from "../pipefy/ipaas.js";
8
+ /**
9
+ * The flow half of the pipe loop: read on pull, diff, compile, apply.
10
+ *
11
+ * Kept in one module because the flow path is small and shares nothing with the
12
+ * pipe mutation compiler — the operations are per node against a different
13
+ * endpoint, and pretending otherwise would force both through a registry that
14
+ * describes GraphQL mutations.
15
+ */
16
+ /** Read a pipe's whole iPaaS workspace, or record why it could not be read. */
17
+ export const pullFlowsFor = async (client, pipeId, opts = {}) => {
18
+ const probe = await client.available(pipeId);
19
+ if (!probe.ok) {
20
+ /**
21
+ * Not an error. iPaaS is gated per organization and per pipe, so "no token"
22
+ * is the normal answer for most pipes — and it must never be read as "this
23
+ * pipe has no flows", which is what would license deleting them later.
24
+ */
25
+ return unknownFlows(probe.reason);
26
+ }
27
+ const { projectId } = probe.session;
28
+ const rows = await client.listFlows(pipeId);
29
+ const flows = [];
30
+ const notes = [];
31
+ for (const row of rows) {
32
+ const full = await client.getFlow(pipeId, String(row['id']));
33
+ if (!opts.skipRoundTrip) {
34
+ const rt = assertFlowRoundTrip(full);
35
+ if (!rt.ok) {
36
+ // The fold is not lossless for this flow, so the split files would not
37
+ // rebuild it. Reported, and the raw row is what the diff will use.
38
+ notes.push(`flow ${row['id']}: codec round trip is not clean (${rt.differences
39
+ .map((d) => `${d.where}: ${d.detail}`)
40
+ .join('; ')})`);
41
+ }
42
+ }
43
+ flows.push(unlinkFlow(full));
44
+ }
45
+ const [connections, pieces, runs] = await Promise.all([
46
+ client.listConnections(pipeId),
47
+ client.listPieces(pipeId),
48
+ client.listRuns(pipeId),
49
+ ]);
50
+ return {
51
+ meta: {
52
+ coverage: 'complete',
53
+ projectId,
54
+ readAt: new Date().toISOString(),
55
+ flows: flows.length,
56
+ connections: connections.length,
57
+ pieces: Object.keys(pieces).length,
58
+ notes,
59
+ },
60
+ flows,
61
+ connections,
62
+ pieces,
63
+ runs,
64
+ };
65
+ };
66
+ /** Pull and write, with a line per flow. Returns what was read. */
67
+ export const pullAndWriteFlows = async (client, pipeId, repoDir, opts = {}) => {
68
+ const section = await pullFlowsFor(client, pipeId, opts);
69
+ await writeFlows(repoDir, section);
70
+ /**
71
+ * And record it as the baseline.
72
+ *
73
+ * Without this the flow diff had no past to compare against and re-read the
74
+ * pipe live on every invocation — so there was no offline diff, and no record
75
+ * of what a pull actually brought back. The pipe half has stored its baseline
76
+ * since the first version; this is the flow half catching up.
77
+ */
78
+ if (opts.workspaceRoot)
79
+ await writeFlowBaseline(opts.workspaceRoot, pipeId, section);
80
+ return section;
81
+ };
82
+ /**
83
+ * The flows diff: what is on disk against what was recorded at the last pull.
84
+ *
85
+ * Mirrors the pipe half — `baseline` needs no network, `live` re-reads the pipe.
86
+ * The default is the baseline, so `pipe diff` stays offline and answers the same
87
+ * question for both halves of a process: what did *I* change.
88
+ *
89
+ * `live` is what an apply uses, because what matters there is the current state
90
+ * of the pipe, not the state someone pulled an hour ago.
91
+ */
92
+ export const diffFlowsFor = async (client, pipeId, repoDir, opts) => {
93
+ const authored = await readFlows(repoDir);
94
+ if ((opts.against ?? 'baseline') === 'baseline') {
95
+ const before = await readFlowBaseline(opts.workspaceRoot, pipeId);
96
+ if (!before) {
97
+ const reason = 'no flow baseline for this repo — re-pull to record one, or use --against live';
98
+ return { changeSet: { changes: [], skipped: [{ reason }] }, before: null, authored };
99
+ }
100
+ return { changeSet: diffFlows(before, authored), before, authored };
101
+ }
102
+ if (!client) {
103
+ const reason = 'a live flow diff needs the network, and no client was supplied';
104
+ return { changeSet: { changes: [], skipped: [{ reason }] }, before: null, authored };
105
+ }
106
+ const live = await pullFlowsFor(client, pipeId, { skipRoundTrip: true });
107
+ return { changeSet: diffFlows(live, authored), before: live, authored };
108
+ };
109
+ /** The flows diff against the live pipe. What an apply compiles from. */
110
+ export const diffFlowsAgainstLive = async (client, pipeId, repoDir) => {
111
+ const authored = await readFlows(repoDir);
112
+ const live = await pullFlowsFor(client, pipeId, { skipRoundTrip: true });
113
+ return { changeSet: diffFlows(live, authored), live, authored };
114
+ };
115
+ /**
116
+ * The revert plan, given both sides already read — pure, no network, so it
117
+ * is the part `test/flows-revert.test.ts` can actually exercise without a
118
+ * live `IpaasClient`.
119
+ *
120
+ * Direction is the mirror of an ordinary apply: `live` is the *current*
121
+ * state (baseline), `before` is the old pre-apply one (intended target). A
122
+ * node the failed apply added and `before` never had comes out as a delete;
123
+ * one it removed comes out as a create — the same diff engine, just pointed
124
+ * backwards.
125
+ */
126
+ export const buildFlowRevertPlan = (live, before) => {
127
+ const changeSet = diffFlows(live, before);
128
+ /**
129
+ * Allowed unconditionally, unlike an ordinary apply's --allow-flow-enable
130
+ * gate: putting a flow back to enabled is restoring what was already true
131
+ * before this apply touched it, not arming something new. The
132
+ * confirmation prompt renders the plan either way, so an enable is never
133
+ * silent.
134
+ */
135
+ return planFlows(changeSet, { allowArm: true, connections: live.connections });
136
+ };
137
+ /**
138
+ * What reverting to a pre-apply flow state would take — computed, not sent.
139
+ *
140
+ * `before` is the live read `diffFlowsAgainstLive` already took right
141
+ * before this apply's own diff was computed — the only rollback point flows
142
+ * have at all, since Pipefy's snapshot restore never covered them (it is
143
+ * pipe structure only; see AGENTS.md). `live` here is read fresh, because
144
+ * some steps may have landed since `before` was taken.
145
+ */
146
+ export const planFlowRevert = async (client, pipeId, before) => {
147
+ const live = await pullFlowsFor(client, pipeId, { skipRoundTrip: true });
148
+ return { plan: buildFlowRevertPlan(live, before), live };
149
+ };
150
+ export const planFlows = (cs, opts = {}) => compileFlows(cs, {
151
+ allowArm: opts.allowArm,
152
+ connections: new Map((opts.connections ?? []).map((c) => [
153
+ c.externalId,
154
+ // A placeholder is reported as its own status so the compiler can refuse to
155
+ // arm a flow that depends on one.
156
+ { pieceName: c.pieceName, status: c._placeholder ? 'PLACEHOLDER' : c.status },
157
+ ])),
158
+ });
159
+ export const renderFlowPlan = (plan) => {
160
+ const lines = [];
161
+ if (plan.blocked.length) {
162
+ lines.push('');
163
+ lines.push(` ${plan.blocked.length} flow change(s) cannot be applied:`);
164
+ for (const b of plan.blocked) {
165
+ lines.push(` x ${b.label}`);
166
+ lines.push(` ${b.reason}`);
167
+ }
168
+ }
169
+ if (!plan.steps.length) {
170
+ if (!plan.blocked.length)
171
+ lines.push(' flows: nothing to apply');
172
+ return lines.join('\n');
173
+ }
174
+ lines.push('');
175
+ lines.push(` ${plan.steps.length} flow step(s)`);
176
+ for (const s of plan.steps) {
177
+ const tags = [s.destructive ? 'destructive' : '', s.arms ? 'ARMS THE FLOW' : ''].filter(Boolean);
178
+ lines.push(` ${s.id} ${s.description}${tags.length ? ` ${tags.join(' ')}` : ''}`);
179
+ }
180
+ return lines.join('\n');
181
+ };
182
+ /**
183
+ * Execute the flow steps.
184
+ *
185
+ * A created flow is laid down in one step from the outside — create, then trigger,
186
+ * then each node in execution order — because the flow id does not exist until the
187
+ * create returns, and every subsequent operation needs it. The alternative is
188
+ * placeholders in a second id space, which buys nothing here: the whole sequence
189
+ * is one flow's contents and either all of it lands or the flow is visibly
190
+ * half-built and reported as such.
191
+ */
192
+ export const applyFlows = async (client, pipeId, plan, opts = {}) => {
193
+ const ui = new Progress({
194
+ title: opts.dryRun ? `dry run · ${plan.steps.length} flow steps` : `applying ${plan.steps.length} flow steps`,
195
+ });
196
+ for (const s of plan.steps) {
197
+ ui.add({
198
+ id: s.id,
199
+ label: s.description,
200
+ state: 'pending',
201
+ tags: [...(s.destructive ? ['destructive'] : []), ...(s.arms ? ['arms'] : [])],
202
+ });
203
+ }
204
+ ui.start();
205
+ for (const step of plan.steps) {
206
+ if (opts.dryRun) {
207
+ step.status = 'skipped';
208
+ ui.update(step.id, { state: 'done', detail: 'not sent' });
209
+ continue;
210
+ }
211
+ step.status = 'running';
212
+ ui.update(step.id, { state: 'running' });
213
+ try {
214
+ if (step.createFlow) {
215
+ const flowId = await client.createFlow(pipeId, step.createFlow.displayName);
216
+ step.flowId = flowId;
217
+ const ops = buildFlowOperations(step.createFlow.tree);
218
+ for (const [i, op] of ops.entries()) {
219
+ ui.update(step.id, { detail: `${i + 1}/${ops.length} ${op.describe}` });
220
+ await client.operate(pipeId, flowId, op.type, op.request);
221
+ }
222
+ step.result = { flowId, operations: ops.length };
223
+ ui.update(step.id, { state: 'done', detail: `flow ${flowId}, ${ops.length} operations` });
224
+ }
225
+ else if (step.createConnection) {
226
+ const made = await client.createPlaceholderConnection(pipeId, step.createConnection);
227
+ step.result = made;
228
+ ui.update(step.id, {
229
+ state: 'done',
230
+ detail: `${made.externalId} — placeholder, reconnect before enabling`,
231
+ });
232
+ }
233
+ else if (step.deleteFlow) {
234
+ await client.deleteFlow(pipeId, step.flowId);
235
+ step.status = 'done';
236
+ ui.update(step.id, { state: 'done' });
237
+ }
238
+ else if (step.operation) {
239
+ step.result = await client.operate(pipeId, step.flowId, step.operation.type, step.operation.request);
240
+ ui.update(step.id, { state: 'done' });
241
+ }
242
+ step.status = 'done';
243
+ }
244
+ catch (e) {
245
+ step.status = 'failed';
246
+ step.error = e.message;
247
+ ui.update(step.id, { state: 'failed', detail: step.error });
248
+ if (!opts.continueOnError)
249
+ break;
250
+ }
251
+ }
252
+ const count = (s) => plan.steps.filter((x) => x.status === s).length;
253
+ ui.stop();
254
+ /**
255
+ * Placeholders are announced after the run, not buried in the step list.
256
+ *
257
+ * The host marks any connection ACTIVE without validating its credential, so
258
+ * nothing downstream will tell you these do not work — the flow simply fails on
259
+ * every trigger once enabled.
260
+ */
261
+ const placeholders = plan.steps.filter((s) => s.createConnection && s.status === 'done');
262
+ if (placeholders.length && !opts.dryRun) {
263
+ info('');
264
+ warn(`${placeholders.length} placeholder connection(s) were created — the flow is built but not ` +
265
+ 'authenticated');
266
+ for (const s of placeholders) {
267
+ info(dim(` ${s.createConnection.externalId} ${s.createConnection.pieceName}`));
268
+ }
269
+ info(dim(' reconnect each under Advanced Automations > Connections before enabling a flow'));
270
+ }
271
+ return { done: count('done'), failed: count('failed'), skipped: count('skipped'), steps: plan.steps };
272
+ };
273
+ /**
274
+ * After applying, read the flows back and confirm every datapill survived exactly.
275
+ *
276
+ * This is the check nothing else can make. A migrating write path — `IMPORT_FLOW`
277
+ * is one — rewrites `{{step_1['output'].x}}` into `{{step_1['output']['output'].x}}`,
278
+ * which is structurally valid, passes every schema check, passes the platform's own
279
+ * validator, and resolves to nothing at runtime. Only comparing the stored string
280
+ * against the intended one finds it.
281
+ */
282
+ export const verifyFlowDatapills = async (client, pipeId, authored) => {
283
+ const drift = [];
284
+ const pillsOf = (v, at, out = []) => {
285
+ if (typeof v === 'string') {
286
+ for (const m of v.matchAll(/\{\{[^}]+\}\}/g))
287
+ out.push({ at, pill: m[0] });
288
+ return out;
289
+ }
290
+ if (Array.isArray(v)) {
291
+ v.forEach((x, i) => pillsOf(x, `${at}[${i}]`, out));
292
+ return out;
293
+ }
294
+ if (v && typeof v === 'object')
295
+ for (const [k, val] of Object.entries(v))
296
+ pillsOf(val, at ? `${at}.${k}` : k, out);
297
+ return out;
298
+ };
299
+ for (const tree of authored.flows) {
300
+ const id = flowIdOf(tree);
301
+ if (!id)
302
+ continue;
303
+ let live;
304
+ try {
305
+ live = unlinkFlow(await client.getFlow(pipeId, id));
306
+ }
307
+ catch {
308
+ continue;
309
+ }
310
+ const liveByName = new Map(live.nodes.map((n) => [n.name, n]));
311
+ for (const node of tree.nodes) {
312
+ const other = liveByName.get(node.name);
313
+ if (!other)
314
+ continue;
315
+ const sent = pillsOf(node['settings'], 'settings');
316
+ const stored = pillsOf(other['settings'], 'settings');
317
+ for (const one of sent) {
318
+ const sameKey = stored.filter((x) => x.at === one.at);
319
+ if (!sameKey.length)
320
+ continue;
321
+ if (!sameKey.some((x) => x.pill === one.pill)) {
322
+ drift.push({
323
+ flow: String(tree.version['displayName'] ?? id),
324
+ node: node.name,
325
+ at: one.at,
326
+ sent: one.pill,
327
+ stored: sameKey[0].pill,
328
+ });
329
+ }
330
+ }
331
+ }
332
+ }
333
+ return drift;
334
+ };
335
+ export const reportDatapillDrift = (drift) => {
336
+ if (!drift.length)
337
+ return;
338
+ warn(`${drift.length} datapill(s) are not stored as they were sent`);
339
+ for (const d of drift.slice(0, 8)) {
340
+ info(dim(` ${d.flow} / ${d.node} ${d.at}`));
341
+ info(dim(` sent ${d.sent}`));
342
+ info(dim(` stored ${d.stored}`));
343
+ }
344
+ if (drift.some((d) => d.stored.includes("['output']['output']"))) {
345
+ info(dim(" ['output']['output'] is a double migration — the write path migrated an already-live flow"));
346
+ }
347
+ };
348
+ export { IpaasUnavailable };