@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
@@ -1,380 +0,0 @@
1
- import { ENDPOINTS, type TokenProvider } from '../config.ts';
2
- import { debug, warn } from '../util/log.ts';
3
- import { retry } from '../util/pool.ts';
4
- import type { Row } from '../model/payload.ts';
5
-
6
- /**
7
- * The internal-API adapter. PLAN.md §2.1 flags this as an unversioned endpoint
8
- * with no compatibility contract, so it is isolated here, behind one module,
9
- * and every entry point degrades to `null`/`unsupported` rather than throwing.
10
- *
11
- * Two things live only here — both taken from the Change Migrator recipes,
12
- * which have been running them in production:
13
- *
14
- * 1. Automations. Public `getAutomations` returns a summary; the recipes use
15
- * `loadAutomationToEdit` to get `action_params.field_map`, `searchFor`,
16
- * `responseSchema`, `schedulerCron` and `aiParams` — everything a
17
- * round trip needs and the summary omits.
18
- * 2. Phase jumps. `GET/PUT /internal_api/settings/phases/:id`, set-replacing.
19
- *
20
- * Both accept the same personal API token as the public endpoint, so there is
21
- * no session-cookie or CSRF subsystem here.
22
- */
23
-
24
- export type InternalStatus = 'unknown' | 'ok' | 'unavailable';
25
-
26
- export class InternalApi {
27
- private readonly tokenProvider: TokenProvider;
28
- status: InternalStatus = 'unknown';
29
- requestCount = 0;
30
- /** Set once, so a broken endpoint produces one warning and not one per call. */
31
- private warned = false;
32
-
33
- constructor(token: string | TokenProvider) {
34
- this.tokenProvider = typeof token === 'string' ? async () => token : token;
35
- }
36
-
37
- private async bearer(): Promise<string> {
38
- return this.tokenProvider();
39
- }
40
-
41
- private markUnavailable(reason: string) {
42
- this.status = 'unavailable';
43
- if (!this.warned) {
44
- this.warned = true;
45
- warn(
46
- `internal API unavailable (${reason}). Automation details and phase jumps ` +
47
- `will be reported as unknown rather than empty — see PLAN.md §2.1.`,
48
- );
49
- }
50
- }
51
-
52
- /** GraphQL over the internal endpoint. Returns null when the endpoint fails. */
53
- async gql<T>(query: string, variables: Record<string, unknown>, label: string): Promise<T | null> {
54
- try {
55
- const data = await retry(
56
- async () => {
57
- this.requestCount++;
58
- const res = await fetch(ENDPOINTS.internal, {
59
- method: 'POST',
60
- headers: {
61
- authorization: `Bearer ${await this.bearer()}`,
62
- 'content-type': 'application/json',
63
- accept: 'application/json',
64
- },
65
- body: JSON.stringify({ query, variables }),
66
- });
67
- const text = await res.text();
68
- if (!res.ok) {
69
- const err = new Error(`HTTP ${res.status}: ${text.slice(0, 160)}`);
70
- (err as { retryable?: boolean }).retryable = res.status >= 500 || res.status === 429;
71
- throw err;
72
- }
73
- const body = JSON.parse(text) as { data?: T; errors?: Array<{ message: string }> };
74
- if (body.errors?.length) {
75
- const err = new Error(body.errors.map((e) => e.message).join('; '));
76
- (err as { retryable?: boolean }).retryable = false;
77
- throw err;
78
- }
79
- return body.data ?? null;
80
- },
81
- { label },
82
- );
83
- if (data) this.status = 'ok';
84
- return data;
85
- } catch (e) {
86
- debug(`internal gql ${label} failed: ${(e as Error).message}`);
87
- this.markUnavailable(`${label}: ${(e as Error).message}`);
88
- return null;
89
- }
90
- }
91
-
92
- // ── Automations ────────────────────────────────────────────────────────────
93
-
94
- /** Recipe `Pipe Data Treatment` step 156. */
95
- async listAutomations(orgId: string, repoId: string | number): Promise<Row[] | null> {
96
- const q = `
97
- query getAutomations($orgId: ID!, $repoId: ID) {
98
- automations(organizationId: $orgId, repoId: $repoId) {
99
- id
100
- name
101
- action_id
102
- event_id
103
- created_at
104
- active
105
- action_repo: action_repo_v2 {
106
- __typename
107
- ... on Pipe { id }
108
- ... on Table { id }
109
- }
110
- event_repo { __typename id }
111
- }
112
- }`;
113
- const data = await this.gql<{ automations: Row[] }>(q, { orgId: String(orgId), repoId: String(repoId) }, 'listAutomations');
114
- return data?.automations ?? null;
115
- }
116
-
117
- /**
118
- * Recipe `Apply Changes` step 118 — the full editable definition. This is the
119
- * only read path that returns `action_params.field_map`, `searchFor`,
120
- * `responseSchema` and `aiParams`, which is why the reconstruction path
121
- * cannot avoid the internal API for automations.
122
- */
123
- async loadAutomation(id: string | number): Promise<Row | null> {
124
- const q = `
125
- query loadAutomationToEdit($id: ID!) {
126
- automation(id: $id) {
127
- name
128
- event_id
129
- active
130
- event_repo { id }
131
- event_params {
132
- toPhaseId: to_phase_id
133
- fromPhaseId
134
- inPhaseId
135
- triggerFieldIds
136
- kindOfSla
137
- triggerAutomationId
138
- }
139
- action_id
140
- action_repo: action_repo_v2 {
141
- ... on Pipe { id uuid }
142
- ... on Table { id uuid }
143
- }
144
- scheduler_frequency
145
- schedulerCron { minute hour dayOfWeek dayOfMonth month }
146
- searchFor { id field operation value }
147
- responseSchema
148
- action_params {
149
- toPhaseId: to_phase_id
150
- fieldsMapOrder: fields_map_order
151
- email_template_id
152
- fieldMap: field_map { fieldId value inputMode }
153
- cardId: card_id
154
- headers
155
- body
156
- url
157
- httpMethod
158
- authenticationKey
159
- hasAuthenticationValue
160
- authenticationAddTo
161
- authenticationType
162
- strategy
163
- # taskParams is not in the recipe's query, and the omission is not
164
- # cosmetic: every send_a_task automation came back with an empty
165
- # action_params, so a reconstructed read lost its recipients and
166
- # title, and a recreate would have rebuilt it blank. Introspected
167
- # from AutomationActionParams on the internal endpoint.
168
- taskParams { recipients title }
169
- aiParams { fieldIds value skillsIds }
170
- aiBehaviorParams
171
- slaParams {
172
- monday { startHour endHour enabled }
173
- tuesday { startHour endHour enabled }
174
- wednesday { startHour endHour enabled }
175
- thursday { startHour endHour enabled }
176
- friday { startHour endHour enabled }
177
- saturday { startHour endHour enabled }
178
- sunday { startHour endHour enabled }
179
- timezone
180
- holidays { date recurrence description }
181
- }
182
- }
183
- condition {
184
- expressions_structure
185
- expressions { id field_address operation value structure_id }
186
- }
187
- }
188
- }`;
189
- const data = await this.gql<{ automation: Row | null }>(q, { id: String(id) }, `loadAutomation ${id}`);
190
- return data?.automation ?? null;
191
- }
192
-
193
- /** Recipe `Apply Changes` step 121. `input` is the AutomationInput variable set. */
194
- async createAutomation(vars: Record<string, unknown>): Promise<{ id: string } | null> {
195
- const q = `
196
- mutation createAutomation(
197
- $name: String!, $action_id: ID!, $event_id: ID!,
198
- $action_repo_id: ID, $event_repo_id: ID,
199
- $event_params: AutomationEventParamsInput,
200
- $action_params: AutomationActionParamsInput,
201
- $condition: ConditionInput,
202
- $scheduler_frequency: String,
203
- $searchFor: [SearchConditionInput],
204
- $schedulerCron: CronInput,
205
- $responseSchema: JSON
206
- ) {
207
- createAutomation(input: {
208
- name: $name, action_id: $action_id, event_id: $event_id,
209
- action_repo_id: $action_repo_id, event_repo_id: $event_repo_id,
210
- event_params: $event_params, action_params: $action_params,
211
- condition: $condition, scheduler_frequency: $scheduler_frequency,
212
- searchFor: $searchFor, schedulerCron: $schedulerCron,
213
- responseSchema: $responseSchema
214
- }) {
215
- automation { id }
216
- error_details { object_key messages }
217
- }
218
- }`;
219
- const data = await this.gql<{
220
- createAutomation: { automation: { id: string } | null; error_details?: Array<{ object_key: string; messages: string[] }> };
221
- }>(q, vars, 'createAutomation');
222
- const payload = data?.createAutomation;
223
- if (!payload) return null;
224
- if (payload.error_details?.length) {
225
- const detail = payload.error_details.map((d) => `${d.object_key}: ${d.messages.join(', ')}`).join(' | ');
226
- throw new Error(`createAutomation rejected — ${detail}`);
227
- }
228
- return payload.automation;
229
- }
230
-
231
- /** Recipe `Apply Changes` step 126. */
232
- async deleteAutomation(id: string | number): Promise<boolean> {
233
- const q = `
234
- mutation deleteAutomation($id: ID!) {
235
- deleteAutomation(input: { id: $id }) { success }
236
- }`;
237
- const data = await this.gql<{ deleteAutomation: { success: boolean } }>(q, { id: String(id) }, `deleteAutomation ${id}`);
238
- return Boolean(data?.deleteAutomation?.success);
239
- }
240
-
241
- // ── Phase jumps ────────────────────────────────────────────────────────────
242
-
243
- /**
244
- * `GET /internal_api/settings/phases/:id` — recipe step 23.
245
- *
246
- * **The GET and the PUT answer in different shapes.** Both captured live:
247
- *
248
- * GET { data: { attributes: { name, phases: [[id, name, done], …],
249
- * jump_targets: [344054275] } } }
250
- * PUT { data: { name, back_phase_ids: [], next_phase_ids: [344054275] } }
251
- *
252
- * The GET's `jump_targets` is one merged set, which is exactly what the
253
- * payload's `phase_jumps` array holds. Reading it as `data.next_phase_ids` —
254
- * the shape PLAN.md §2.1 recorded, from the PUT — yields an empty set for
255
- * every phase without any error, and a diff that then proposes deleting every
256
- * jump in the pipe.
257
- */
258
- async getPhaseSettings(
259
- phaseId: string | number,
260
- ): Promise<{ jump_targets: number[]; phases: Array<{ id: number; name: string; done: boolean }> } | null> {
261
- try {
262
- const res = await retry(
263
- async () => {
264
- this.requestCount++;
265
- const r = await fetch(`${ENDPOINTS.internalSettings}/phases/${phaseId}`, {
266
- method: 'GET',
267
- headers: { authorization: `Bearer ${await this.bearer()}`, accept: 'application/json' },
268
- });
269
- if (!r.ok) {
270
- const err = new Error(`HTTP ${r.status}`);
271
- (err as { retryable?: boolean }).retryable = r.status >= 500 || r.status === 429;
272
- throw err;
273
- }
274
- return (await r.json()) as {
275
- data?: {
276
- attributes?: { jump_targets?: number[]; phases?: Array<[number, string, boolean]> };
277
- /** The PUT-style shape, accepted in case a deployment answers with it here. */
278
- jump_targets?: number[];
279
- next_phase_ids?: number[];
280
- back_phase_ids?: number[];
281
- };
282
- };
283
- },
284
- { label: `getPhaseSettings ${phaseId}` },
285
- );
286
-
287
- const d = res.data ?? {};
288
- const attrs = d.attributes ?? {};
289
- const targets = attrs.jump_targets ?? d.jump_targets ?? [...(d.next_phase_ids ?? []), ...(d.back_phase_ids ?? [])];
290
-
291
- this.status = 'ok';
292
- return {
293
- jump_targets: [...new Set(targets.map(Number).filter((n) => Number.isFinite(n)))],
294
- phases: (attrs.phases ?? []).map(([id, name, done]) => ({ id: Number(id), name: String(name), done: Boolean(done) })),
295
- };
296
- } catch (e) {
297
- debug(`getPhaseSettings ${phaseId} failed: ${(e as Error).message}`);
298
- this.markUnavailable(`getPhaseSettings: ${(e as Error).message}`);
299
- return null;
300
- }
301
- }
302
-
303
- /**
304
- * `PUT /internal_api/settings/phases/:id` with the complete desired jump set.
305
- * Recipe step 24 does the same thing in Python. Set-replacing, so we send the
306
- * whole set and assert on the echoed `next_phase_ids` rather than on the HTTP
307
- * status — PLAN.md §2.1.
308
- */
309
- async setPhaseJumpTargets(
310
- phaseId: string | number,
311
- targetPhaseIds: Array<string | number>,
312
- ): Promise<{ ok: boolean; echoed: number[]; error?: string }> {
313
- const params = new URLSearchParams();
314
- if (targetPhaseIds.length === 0) {
315
- // An empty set still has to be expressible, or "remove the last jump"
316
- // becomes impossible. The form convention for an empty array is a single
317
- // blank entry.
318
- params.append('phase[jump_target_ids][]', '');
319
- }
320
- for (const t of targetPhaseIds) params.append('phase[jump_target_ids][]', String(t));
321
-
322
- try {
323
- const res = await retry(
324
- async () => {
325
- this.requestCount++;
326
- const r = await fetch(`${ENDPOINTS.internalSettings}/phases/${phaseId}`, {
327
- method: 'PUT',
328
- headers: {
329
- authorization: `Bearer ${await this.bearer()}`,
330
- 'content-type': 'application/x-www-form-urlencoded',
331
- accept: 'application/json',
332
- },
333
- body: params.toString(),
334
- });
335
- const text = await r.text();
336
- if (!r.ok) {
337
- const err = new Error(`HTTP ${r.status}: ${text.slice(0, 160)}`);
338
- (err as { retryable?: boolean }).retryable = r.status >= 500 || r.status === 429;
339
- throw err;
340
- }
341
- return JSON.parse(text) as {
342
- data?: {
343
- next_phase_ids?: number[];
344
- back_phase_ids?: number[];
345
- jump_targets?: number[];
346
- attributes?: { jump_targets?: number[] };
347
- };
348
- };
349
- },
350
- { label: `setPhaseJumpTargets ${phaseId}` },
351
- );
352
-
353
- /**
354
- * The echo is the union of both directions.
355
- *
356
- * `next_phase_ids` alone is forward-only, so a jump to an *earlier* phase
357
- * echoes back as an empty next list even though the write succeeded — which
358
- * this assertion reported as a failed write. Asserting on the union is what
359
- * the sent set actually means.
360
- */
361
- const d = res.data ?? {};
362
- const echoed = [
363
- ...(d.attributes?.jump_targets ?? []),
364
- ...(d.jump_targets ?? []),
365
- ...(d.next_phase_ids ?? []),
366
- ...(d.back_phase_ids ?? []),
367
- ].map(Number);
368
- const want = [...new Set(targetPhaseIds.map(Number))].sort((a, b) => a - b);
369
- const got = [...new Set(echoed)].sort((a, b) => a - b);
370
- const ok = want.length === got.length && want.every((v, i) => v === got[i]);
371
- this.status = 'ok';
372
- return ok
373
- ? { ok, echoed }
374
- : { ok, echoed, error: `echo mismatch: wanted [${want.join(',')}], endpoint reports [${got.join(',')}]` };
375
- } catch (e) {
376
- this.markUnavailable(`setPhaseJumpTargets: ${(e as Error).message}`);
377
- return { ok: false, echoed: [], error: (e as Error).message };
378
- }
379
- }
380
- }