@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,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
+ };