@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.
- package/.agents/skills/ppc-pipefy-flow-authoring/SKILL.md +262 -262
- package/.agents/skills/ppc-pipefy-flow-authoring/examples/01-main-2-danfe-consulta.json +247 -247
- package/.agents/skills/ppc-pipefy-flow-authoring/examples/01-main-2-webhook-retorno-consulta.json +589 -589
- package/.agents/skills/ppc-pipefy-flow-authoring/examples/01-main-4-recebimento-barramento.json +1391 -1391
- package/.agents/skills/ppc-pipefy-flow-authoring/examples/02-subflow-2-danfe-retorno.json +623 -623
- package/.agents/skills/ppc-pipefy-flow-authoring/examples/02-subflow-4-criacao-operacao.json +636 -636
- package/.agents/skills/ppc-pipefy-flow-authoring/examples/03-subflow-4-criacao-titulo.json +3642 -3642
- package/.agents/skills/ppc-pipefy-flow-authoring/examples/04-subflow-4-criacao-cedente.json +863 -863
- package/.agents/skills/ppc-pipefy-flow-authoring/examples/04-subflow-4-criacao-sacado.json +799 -799
- package/.agents/skills/ppc-pipefy-flow-authoring/examples/README.md +42 -42
- package/.agents/skills/ppc-pipefy-flow-authoring/examples/hml-01-subflow-1-titulo-acompanhamento-cobranca.json +581 -581
- package/.agents/skills/ppc-pipefy-flow-authoring/examples/hml-01-subflow-1-titulo-nfe-monitoramento.json +503 -503
- package/.agents/skills/ppc-pipefy-flow-authoring/examples/hml-01-subflow-1-titulo-retorno-bancario.json +562 -562
- package/.agents/skills/ppc-pipefy-flow-authoring/examples/hml-02-subflow-2-retorno-consulta-cedente.json +557 -557
- package/.agents/skills/ppc-pipefy-flow-authoring/examples/hml-02-subflow-2-retorno-consulta-sacado.json +609 -609
- package/.agents/skills/ppc-pipefy-pipe-authoring/SKILL.md +146 -146
- package/.agents/skills/ppc-pipefy-process-design/SKILL.md +127 -127
- package/.agents/skills/ppc-pipefy-workspace/SKILL.md +91 -91
- package/AGENTS.md +465 -456
- package/README.md +825 -807
- package/bin/pipe.js +25 -4
- package/dist/apply/adopt.js +128 -0
- package/dist/apply/agentops.js +42 -0
- package/dist/apply/compile.js +722 -0
- package/dist/apply/execute.js +277 -0
- package/dist/apply/flowops.js +293 -0
- package/dist/apply/idmap.js +233 -0
- package/dist/apply/mutations.js +704 -0
- package/dist/apply/registry.js +348 -0
- package/dist/apply/types.js +18 -0
- package/dist/cli/args.js +79 -0
- package/dist/cli.js +187 -0
- package/dist/codec/flow.js +164 -0
- package/dist/codec/pack.js +88 -0
- package/dist/codec/roundtrip.js +86 -0
- package/dist/codec/unpack.js +181 -0
- package/dist/commands/agents.js +145 -0
- package/dist/commands/apply.js +982 -0
- package/dist/commands/context.js +79 -0
- package/dist/commands/create.js +63 -0
- package/dist/commands/diff.js +294 -0
- package/dist/commands/flows.js +348 -0
- package/dist/commands/misc.js +528 -0
- package/dist/commands/plan.js +301 -0
- package/dist/commands/pull.js +511 -0
- package/dist/commands/runs.js +74 -0
- package/dist/commands/skills.js +131 -0
- package/dist/commands/verify.js +205 -0
- package/dist/config.js +125 -0
- package/dist/diff/agents.js +80 -0
- package/dist/diff/diff.js +983 -0
- package/dist/diff/flow.js +258 -0
- package/{src/diff/html.ts → dist/diff/html.js} +274 -322
- package/dist/diff/render.js +90 -0
- package/dist/model/payload.js +84 -0
- package/dist/model/tree.js +7 -0
- package/dist/model/volatile.js +54 -0
- package/dist/pipefy/agents.js +122 -0
- package/dist/pipefy/automations.js +155 -0
- package/dist/pipefy/capability.js +84 -0
- package/dist/pipefy/client.js +204 -0
- package/dist/pipefy/discovery.js +148 -0
- package/dist/pipefy/internal.js +315 -0
- package/dist/pipefy/ipaas.js +269 -0
- package/dist/pipefy/reconstruct.js +674 -0
- package/dist/pipefy/reference.js +172 -0
- package/dist/pipefy/snapshot.js +185 -0
- package/dist/pipefy/toolkit.js +158 -0
- package/dist/report/integrations.js +165 -0
- package/dist/report/run.js +355 -0
- package/dist/util/fsx.js +50 -0
- package/dist/util/git.js +25 -0
- package/dist/util/json.js +56 -0
- package/dist/util/log.js +69 -0
- package/dist/util/pool.js +42 -0
- package/dist/util/slug.js +23 -0
- package/dist/util/tui.js +287 -0
- package/dist/validate/index.js +86 -0
- package/dist/validate/integrity.js +370 -0
- package/dist/validate/reference.js +120 -0
- package/dist/validate/schema.js +278 -0
- package/dist/workspace/agents.js +245 -0
- package/{src/workspace/docs.ts → dist/workspace/docs.js} +367 -407
- package/dist/workspace/flows.js +149 -0
- package/dist/workspace/layout.js +146 -0
- package/dist/workspace/lock.js +83 -0
- package/dist/workspace/read.js +168 -0
- package/dist/workspace/reference.js +24 -0
- package/dist/workspace/stamp.js +234 -0
- package/dist/workspace/write.js +194 -0
- package/package.json +37 -35
- package/src/apply/adopt.ts +0 -150
- package/src/apply/agentops.ts +0 -71
- package/src/apply/compile.ts +0 -875
- package/src/apply/execute.ts +0 -336
- package/src/apply/flowops.ts +0 -399
- package/src/apply/idmap.ts +0 -241
- package/src/apply/mutations.ts +0 -955
- package/src/apply/registry.ts +0 -430
- package/src/apply/types.ts +0 -134
- package/src/cli/args.ts +0 -88
- package/src/cli.ts +0 -211
- package/src/codec/flow.ts +0 -199
- package/src/codec/pack.ts +0 -103
- package/src/codec/roundtrip.ts +0 -94
- package/src/codec/unpack.ts +0 -198
- package/src/commands/agents.ts +0 -184
- package/src/commands/apply.ts +0 -1144
- package/src/commands/context.ts +0 -119
- package/src/commands/create.ts +0 -79
- package/src/commands/diff.ts +0 -314
- package/src/commands/flows.ts +0 -414
- package/src/commands/misc.ts +0 -644
- package/src/commands/plan.ts +0 -331
- package/src/commands/pull.ts +0 -567
- package/src/commands/runs.ts +0 -83
- package/src/commands/skills.ts +0 -137
- package/src/commands/verify.ts +0 -253
- package/src/config.ts +0 -168
- package/src/diff/agents.ts +0 -122
- package/src/diff/diff.ts +0 -1130
- package/src/diff/flow.ts +0 -318
- package/src/diff/render.ts +0 -101
- package/src/model/payload.ts +0 -154
- package/src/model/tree.ts +0 -99
- package/src/model/volatile.ts +0 -55
- package/src/pipefy/agents.ts +0 -165
- package/src/pipefy/automations.ts +0 -219
- package/src/pipefy/capability.ts +0 -119
- package/src/pipefy/client.ts +0 -267
- package/src/pipefy/discovery.ts +0 -209
- package/src/pipefy/internal.ts +0 -380
- package/src/pipefy/ipaas.ts +0 -365
- package/src/pipefy/reconstruct.ts +0 -775
- package/src/pipefy/reference.ts +0 -251
- package/src/pipefy/snapshot.ts +0 -245
- package/src/pipefy/toolkit.ts +0 -200
- package/src/report/integrations.ts +0 -231
- package/src/report/run.ts +0 -475
- package/src/util/fsx.ts +0 -45
- package/src/util/git.ts +0 -32
- package/src/util/json.ts +0 -55
- package/src/util/log.ts +0 -76
- package/src/util/pool.ts +0 -48
- package/src/util/slug.ts +0 -26
- package/src/util/tui.ts +0 -335
- package/src/validate/index.ts +0 -123
- package/src/validate/integrity.ts +0 -387
- package/src/validate/reference.ts +0 -136
- package/src/validate/schema.ts +0 -328
- package/src/workspace/agents.ts +0 -290
- package/src/workspace/flows.ts +0 -191
- package/src/workspace/layout.ts +0 -165
- package/src/workspace/lock.ts +0 -148
- package/src/workspace/read.ts +0 -165
- package/src/workspace/reference.ts +0 -24
- package/src/workspace/stamp.ts +0 -301
- package/src/workspace/write.ts +0 -225
- /package/{src → dist}/pipefy/toolkit_bearer.py +0 -0
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { canonicalString } from "../util/json.js";
|
|
2
|
+
const LINK_KEYS = ['nextAction', 'firstLoopAction', 'children'];
|
|
3
|
+
/** Split the linked list into an ordered array, recording the links as metadata. */
|
|
4
|
+
export const unlinkFlow = (flowRow) => {
|
|
5
|
+
const { version, ...flow } = flowRow;
|
|
6
|
+
const v = (version ?? {});
|
|
7
|
+
const { trigger, ...versionRest } = v;
|
|
8
|
+
const nodes = [];
|
|
9
|
+
const visit = (n) => {
|
|
10
|
+
if (!n)
|
|
11
|
+
return;
|
|
12
|
+
const { nextAction, firstLoopAction, children, ...rest } = n;
|
|
13
|
+
const node = {
|
|
14
|
+
...rest,
|
|
15
|
+
name: String(n['name'] ?? ''),
|
|
16
|
+
type: String(n['type'] ?? ''),
|
|
17
|
+
};
|
|
18
|
+
if (firstLoopAction)
|
|
19
|
+
node['_firstLoopAction'] = String(firstLoopAction['name']);
|
|
20
|
+
if (children) {
|
|
21
|
+
node['_children'] = children.map((c) => (c ? String(c['name']) : null));
|
|
22
|
+
}
|
|
23
|
+
if (nextAction)
|
|
24
|
+
node['_nextAction'] = String(nextAction['name']);
|
|
25
|
+
nodes.push(node);
|
|
26
|
+
if (firstLoopAction)
|
|
27
|
+
visit(firstLoopAction);
|
|
28
|
+
for (const c of (children ?? []))
|
|
29
|
+
visit(c);
|
|
30
|
+
visit(nextAction);
|
|
31
|
+
};
|
|
32
|
+
visit(trigger);
|
|
33
|
+
return { flow: flow, version: versionRest, nodes };
|
|
34
|
+
};
|
|
35
|
+
/** Reassemble the linked list from the ordered array. The inverse of `unlinkFlow`. */
|
|
36
|
+
export const linkFlow = (tree) => {
|
|
37
|
+
const byName = new Map();
|
|
38
|
+
for (const n of tree.nodes)
|
|
39
|
+
byName.set(n.name, n);
|
|
40
|
+
/**
|
|
41
|
+
* Rebuilt depth-first from the trigger, so a node the chain does not reach is
|
|
42
|
+
* simply not in the result — and `assertFlowRoundTrip` then reports it, rather
|
|
43
|
+
* than the node vanishing silently into a flow that looks fine.
|
|
44
|
+
*/
|
|
45
|
+
const seen = new Set();
|
|
46
|
+
const build = (name) => {
|
|
47
|
+
if (!name)
|
|
48
|
+
return undefined;
|
|
49
|
+
const node = byName.get(name);
|
|
50
|
+
if (!node)
|
|
51
|
+
return undefined;
|
|
52
|
+
if (seen.has(name)) {
|
|
53
|
+
throw new Error(`flow codec: node ${JSON.stringify(name)} is reachable twice — the chain has a cycle`);
|
|
54
|
+
}
|
|
55
|
+
seen.add(name);
|
|
56
|
+
const { _nextAction, _firstLoopAction, _children, ...rest } = node;
|
|
57
|
+
const out = { ...rest };
|
|
58
|
+
if (_firstLoopAction) {
|
|
59
|
+
const first = build(String(_firstLoopAction));
|
|
60
|
+
if (first)
|
|
61
|
+
out['firstLoopAction'] = first;
|
|
62
|
+
}
|
|
63
|
+
if (_children) {
|
|
64
|
+
out['children'] = _children.map((c) => build(c ?? undefined) ?? null);
|
|
65
|
+
}
|
|
66
|
+
if (_nextAction) {
|
|
67
|
+
const next = build(String(_nextAction));
|
|
68
|
+
if (next)
|
|
69
|
+
out['nextAction'] = next;
|
|
70
|
+
}
|
|
71
|
+
return out;
|
|
72
|
+
};
|
|
73
|
+
const trigger = build(tree.nodes[0]?.name);
|
|
74
|
+
return { ...tree.flow, version: { ...tree.version, ...(trigger ? { trigger } : {}) } };
|
|
75
|
+
};
|
|
76
|
+
/**
|
|
77
|
+
* `link(unlink(F))` must equal `F`. The exit criterion PLAN-IPAAS sets for the
|
|
78
|
+
* read path, checked per flow rather than asserted once.
|
|
79
|
+
*
|
|
80
|
+
* A linear chain is the easy case; routers with empty branches and nested loops
|
|
81
|
+
* are where a fold like this breaks, so this runs on every pull.
|
|
82
|
+
*/
|
|
83
|
+
export const assertFlowRoundTrip = (flowRow) => {
|
|
84
|
+
const differences = [];
|
|
85
|
+
let rebuilt;
|
|
86
|
+
try {
|
|
87
|
+
rebuilt = linkFlow(unlinkFlow(flowRow));
|
|
88
|
+
}
|
|
89
|
+
catch (e) {
|
|
90
|
+
return { ok: false, differences: [{ where: '$', detail: e.message }] };
|
|
91
|
+
}
|
|
92
|
+
const a = canonicalString(flowRow);
|
|
93
|
+
const b = canonicalString(rebuilt);
|
|
94
|
+
if (a === b)
|
|
95
|
+
return { ok: true, differences: [] };
|
|
96
|
+
// Narrow it to the node that drifted; "the flow differs" is not actionable.
|
|
97
|
+
const before = unlinkFlow(flowRow);
|
|
98
|
+
const after = unlinkFlow(rebuilt);
|
|
99
|
+
if (before.nodes.length !== after.nodes.length) {
|
|
100
|
+
differences.push({
|
|
101
|
+
where: 'nodes',
|
|
102
|
+
detail: `${before.nodes.length} nodes before, ${after.nodes.length} after — ` +
|
|
103
|
+
`${before.nodes
|
|
104
|
+
.map((n) => n.name)
|
|
105
|
+
.filter((n) => !after.nodes.some((m) => m.name === n))
|
|
106
|
+
.join(', ') || 'order changed'} did not survive`,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
for (const n of before.nodes) {
|
|
110
|
+
const m = after.nodes.find((x) => x.name === n.name);
|
|
111
|
+
if (!m)
|
|
112
|
+
continue;
|
|
113
|
+
if (canonicalString(n) !== canonicalString(m)) {
|
|
114
|
+
const keys = [...new Set([...Object.keys(n), ...Object.keys(m)])].filter((k) => canonicalString(n[k]) !== canonicalString(m[k]));
|
|
115
|
+
differences.push({ where: `node ${n.name}`, detail: `differs at ${keys.join(', ')}` });
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
if (canonicalString(before.version) !== canonicalString(after.version)) {
|
|
119
|
+
differences.push({ where: 'version', detail: 'the version row differs' });
|
|
120
|
+
}
|
|
121
|
+
if (!differences.length)
|
|
122
|
+
differences.push({ where: '$', detail: 'canonical forms differ but no node did' });
|
|
123
|
+
return { ok: false, differences };
|
|
124
|
+
};
|
|
125
|
+
/** The node names a flow reaches, in execution order. */
|
|
126
|
+
export const nodeOrder = (tree) => tree.nodes.map((n) => n.name);
|
|
127
|
+
/**
|
|
128
|
+
* Which nodes run strictly before a given one.
|
|
129
|
+
*
|
|
130
|
+
* Execution order is the array order, which is depth-first through the chain — so
|
|
131
|
+
* a node in one router branch does precede a node in a later branch positionally
|
|
132
|
+
* while never running with it. That is why this walks the links instead: only the
|
|
133
|
+
* nodes on the path from the trigger to this node can be referenced by it.
|
|
134
|
+
*/
|
|
135
|
+
export const ancestorsOf = (tree, name) => {
|
|
136
|
+
const byName = new Map(tree.nodes.map((n) => [n.name, n]));
|
|
137
|
+
const path = [];
|
|
138
|
+
const found = new Set();
|
|
139
|
+
const walk = (current) => {
|
|
140
|
+
if (!current)
|
|
141
|
+
return false;
|
|
142
|
+
const node = byName.get(current);
|
|
143
|
+
if (!node)
|
|
144
|
+
return false;
|
|
145
|
+
if (current === name) {
|
|
146
|
+
for (const p of path)
|
|
147
|
+
found.add(p);
|
|
148
|
+
return true;
|
|
149
|
+
}
|
|
150
|
+
path.push(current);
|
|
151
|
+
const branches = [
|
|
152
|
+
node['_firstLoopAction'],
|
|
153
|
+
...(node['_children'] ?? []),
|
|
154
|
+
node['_nextAction'],
|
|
155
|
+
];
|
|
156
|
+
for (const b of branches)
|
|
157
|
+
if (walk(b ?? undefined))
|
|
158
|
+
return true;
|
|
159
|
+
path.pop();
|
|
160
|
+
return false;
|
|
161
|
+
};
|
|
162
|
+
walk(tree.nodes[0]?.name);
|
|
163
|
+
return found;
|
|
164
|
+
};
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { emptyRelations } from "../model/payload.js";
|
|
2
|
+
/**
|
|
3
|
+
* tree → payload. The exact inverse of unpack: it restores the owner-pointer
|
|
4
|
+
* columns that folding dropped and re-flattens the arrays.
|
|
5
|
+
*
|
|
6
|
+
* PLAN.md §3 — pack is not on v1's write path, and is built anyway, because it
|
|
7
|
+
* is the only rigorous proof that unpack is lossless, and because it is v2's
|
|
8
|
+
* engine arriving pre-tested.
|
|
9
|
+
*/
|
|
10
|
+
const num = (v) => {
|
|
11
|
+
if (v === null || v === undefined || v === '')
|
|
12
|
+
return null;
|
|
13
|
+
const n = Number(v);
|
|
14
|
+
return Number.isFinite(n) ? n : null;
|
|
15
|
+
};
|
|
16
|
+
export const unfoldCondition = (cond, sourceType, sourceId, conditions, expressions) => {
|
|
17
|
+
const { expressions: exprs, ...rest } = cond;
|
|
18
|
+
const condRow = { ...rest, source_type: sourceType, source_id: sourceId };
|
|
19
|
+
conditions.push(condRow);
|
|
20
|
+
for (const e of exprs ?? []) {
|
|
21
|
+
expressions.push({ ...e, condition_id: condRow['id'] ?? null });
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
export const pack = (tree) => {
|
|
25
|
+
const relations = emptyRelations();
|
|
26
|
+
for (const phase of tree.phases) {
|
|
27
|
+
const { fields, jump_target_ids, team_member_ids, ...phaseRow } = phase;
|
|
28
|
+
const memberKey = String(phaseRow['_team_member_key'] ?? 'user_id');
|
|
29
|
+
relations.phases.push(phaseRow);
|
|
30
|
+
for (const f of fields ?? []) {
|
|
31
|
+
relations.fields.push({ ...f, phase_id: num(phaseRow['id']) ?? phaseRow['id'] });
|
|
32
|
+
}
|
|
33
|
+
for (const t of jump_target_ids ?? []) {
|
|
34
|
+
relations.phase_jumps.push({ source_phase_id: num(phaseRow['id']), target_phase_id: num(t) });
|
|
35
|
+
}
|
|
36
|
+
for (const m of team_member_ids ?? []) {
|
|
37
|
+
relations.phase_team_memberships.push({ phase_id: num(phaseRow['id']), [memberKey]: num(m) });
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
for (const auto of tree.automations) {
|
|
41
|
+
const { condition, field_maps, distribute_assignments, response_schemas, ...autoRow } = auto;
|
|
42
|
+
relations.automations.push(autoRow);
|
|
43
|
+
const id = autoRow['id'];
|
|
44
|
+
if (condition)
|
|
45
|
+
unfoldCondition(condition, 'Automation', id, relations.conditions, relations.condition_expressions);
|
|
46
|
+
for (const m of field_maps ?? []) {
|
|
47
|
+
relations.field_maps.push({ ...m, owner_type: 'Automation', owner_id: id });
|
|
48
|
+
}
|
|
49
|
+
for (const d of distribute_assignments ?? []) {
|
|
50
|
+
relations.distribute_assignments.push({ ...d, automation_id: id });
|
|
51
|
+
}
|
|
52
|
+
for (const s of response_schemas ?? []) {
|
|
53
|
+
relations.response_schemas.push({ ...s, automation_id: id });
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
for (const fc of tree.field_conditions) {
|
|
57
|
+
const { actions, condition, ...fcRow } = fc;
|
|
58
|
+
relations.field_conditions.push(fcRow);
|
|
59
|
+
for (const a of actions ?? []) {
|
|
60
|
+
relations.field_condition_actions.push({ ...a, field_condition_id: fcRow['id'] });
|
|
61
|
+
}
|
|
62
|
+
if (condition) {
|
|
63
|
+
unfoldCondition(condition, 'FieldCondition', fcRow['id'], relations.conditions, relations.condition_expressions);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
relations.labels = tree.labels.map((r) => ({ ...r }));
|
|
67
|
+
relations.webhooks = tree.webhooks.map((r) => ({ ...r }));
|
|
68
|
+
relations.pipe_relations = tree.pipe_relations.map((r) => ({ ...r }));
|
|
69
|
+
relations.public_forms = tree.public_form ? [{ ...tree.public_form }] : [];
|
|
70
|
+
relations.repo_preferences = tree.preferences ? [{ ...tree.preferences }] : [];
|
|
71
|
+
relations.email_templates = tree.readonly.email_templates.map((r) => ({ ...r }));
|
|
72
|
+
relations.email_inboxes = tree.readonly.email_inboxes.map((r) => ({ ...r }));
|
|
73
|
+
relations.visibilities = tree.readonly.visibilities.map((r) => ({ ...r }));
|
|
74
|
+
// Restores the non-Automation field_maps rows the loop above never sees.
|
|
75
|
+
for (const m of tree.readonly.field_maps)
|
|
76
|
+
relations.field_maps.push({ ...m });
|
|
77
|
+
return { ...tree.repo, id: Number(tree.repo['id']), relations };
|
|
78
|
+
};
|
|
79
|
+
/** Pack back into an envelope, preserving the provenance the tree carries. */
|
|
80
|
+
export const packEnvelope = (tree) => ({
|
|
81
|
+
source: tree.meta.source,
|
|
82
|
+
versionId: tree.meta.versionId ?? null,
|
|
83
|
+
readAt: tree.meta.readAt,
|
|
84
|
+
repoKind: tree.meta.repoKind,
|
|
85
|
+
coverage: tree.meta.coverage,
|
|
86
|
+
notes: tree.meta.notes,
|
|
87
|
+
payload: pack(tree),
|
|
88
|
+
});
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { canonical, canonicalString } from "../util/json.js";
|
|
2
|
+
import { ENTITIES, ENTITY_NAMES } from "../model/payload.js";
|
|
3
|
+
import { VOLATILE_KEYS } from "../model/volatile.js";
|
|
4
|
+
import { pack } from "./pack.js";
|
|
5
|
+
import { unpack } from "./unpack.js";
|
|
6
|
+
/**
|
|
7
|
+
* `pack(unpack(P)) ≡ P` under a canonical form — PLAN.md §3, corrected there
|
|
8
|
+
* from byte equality to semantic equality. Chasing byte-identity would waste a
|
|
9
|
+
* week and then fail: JSON.parse turns `index: 6.0` into `6`.
|
|
10
|
+
*
|
|
11
|
+
* Two normalisations beyond canonical JSON, both principled rather than
|
|
12
|
+
* convenient:
|
|
13
|
+
*
|
|
14
|
+
* 1. Volatile columns are dropped from both sides. The codec strips them by
|
|
15
|
+
* design (PLAN.md §9); comparing them would only ever report noise.
|
|
16
|
+
* 2. For `owned` entities — the fourteen arrays with no `uuid` and no
|
|
17
|
+
* identity of their own — `id` is dropped. ROUTE-B-V2.md §4 argues these
|
|
18
|
+
* are replace-sets under their owner, which is precisely the claim that
|
|
19
|
+
* their `id` is not identity. Rows are then compared as sorted sets.
|
|
20
|
+
*
|
|
21
|
+
* Anything else that differs is a real codec bug and blocks planning.
|
|
22
|
+
*/
|
|
23
|
+
const stripDeep = (v) => {
|
|
24
|
+
if (Array.isArray(v))
|
|
25
|
+
return v.map(stripDeep);
|
|
26
|
+
if (v && typeof v === 'object') {
|
|
27
|
+
const out = {};
|
|
28
|
+
for (const [k, val] of Object.entries(v)) {
|
|
29
|
+
if (VOLATILE_KEYS.has(k))
|
|
30
|
+
continue;
|
|
31
|
+
if (k.startsWith('_'))
|
|
32
|
+
continue; // tool metadata, never pipe structure
|
|
33
|
+
out[k] = stripDeep(val);
|
|
34
|
+
}
|
|
35
|
+
return out;
|
|
36
|
+
}
|
|
37
|
+
return v;
|
|
38
|
+
};
|
|
39
|
+
const normaliseRows = (name, rows) => {
|
|
40
|
+
const owned = ENTITIES[name].identity === 'owned';
|
|
41
|
+
const cleaned = rows.map((r) => {
|
|
42
|
+
const row = stripDeep(r);
|
|
43
|
+
if (owned)
|
|
44
|
+
delete row['id'];
|
|
45
|
+
return canonicalString(row);
|
|
46
|
+
});
|
|
47
|
+
// Owned rows are sets; identified rows keep their order-independent identity
|
|
48
|
+
// too, so sorting is safe for both and removes ordering noise the codec
|
|
49
|
+
// introduces by grouping fields under phases.
|
|
50
|
+
return cleaned.sort();
|
|
51
|
+
};
|
|
52
|
+
export const compare = (before, after) => {
|
|
53
|
+
const differences = [];
|
|
54
|
+
const rootBefore = canonical(stripDeep({ ...before, relations: undefined }));
|
|
55
|
+
const rootAfter = canonical(stripDeep({ ...after, relations: undefined }));
|
|
56
|
+
const rootKeys = new Set([...Object.keys(rootBefore), ...Object.keys(rootAfter)]);
|
|
57
|
+
for (const k of [...rootKeys].sort()) {
|
|
58
|
+
const a = JSON.stringify(rootBefore[k]);
|
|
59
|
+
const b = JSON.stringify(rootAfter[k]);
|
|
60
|
+
if (a !== b)
|
|
61
|
+
differences.push({ where: `repo.${k}`, detail: `${a} -> ${b}` });
|
|
62
|
+
}
|
|
63
|
+
for (const name of ENTITY_NAMES) {
|
|
64
|
+
const a = normaliseRows(name, before.relations?.[name] ?? []);
|
|
65
|
+
const b = normaliseRows(name, after.relations?.[name] ?? []);
|
|
66
|
+
if (JSON.stringify(a) === JSON.stringify(b))
|
|
67
|
+
continue;
|
|
68
|
+
if (a.length !== b.length) {
|
|
69
|
+
differences.push({ where: name, detail: `${a.length} rows before, ${b.length} after` });
|
|
70
|
+
}
|
|
71
|
+
const setB = new Set(b);
|
|
72
|
+
const lost = a.filter((r) => !setB.has(r));
|
|
73
|
+
const setA = new Set(a);
|
|
74
|
+
const gained = b.filter((r) => !setA.has(r));
|
|
75
|
+
for (const l of lost.slice(0, 3))
|
|
76
|
+
differences.push({ where: `${name} (lost)`, detail: l.slice(0, 300) });
|
|
77
|
+
for (const g of gained.slice(0, 3))
|
|
78
|
+
differences.push({ where: `${name} (gained)`, detail: g.slice(0, 300) });
|
|
79
|
+
if (lost.length + gained.length > 6) {
|
|
80
|
+
differences.push({ where: name, detail: `…and ${lost.length + gained.length - 6} more row differences` });
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return { ok: differences.length === 0, differences };
|
|
84
|
+
};
|
|
85
|
+
/** The Phase 0 exit criterion, callable on any payload. */
|
|
86
|
+
export const assertRoundTrip = (env) => compare(env.payload, pack(unpack(env)));
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { rel } from "../model/payload.js";
|
|
2
|
+
import { stripVolatile } from "../model/volatile.js";
|
|
3
|
+
/**
|
|
4
|
+
* payload → tree.
|
|
5
|
+
*
|
|
6
|
+
* Folding is *movement only*: a folded row is the payload row, unchanged, moved
|
|
7
|
+
* under its owner. The only rewriting is (a) stripping volatile columns and
|
|
8
|
+
* (b) dropping the owner-pointer columns that folding makes redundant, both of
|
|
9
|
+
* which `pack` restores. That is what makes `pack(unpack(P)) ≡ P` a property of
|
|
10
|
+
* the design rather than of the care taken.
|
|
11
|
+
*
|
|
12
|
+
* Keys beginning with `_` are tool metadata, not pipe structure. They survive a
|
|
13
|
+
* round trip and are ignored by every comparator.
|
|
14
|
+
*/
|
|
15
|
+
const idOf = (r) => String(r['id'] ?? '');
|
|
16
|
+
const num = (v) => {
|
|
17
|
+
if (v === null || v === undefined || v === '')
|
|
18
|
+
return null;
|
|
19
|
+
const n = Number(v);
|
|
20
|
+
return Number.isFinite(n) ? n : null;
|
|
21
|
+
};
|
|
22
|
+
const byIndex = (a, b) => {
|
|
23
|
+
const ai = num(a['index']);
|
|
24
|
+
const bi = num(b['index']);
|
|
25
|
+
if (ai !== null && bi !== null && ai !== bi)
|
|
26
|
+
return ai - bi;
|
|
27
|
+
return (num(a['id']) ?? 0) - (num(b['id']) ?? 0);
|
|
28
|
+
};
|
|
29
|
+
/** Which key a phase_team_memberships row uses for the user id. */
|
|
30
|
+
const teamMemberKey = (row) => {
|
|
31
|
+
for (const k of ['user_id', 'member_id', 'team_member_id', 'user']) {
|
|
32
|
+
if (k in row && num(row[k]) !== null)
|
|
33
|
+
return k;
|
|
34
|
+
}
|
|
35
|
+
return null;
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* Attach a condition to its owner. `conditions` rows point at their owner with
|
|
39
|
+
* (source_type, source_id); expressions point at the condition with
|
|
40
|
+
* `condition_id`, except in a reconstructed payload where no condition id
|
|
41
|
+
* exists and `_condition_owner` carries the same information.
|
|
42
|
+
*/
|
|
43
|
+
const takeCondition = (payload, sourceType, sourceId) => {
|
|
44
|
+
const cond = rel(payload, 'conditions').find((c) => String(c['source_type']) === sourceType && String(c['source_id']) === sourceId);
|
|
45
|
+
if (!cond)
|
|
46
|
+
return null;
|
|
47
|
+
const condId = idOf(cond);
|
|
48
|
+
const expressions = rel(payload, 'condition_expressions')
|
|
49
|
+
.filter((e) => {
|
|
50
|
+
if (condId && String(e['condition_id'] ?? '') === condId)
|
|
51
|
+
return true;
|
|
52
|
+
const owner = e['_condition_owner'];
|
|
53
|
+
return Boolean(owner && String(owner['source_type']) === sourceType && String(owner['source_id']) === sourceId);
|
|
54
|
+
})
|
|
55
|
+
.map((e) => {
|
|
56
|
+
const row = stripVolatile({ ...e });
|
|
57
|
+
// Restored by pack from the owning condition.
|
|
58
|
+
delete row['condition_id'];
|
|
59
|
+
delete row['_condition_owner'];
|
|
60
|
+
return row;
|
|
61
|
+
})
|
|
62
|
+
.sort((a, b) => (num(a['structure_id']) ?? 0) - (num(b['structure_id']) ?? 0) || (num(a['id']) ?? 0) - (num(b['id']) ?? 0));
|
|
63
|
+
const node = stripVolatile({ ...cond });
|
|
64
|
+
// Restored by pack from the owner it is nested under.
|
|
65
|
+
delete node['source_type'];
|
|
66
|
+
delete node['source_id'];
|
|
67
|
+
node.expressions = expressions;
|
|
68
|
+
return node;
|
|
69
|
+
};
|
|
70
|
+
export const unpack = (env) => {
|
|
71
|
+
const p = env.payload;
|
|
72
|
+
const root = {};
|
|
73
|
+
for (const [k, v] of Object.entries(p)) {
|
|
74
|
+
if (k === 'relations')
|
|
75
|
+
continue;
|
|
76
|
+
root[k] = v;
|
|
77
|
+
}
|
|
78
|
+
const fieldsByPhase = new Map();
|
|
79
|
+
for (const f of rel(p, 'fields')) {
|
|
80
|
+
const key = String(f['phase_id'] ?? '');
|
|
81
|
+
const arr = fieldsByPhase.get(key) ?? [];
|
|
82
|
+
arr.push(f);
|
|
83
|
+
fieldsByPhase.set(key, arr);
|
|
84
|
+
}
|
|
85
|
+
const phases = [...rel(p, 'phases')].sort(byIndex).map((raw) => {
|
|
86
|
+
const phaseId = idOf(raw);
|
|
87
|
+
const node = stripVolatile({ ...raw });
|
|
88
|
+
node.fields = (fieldsByPhase.get(phaseId) ?? [])
|
|
89
|
+
.slice()
|
|
90
|
+
.sort(byIndex)
|
|
91
|
+
.map((f) => stripVolatile({ ...f }));
|
|
92
|
+
// A jump row can have a null target — a jump slot enabled in the UI with
|
|
93
|
+
// no destination phase chosen yet. Kept, not filtered: a flat id array
|
|
94
|
+
// has no other way to represent "the row exists, its target does not",
|
|
95
|
+
// and dropping it here is unrecoverable by the time `pack` runs.
|
|
96
|
+
node.jump_target_ids = rel(p, 'phase_jumps')
|
|
97
|
+
.filter((j) => String(j['source_phase_id']) === phaseId)
|
|
98
|
+
.map((j) => num(j['target_phase_id']))
|
|
99
|
+
.sort((a, b) => (a === null ? 1 : b === null ? -1 : a - b));
|
|
100
|
+
const memberships = rel(p, 'phase_team_memberships').filter((m) => String(m['phase_id']) === phaseId);
|
|
101
|
+
const key = memberships.length ? teamMemberKey(memberships[0]) : null;
|
|
102
|
+
node.team_member_ids = key
|
|
103
|
+
? memberships.map((m) => num(m[key])).filter((v) => v !== null).sort((a, b) => a - b)
|
|
104
|
+
: [];
|
|
105
|
+
if (key && key !== 'user_id')
|
|
106
|
+
node['_team_member_key'] = key;
|
|
107
|
+
return node;
|
|
108
|
+
});
|
|
109
|
+
const automations = rel(p, 'automations').map((raw) => {
|
|
110
|
+
const id = idOf(raw);
|
|
111
|
+
const node = stripVolatile({ ...raw });
|
|
112
|
+
node.condition = takeCondition(p, 'Automation', id);
|
|
113
|
+
node.field_maps = rel(p, 'field_maps')
|
|
114
|
+
.filter((m) => String(m['owner_type']) === 'Automation' && String(m['owner_id']) === id)
|
|
115
|
+
.map((m) => {
|
|
116
|
+
const row = stripVolatile({ ...m });
|
|
117
|
+
delete row['owner_type'];
|
|
118
|
+
delete row['owner_id'];
|
|
119
|
+
return row;
|
|
120
|
+
});
|
|
121
|
+
node.distribute_assignments = rel(p, 'distribute_assignments')
|
|
122
|
+
.filter((d) => String(d['automation_id'] ?? d['owner_id'] ?? '') === id)
|
|
123
|
+
.map((d) => {
|
|
124
|
+
const row = stripVolatile({ ...d });
|
|
125
|
+
delete row['automation_id'];
|
|
126
|
+
return row;
|
|
127
|
+
});
|
|
128
|
+
node.response_schemas = rel(p, 'response_schemas')
|
|
129
|
+
.filter((s) => String(s['automation_id'] ?? s['owner_id'] ?? '') === id)
|
|
130
|
+
.map((s) => {
|
|
131
|
+
const row = stripVolatile({ ...s });
|
|
132
|
+
delete row['automation_id'];
|
|
133
|
+
return row;
|
|
134
|
+
});
|
|
135
|
+
return node;
|
|
136
|
+
});
|
|
137
|
+
const field_conditions = [...rel(p, 'field_conditions')].sort(byIndex).map((raw) => {
|
|
138
|
+
const id = idOf(raw);
|
|
139
|
+
const node = stripVolatile({ ...raw });
|
|
140
|
+
node.actions = rel(p, 'field_condition_actions')
|
|
141
|
+
.filter((a) => String(a['field_condition_id']) === id)
|
|
142
|
+
.map((a) => {
|
|
143
|
+
const row = stripVolatile({ ...a });
|
|
144
|
+
delete row['field_condition_id'];
|
|
145
|
+
return row;
|
|
146
|
+
});
|
|
147
|
+
node.condition = takeCondition(p, 'FieldCondition', id);
|
|
148
|
+
return node;
|
|
149
|
+
});
|
|
150
|
+
const simple = (n) => rel(p, n).map((r) => stripVolatile({ ...r }));
|
|
151
|
+
return {
|
|
152
|
+
meta: {
|
|
153
|
+
source: env.source,
|
|
154
|
+
versionId: env.versionId ?? null,
|
|
155
|
+
readAt: env.readAt,
|
|
156
|
+
repoKind: env.repoKind,
|
|
157
|
+
coverage: env.coverage,
|
|
158
|
+
repo_columns: env.repo_columns ?? 'complete',
|
|
159
|
+
notes: env.notes,
|
|
160
|
+
},
|
|
161
|
+
repo: stripVolatile(root),
|
|
162
|
+
phases,
|
|
163
|
+
automations,
|
|
164
|
+
field_conditions,
|
|
165
|
+
labels: simple('labels'),
|
|
166
|
+
webhooks: simple('webhooks'),
|
|
167
|
+
pipe_relations: simple('pipe_relations'),
|
|
168
|
+
public_form: simple('public_forms')[0] ?? null,
|
|
169
|
+
preferences: simple('repo_preferences')[0] ?? null,
|
|
170
|
+
readonly: {
|
|
171
|
+
email_templates: simple('email_templates'),
|
|
172
|
+
email_inboxes: simple('email_inboxes'),
|
|
173
|
+
visibilities: simple('visibilities'),
|
|
174
|
+
// owner_type/owner_id kept — see model/tree.ts. The automation-owned half
|
|
175
|
+
// above already claimed and stripped its rows; this is everything left.
|
|
176
|
+
field_maps: rel(p, 'field_maps')
|
|
177
|
+
.filter((m) => String(m['owner_type']) !== 'Automation')
|
|
178
|
+
.map((m) => stripVolatile({ ...m })),
|
|
179
|
+
},
|
|
180
|
+
};
|
|
181
|
+
};
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { info, dim, ok, warn, debug } from "../util/log.js";
|
|
2
|
+
import { fetchAgents } from "../pipefy/agents.js";
|
|
3
|
+
import { writeAgents, writeAgentBaseline, readAgents, readAgentBaseline, unknownAgents, agentDirName, } from "../workspace/agents.js";
|
|
4
|
+
import { readTree } from "../workspace/read.js";
|
|
5
|
+
import { diffAgents, groupBehaviorsByAgentDir } from "../diff/agents.js";
|
|
6
|
+
import { compileAgents } from "../apply/agentops.js";
|
|
7
|
+
/**
|
|
8
|
+
* The agent half of the pipe loop: read, diff, plan, apply.
|
|
9
|
+
*
|
|
10
|
+
* A behavior's own edits already flow through the ordinary automation
|
|
11
|
+
* diff/plan/apply once it is placed under its agent's `behaviors/`
|
|
12
|
+
* (workspace/agents.ts) — the row is folded into `tree.automations` like any
|
|
13
|
+
* other automation. But *sending* one is not the automation mutations: this
|
|
14
|
+
* pipe's action catalogue does not offer `ai_behavior` to `createAutomation`
|
|
15
|
+
* (confirmed live, refused), and the confirmed write path — captured from
|
|
16
|
+
* real Pipefy web UI traffic and reproduced directly — is a single
|
|
17
|
+
* `createAiAgent` / `updateAiAgent` call carrying the agent's *entire*
|
|
18
|
+
* behaviors list. So `diff/diff.ts` and `apply/compile.ts` are taught to skip
|
|
19
|
+
* any automation tagged `_agent_dir`, and this module is the whole agent-side
|
|
20
|
+
* pipeline instead: read the agent's fields plus its current behaviors
|
|
21
|
+
* (grouped from the already-read tree, since `agent.json` never carries them),
|
|
22
|
+
* diff against the baseline, compile straight to `createAiAgent` /
|
|
23
|
+
* `updateAiAgent` / `deleteAiAgent`, and send.
|
|
24
|
+
*/
|
|
25
|
+
/** Read a pipe's AI agents, or record why they could not be read. */
|
|
26
|
+
export const pullAgentsFor = async (client, repoUuid) => {
|
|
27
|
+
if (!repoUuid)
|
|
28
|
+
return unknownAgents('this repo has no uuid on the row that was read, so agents cannot be looked up');
|
|
29
|
+
const read = await fetchAgents(client, repoUuid);
|
|
30
|
+
return {
|
|
31
|
+
meta: {
|
|
32
|
+
coverage: read.status,
|
|
33
|
+
readAt: new Date().toISOString(),
|
|
34
|
+
agents: read.status === 'complete' ? read.agents.length : undefined,
|
|
35
|
+
notes: read.note ? [read.note] : [],
|
|
36
|
+
},
|
|
37
|
+
agents: read.agents,
|
|
38
|
+
};
|
|
39
|
+
};
|
|
40
|
+
/** Pull and write, mirroring `pullAndWriteFlows`. Returns what was read. */
|
|
41
|
+
export const pullAndWriteAgents = async (client, repoId, repoUuid, repoDir, opts = {}) => {
|
|
42
|
+
const section = await pullAgentsFor(client, repoUuid);
|
|
43
|
+
await writeAgents(repoDir, section);
|
|
44
|
+
if (opts.workspaceRoot)
|
|
45
|
+
await writeAgentBaseline(opts.workspaceRoot, repoId, section);
|
|
46
|
+
return section;
|
|
47
|
+
};
|
|
48
|
+
/**
|
|
49
|
+
* The agent diff: baseline against the workspace's own files.
|
|
50
|
+
*
|
|
51
|
+
* `tree`, if the caller already read one for the pipe half, is reused rather
|
|
52
|
+
* than reading the whole workspace a second time — `agent.json` alone cannot
|
|
53
|
+
* answer "what are this agent's current behaviors", only `tree.automations`
|
|
54
|
+
* (filtered to what `_agent_dir` tags) can.
|
|
55
|
+
*/
|
|
56
|
+
export const diffAgentsFor = async (root, repoId, dir, opts = {}) => {
|
|
57
|
+
const baseline = await readAgentBaseline(root, repoId);
|
|
58
|
+
const authored = await readAgents(dir);
|
|
59
|
+
const tree = opts.tree ?? (await readTree(dir));
|
|
60
|
+
const behaviorsByDir = groupBehaviorsByAgentDir(tree.automations);
|
|
61
|
+
return diffAgents(baseline, authored, behaviorsByDir);
|
|
62
|
+
};
|
|
63
|
+
export const planAgents = (client, cs) => compileAgents(client, cs);
|
|
64
|
+
/**
|
|
65
|
+
* The revert change set, given both sides already read — the pure half of
|
|
66
|
+
* `planAgentRevert`, mirroring `buildFlowRevertPlan` (commands/flows.ts).
|
|
67
|
+
*
|
|
68
|
+
* A live agent read is self-contained — `AgentRow.behaviors` carries the full
|
|
69
|
+
* list (pipefy/agents.ts), unlike the authored-files side, which needs a
|
|
70
|
+
* whole tree to answer the same question because a behavior's file lives
|
|
71
|
+
* under its agent's `behaviors/` folder, not inside `agent.json`. So `before`
|
|
72
|
+
* alone is enough to build the map `diffAgents` needs for the *target* side.
|
|
73
|
+
*
|
|
74
|
+
* Direction is the mirror of an ordinary apply: `live` is the current state
|
|
75
|
+
* (baseline), `before` is the old pre-apply one (intended target).
|
|
76
|
+
*/
|
|
77
|
+
export const buildAgentRevertChangeSet = (live, before) => {
|
|
78
|
+
const behaviorsByDir = new Map(before.agents.map((a) => [agentDirName(a), a.behaviors ?? []]));
|
|
79
|
+
return diffAgents(live, before, behaviorsByDir);
|
|
80
|
+
};
|
|
81
|
+
/**
|
|
82
|
+
* What reverting to a pre-apply agent state would take — computed, not sent.
|
|
83
|
+
*
|
|
84
|
+
* `before` is a live read taken right before this apply's own agent diff was
|
|
85
|
+
* computed — the only rollback point agents have, since they are not part of
|
|
86
|
+
* the snapshot payload at all (ROUTE-B-V2.md ask 5) and their diff is
|
|
87
|
+
* ordinarily computed offline against the local baseline, which never took
|
|
88
|
+
* this read on its own. `live` here is read fresh, because some steps may
|
|
89
|
+
* have landed since `before` was taken.
|
|
90
|
+
*/
|
|
91
|
+
export const planAgentRevert = async (client, repoUuid, before) => {
|
|
92
|
+
const live = await pullAgentsFor(client, repoUuid);
|
|
93
|
+
const changeSet = buildAgentRevertChangeSet(live, before);
|
|
94
|
+
return { plan: await planAgents(client, changeSet), live };
|
|
95
|
+
};
|
|
96
|
+
export const renderAgentPlan = (plan) => {
|
|
97
|
+
const lines = [];
|
|
98
|
+
if (!plan.steps.length) {
|
|
99
|
+
lines.push(' agents: nothing to apply');
|
|
100
|
+
return lines.join('\n');
|
|
101
|
+
}
|
|
102
|
+
lines.push('');
|
|
103
|
+
lines.push(` ${plan.steps.length} agent step(s)`);
|
|
104
|
+
for (const s of plan.steps) {
|
|
105
|
+
lines.push(` ${s.id} ${s.description}${s.destructive ? ' destructive' : ''}`);
|
|
106
|
+
}
|
|
107
|
+
return lines.join('\n');
|
|
108
|
+
};
|
|
109
|
+
/**
|
|
110
|
+
* Execute the agent steps.
|
|
111
|
+
*
|
|
112
|
+
* No resumability, no placeholder substitution — unlike the pipe half's
|
|
113
|
+
* executor, an agent step never depends on an id created earlier in the same
|
|
114
|
+
* plan (an agent's own uuid is not referenced by anything else this tool
|
|
115
|
+
* writes), so the machinery `apply/execute.ts` needs for that would be
|
|
116
|
+
* unused weight here. Same simplification the flow half already makes.
|
|
117
|
+
*/
|
|
118
|
+
export const applyAgentSteps = async (client, plan, opts = {}) => {
|
|
119
|
+
for (const step of plan.steps) {
|
|
120
|
+
if (opts.dryRun) {
|
|
121
|
+
step.status = 'skipped';
|
|
122
|
+
info(dim(` (dry run) ${step.id} ${step.description} — not sent`));
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
step.status = 'running';
|
|
126
|
+
try {
|
|
127
|
+
if (step.request.missingRequired?.length) {
|
|
128
|
+
throw new Error(`missing required field(s): ${step.request.missingRequired.join(', ')}`);
|
|
129
|
+
}
|
|
130
|
+
debug(JSON.stringify(step.request.variables ?? {}).slice(0, 400));
|
|
131
|
+
step.result = await client.raw(step.request.query ?? '', step.request.variables, step.description);
|
|
132
|
+
step.status = 'done';
|
|
133
|
+
ok(`${step.id} ${step.description}`);
|
|
134
|
+
}
|
|
135
|
+
catch (e) {
|
|
136
|
+
step.status = 'failed';
|
|
137
|
+
step.error = e.message;
|
|
138
|
+
warn(`${step.id} ${step.description} — ${step.error}`);
|
|
139
|
+
if (!opts.continueOnError)
|
|
140
|
+
break;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
const count = (s) => plan.steps.filter((x) => x.status === s).length;
|
|
144
|
+
return { done: count('done'), failed: count('failed'), skipped: count('skipped'), steps: plan.steps };
|
|
145
|
+
};
|