@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
@@ -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
- }