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