@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,301 @@
1
+ import { resolve } from 'node:path';
2
+ import { loadReference } from "../workspace/reference.js";
3
+ import { bool, str } from "../cli/args.js";
4
+ import { readJson, writeJson } from "../util/fsx.js";
5
+ import { UserError, bold, dim, green, info, ok, out, red, warn, yellow } from "../util/log.js";
6
+ import { readTree } from "../workspace/read.js";
7
+ import { planFile, findPlanFile } from "../workspace/layout.js";
8
+ import { diffTrees } from "../diff/diff.js";
9
+ import { compile } from "../apply/compile.js";
10
+ import { readAutomationCatalogue } from "../pipefy/automations.js";
11
+ import { validateTree, renderFindings, blockingErrors } from "../validate/index.js";
12
+ import { connect, openWorkspace } from "./context.js";
13
+ import { loadBaseline, writeHtml, openViewer } from "./diff.js";
14
+ export const help = `pipe plan [workspace] [options]
15
+
16
+ --repo <id> which repo in the workspace (default: the root pipe)
17
+ --dry-run print the plan and do not save it
18
+ --save save the plan to .ppc/plans/ (implied unless --dry-run)
19
+ --automation-recreate delete+create automations instead of updating them in place
20
+ --allow-card-moves permit phase deletes, which require moving cards first
21
+ --force compile a plan even though some changes are unsupported
22
+ --html <file> write the diff + plan as an HTML view
23
+ --open open that view
24
+ --json machine-readable output`;
25
+ /**
26
+ * `pipe plan` — classify every change against the capability registry, refuse the
27
+ * unsupported ones by name, and order the rest.
28
+ *
29
+ * PLAN.md §5: any UNSUPPORTED change aborts here, with the entity and the
30
+ * reason. That refusal is the product working, not the product failing — the
31
+ * alternative is an apply that reports success having ignored something.
32
+ */
33
+ /**
34
+ * The flow half of a plan.
35
+ *
36
+ * Extracted so it is reachable when the pipe half is clean — a flow-only change
37
+ * is the ordinary case now that flows are in the loop, and the early return for an
38
+ * empty pipe diff was reporting "nothing to do" over real pending work.
39
+ *
40
+ * Returns how much flow work there is, so the caller can decide whether "nothing
41
+ * to do" is still true.
42
+ */
43
+ const planFlowHalf = async (p, root, dir, repoId) => {
44
+ if (bool(p, 'json') || bool(p, 'no-flows'))
45
+ return 0;
46
+ const { readFlows } = await import("../workspace/flows.js");
47
+ if (!(await readFlows(dir)))
48
+ return 0;
49
+ try {
50
+ const { diffFlowsAgainstLive, planFlows, renderFlowPlan } = await import("./flows.js");
51
+ const net = await connect(p);
52
+ const { changeSet: flowChanges, live } = await diffFlowsAgainstLive(net.ipaas, repoId, dir);
53
+ const flowPlan = planFlows(flowChanges, {
54
+ allowArm: bool(p, 'allow-flow-enable'),
55
+ connections: live.connections,
56
+ });
57
+ const rendered = renderFlowPlan(flowPlan);
58
+ if (rendered.trim()) {
59
+ out('');
60
+ out(bold('flows'));
61
+ out(rendered);
62
+ }
63
+ return flowPlan.steps.length + flowPlan.blocked.length;
64
+ }
65
+ catch (e) {
66
+ info(dim(` flows not planned: ${e.message}`));
67
+ return 0;
68
+ }
69
+ };
70
+ /**
71
+ * The agent half of a plan. Offline, like the pipe half — the agent baseline is
72
+ * the file `pipe pull` wrote, not a live read — so unlike `planFlowHalf` this
73
+ * needs no network for the diff itself; only `planAgents` takes a client, and
74
+ * only to resolve nested input type names when one is available.
75
+ */
76
+ const planAgentHalf = async (p, root, dir, repoId) => {
77
+ if (bool(p, 'json'))
78
+ return 0;
79
+ try {
80
+ const { diffAgentsFor, planAgents, renderAgentPlan } = await import("./agents.js");
81
+ const agentChanges = await diffAgentsFor(root, repoId, dir);
82
+ if (!agentChanges.changes.length)
83
+ return 0;
84
+ const net = await connect(p).catch(() => null);
85
+ const agentPlan = await planAgents(net?.client ?? null, agentChanges);
86
+ const rendered = renderAgentPlan(agentPlan);
87
+ if (rendered.trim()) {
88
+ out('');
89
+ out(bold('agents'));
90
+ out(rendered);
91
+ }
92
+ return agentPlan.steps.length;
93
+ }
94
+ catch (e) {
95
+ info(dim(` agents not planned: ${e.message}`));
96
+ return 0;
97
+ }
98
+ };
99
+ export const run = async (p) => {
100
+ const { root, lock, repo, repoDir: dir } = await openWorkspace(p);
101
+ const edited = await readTree(dir);
102
+ const baseline = await loadBaseline(root, repo.id);
103
+ // Validation first: planning an invalid tree wastes a network round trip and
104
+ // produces a worse error message than validate does.
105
+ const { readAgents } = await import("../workspace/agents.js");
106
+ const findings = validateTree(edited, {
107
+ knownRepoIds: lock.repos.map((r) => r.id),
108
+ baseline,
109
+ reference: await loadReference(root),
110
+ agents: await readAgents(dir),
111
+ });
112
+ const blocking = blockingErrors(findings);
113
+ if (blocking.length) {
114
+ out(renderFindings(findings, `${repo.name ?? repo.id}`));
115
+ throw new UserError(`${blocking.length} validation error${blocking.length === 1 ? '' : 's'} — fix these before planning`);
116
+ }
117
+ const changeSet = diffTrees(baseline, edited);
118
+ if (!changeSet.changes.length) {
119
+ /**
120
+ * The pipe half is clean, which is not the same as nothing to do: a flow-only
121
+ * change is the ordinary case now that flows are in the loop, and returning
122
+ * here reported "nothing to do" over real pending work.
123
+ */
124
+ const flowWork = await planFlowHalf(p, root, dir, repo.id);
125
+ const agentWork = await planAgentHalf(p, root, dir, repo.id);
126
+ if (!flowWork && !agentWork)
127
+ ok('nothing to do — the workspace matches the baseline');
128
+ return 0;
129
+ }
130
+ const options = {
131
+ automationStrategy: bool(p, 'automation-recreate') ? 'recreate' : 'update',
132
+ allowCardMoves: bool(p, 'allow-card-moves'),
133
+ force: bool(p, 'force'),
134
+ };
135
+ /**
136
+ * Planning offline is a first-class mode, not a fallback: classification is a
137
+ * pure function of the ChangeSet and the registry, and it is the loop Claude
138
+ * iterates against. A missing token degrades the plan, it does not block it.
139
+ */
140
+ const net = await connect(p).catch((e) => {
141
+ if (e instanceof UserError) {
142
+ warn('planning offline — ' + e.message.toLowerCase());
143
+ return null;
144
+ }
145
+ throw e;
146
+ });
147
+ const caps = net ? await net.capabilities() : null;
148
+ const plan = await compile(changeSet, {
149
+ client: net?.client ?? null,
150
+ capabilities: caps,
151
+ repoId: repo.id,
152
+ repoUuid: repo.uuid,
153
+ workspace: root,
154
+ organizationId: lock.organizationId,
155
+ /**
156
+ * The automation catalogue, so a wrong event or action parameter is refused
157
+ * here rather than discovered as "is invalid" mid-apply. Offline planning has
158
+ * no client, and then the check is simply skipped.
159
+ */
160
+ automationCatalogue: net
161
+ ? await readAutomationCatalogue(net.client, repo.id).catch(() => undefined)
162
+ : undefined,
163
+ livePhaseIds: edited.phases.map((ph) => Number(ph['id'])).filter(Number.isFinite),
164
+ repoKind: repo.kind,
165
+ /** A database has one form, so its column order is that form's fields by index. */
166
+ tableFieldOrder: repo.kind === 'table'
167
+ ? [...(edited.phases[0]?.fields ?? [])].sort((a, b) => Number(a['index'] ?? 0) - Number(b['index'] ?? 0))
168
+ : undefined,
169
+ baselineTableFieldOrder: repo.kind === 'table'
170
+ ? [...(baseline?.phases[0]?.fields ?? [])]
171
+ .sort((a, b) => Number(a['index'] ?? 0) - Number(b['index'] ?? 0))
172
+ .map((f) => String(f['slug'] ?? ''))
173
+ : undefined,
174
+ fieldUuidById: Object.fromEntries(edited.phases
175
+ .flatMap((ph) => ph.fields)
176
+ .filter((fl) => fl['id'] && fl['uuid'])
177
+ .map((fl) => [String(fl['id']), String(fl['uuid'])])),
178
+ /** For `updateTable.title_field_id`, which refuses the numeric id. */
179
+ fieldSlugById: Object.fromEntries(edited.phases
180
+ .flatMap((ph) => ph.fields)
181
+ .filter((fl) => fl['id'] && fl['slug'])
182
+ .map((fl) => [String(fl['id']), String(fl['slug'])])),
183
+ startFormPhaseId: edited.phases.find((ph) => Number(ph['index'] ?? -1) === 0)?.['id'] ?? null,
184
+ conditionOrder: Object.fromEntries([...new Set(edited.field_conditions.map((fc) => String(fc['phase_id'] ?? '')))]
185
+ .filter((phaseId) => phaseId !== '')
186
+ .map((phaseId) => [
187
+ phaseId,
188
+ edited.field_conditions
189
+ .filter((fc) => String(fc['phase_id'] ?? '') === phaseId)
190
+ .slice()
191
+ .sort((a, b) => Number(a['index'] ?? 0) - Number(b['index'] ?? 0))
192
+ .map((fc) => ({ name: String(fc['name'] ?? ''), id: fc['id'] ? Number(fc['id']) : null })),
193
+ ])),
194
+ baselineConditionOrder: Object.fromEntries([...new Set((baseline?.field_conditions ?? []).map((fc) => String(fc['phase_id'] ?? '')))]
195
+ .filter((phaseId) => phaseId !== '')
196
+ .map((phaseId) => [
197
+ phaseId,
198
+ (baseline?.field_conditions ?? [])
199
+ .filter((fc) => String(fc['phase_id'] ?? '') === phaseId)
200
+ .slice()
201
+ .sort((a, b) => Number(a['index'] ?? 0) - Number(b['index'] ?? 0))
202
+ .map((fc) => String(fc['id'])),
203
+ ])),
204
+ }, options);
205
+ if (bool(p, 'json')) {
206
+ out(JSON.stringify(plan, null, 2));
207
+ }
208
+ else {
209
+ out(renderPlan(plan));
210
+ }
211
+ await planFlowHalf(p, root, dir, repo.id);
212
+ await planAgentHalf(p, root, dir, repo.id);
213
+ const htmlPath = str(p, 'html') ?? (bool(p, 'open') ? resolve(root, '.ppc', 'plan.html') : undefined);
214
+ if (htmlPath) {
215
+ await writeHtml(htmlPath, changeSet, root, plan);
216
+ info('');
217
+ ok(`HTML view written to ${htmlPath}`);
218
+ if (bool(p, 'open'))
219
+ await openViewer(htmlPath);
220
+ }
221
+ const dryRun = bool(p, 'dry-run');
222
+ if (!dryRun) {
223
+ const path = planFile(root, plan.id);
224
+ await writeJson(path, plan);
225
+ info('');
226
+ ok(`plan saved — ${dim(path)}`);
227
+ info(`${yellow('next')} pipe apply ${dim(`--plan ${plan.id}`)}`);
228
+ }
229
+ if (plan.blocked.length && !options.force)
230
+ return 2;
231
+ return 0;
232
+ };
233
+ export const renderPlan = (plan) => {
234
+ const lines = [];
235
+ lines.push(bold(`plan ${plan.id}`));
236
+ lines.push(dim(`repo ${plan.repoId} · ${plan.mode} · automations: ${plan.options.automationStrategy}` +
237
+ `${plan.options.allowCardMoves ? ' · card moves allowed' : ''}`));
238
+ lines.push('');
239
+ if (plan.blocked.length) {
240
+ lines.push(red(`${plan.blocked.length} change${plan.blocked.length === 1 ? '' : 's'} cannot be applied:`));
241
+ for (const b of plan.blocked) {
242
+ lines.push(` ${red('x')} ${bold(`${b.entity}.${b.op}`)} — ${b.label}`);
243
+ lines.push(` ${dim(b.reason ?? 'no write path is registered')}`);
244
+ if (b.ask)
245
+ lines.push(` ${dim(`would be fixed by platform ask ${b.ask} (ROUTE-B-V2.md §8)`)}`);
246
+ }
247
+ lines.push('');
248
+ lines.push(plan.options.force
249
+ ? yellow('--force given: the unsupported changes will be skipped and the rest applied')
250
+ : red('the apply is blocked. Revert these edits, or run with --force to apply only the rest.'));
251
+ lines.push('');
252
+ }
253
+ const partial = plan.classifications.filter((c) => c.status === 'PARTIAL');
254
+ if (partial.length) {
255
+ lines.push(yellow(`${partial.length} change${partial.length === 1 ? '' : 's'} apply through a partial path:`));
256
+ for (const c of partial)
257
+ lines.push(` ${yellow('!')} ${c.entity}.${c.op} — ${c.label} ${dim(`(${c.via ?? ''})`)}`);
258
+ lines.push('');
259
+ }
260
+ if (plan.warnings.length) {
261
+ for (const w of plan.warnings)
262
+ lines.push(`${yellow('!')} ${w}`);
263
+ lines.push('');
264
+ }
265
+ lines.push(bold(`${plan.steps.length} step${plan.steps.length === 1 ? '' : 's'}`));
266
+ for (const s of plan.steps) {
267
+ const marks = [
268
+ s.destructive ? red('destructive') : '',
269
+ s.unverified ? yellow('unverified') : '',
270
+ s.kind.startsWith('internal') ? dim('internal-api') : '',
271
+ s.needs.length ? dim(`needs ${s.needs.length} id${s.needs.length === 1 ? '' : 's'}`) : '',
272
+ ]
273
+ .filter(Boolean)
274
+ .join(' ');
275
+ lines.push(` ${dim(s.id)} ${s.description}${marks ? ' ' + marks : ''}`);
276
+ }
277
+ lines.push('');
278
+ const destructive = plan.steps.filter((s) => s.destructive).length;
279
+ lines.push(destructive
280
+ ? red(`${destructive} destructive step${destructive === 1 ? '' : 's'} — these delete configuration or data`)
281
+ : green('no destructive steps'));
282
+ return lines.join('\n');
283
+ };
284
+ export const loadPlan = async (root, planId) => {
285
+ try {
286
+ // A finished plan carries its outcome in the filename, so resolve rather
287
+ // than assume the bare `<id>.json`.
288
+ const found = await findPlanFile(root, planId);
289
+ return await readJson(found ?? planFile(root, planId));
290
+ }
291
+ catch {
292
+ throw new UserError(`no saved plan "${planId}" in ${root}/.ppc/plans`, 'run `pipe plan` first, or check `pipe status`');
293
+ }
294
+ };
295
+ export const warnUnverifiedSteps = (plan) => {
296
+ const unverified = plan.steps.filter((s) => s.unverified);
297
+ if (!unverified.length)
298
+ return;
299
+ warn(`${unverified.length} step${unverified.length === 1 ? '' : 's'} use a call whose shape could not be verified ` +
300
+ `against this schema (phase jumps, phase settings, public form). They report their own failure rather than silently no-op.`);
301
+ };