@pipefy/pipefy-process-coder 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (159) hide show
  1. package/.agents/skills/ppc-pipefy-flow-authoring/SKILL.md +262 -262
  2. package/.agents/skills/ppc-pipefy-flow-authoring/examples/01-main-2-danfe-consulta.json +247 -247
  3. package/.agents/skills/ppc-pipefy-flow-authoring/examples/01-main-2-webhook-retorno-consulta.json +589 -589
  4. package/.agents/skills/ppc-pipefy-flow-authoring/examples/01-main-4-recebimento-barramento.json +1391 -1391
  5. package/.agents/skills/ppc-pipefy-flow-authoring/examples/02-subflow-2-danfe-retorno.json +623 -623
  6. package/.agents/skills/ppc-pipefy-flow-authoring/examples/02-subflow-4-criacao-operacao.json +636 -636
  7. package/.agents/skills/ppc-pipefy-flow-authoring/examples/03-subflow-4-criacao-titulo.json +3642 -3642
  8. package/.agents/skills/ppc-pipefy-flow-authoring/examples/04-subflow-4-criacao-cedente.json +863 -863
  9. package/.agents/skills/ppc-pipefy-flow-authoring/examples/04-subflow-4-criacao-sacado.json +799 -799
  10. package/.agents/skills/ppc-pipefy-flow-authoring/examples/README.md +42 -42
  11. package/.agents/skills/ppc-pipefy-flow-authoring/examples/hml-01-subflow-1-titulo-acompanhamento-cobranca.json +581 -581
  12. package/.agents/skills/ppc-pipefy-flow-authoring/examples/hml-01-subflow-1-titulo-nfe-monitoramento.json +503 -503
  13. package/.agents/skills/ppc-pipefy-flow-authoring/examples/hml-01-subflow-1-titulo-retorno-bancario.json +562 -562
  14. package/.agents/skills/ppc-pipefy-flow-authoring/examples/hml-02-subflow-2-retorno-consulta-cedente.json +557 -557
  15. package/.agents/skills/ppc-pipefy-flow-authoring/examples/hml-02-subflow-2-retorno-consulta-sacado.json +609 -609
  16. package/.agents/skills/ppc-pipefy-pipe-authoring/SKILL.md +146 -146
  17. package/.agents/skills/ppc-pipefy-process-design/SKILL.md +127 -127
  18. package/.agents/skills/ppc-pipefy-workspace/SKILL.md +91 -91
  19. package/AGENTS.md +465 -456
  20. package/README.md +825 -807
  21. package/bin/pipe.js +25 -4
  22. package/dist/apply/adopt.js +128 -0
  23. package/dist/apply/agentops.js +42 -0
  24. package/dist/apply/compile.js +722 -0
  25. package/dist/apply/execute.js +277 -0
  26. package/dist/apply/flowops.js +293 -0
  27. package/dist/apply/idmap.js +233 -0
  28. package/dist/apply/mutations.js +704 -0
  29. package/dist/apply/registry.js +348 -0
  30. package/dist/apply/types.js +18 -0
  31. package/dist/cli/args.js +79 -0
  32. package/dist/cli.js +187 -0
  33. package/dist/codec/flow.js +164 -0
  34. package/dist/codec/pack.js +88 -0
  35. package/dist/codec/roundtrip.js +86 -0
  36. package/dist/codec/unpack.js +181 -0
  37. package/dist/commands/agents.js +145 -0
  38. package/dist/commands/apply.js +982 -0
  39. package/dist/commands/context.js +79 -0
  40. package/dist/commands/create.js +63 -0
  41. package/dist/commands/diff.js +294 -0
  42. package/dist/commands/flows.js +348 -0
  43. package/dist/commands/misc.js +528 -0
  44. package/dist/commands/plan.js +301 -0
  45. package/dist/commands/pull.js +511 -0
  46. package/dist/commands/runs.js +74 -0
  47. package/dist/commands/skills.js +131 -0
  48. package/dist/commands/verify.js +205 -0
  49. package/dist/config.js +125 -0
  50. package/dist/diff/agents.js +80 -0
  51. package/dist/diff/diff.js +983 -0
  52. package/dist/diff/flow.js +258 -0
  53. package/{src/diff/html.ts → dist/diff/html.js} +274 -322
  54. package/dist/diff/render.js +90 -0
  55. package/dist/model/payload.js +84 -0
  56. package/dist/model/tree.js +7 -0
  57. package/dist/model/volatile.js +54 -0
  58. package/dist/pipefy/agents.js +122 -0
  59. package/dist/pipefy/automations.js +155 -0
  60. package/dist/pipefy/capability.js +84 -0
  61. package/dist/pipefy/client.js +204 -0
  62. package/dist/pipefy/discovery.js +148 -0
  63. package/dist/pipefy/internal.js +315 -0
  64. package/dist/pipefy/ipaas.js +269 -0
  65. package/dist/pipefy/reconstruct.js +674 -0
  66. package/dist/pipefy/reference.js +172 -0
  67. package/dist/pipefy/snapshot.js +185 -0
  68. package/dist/pipefy/toolkit.js +158 -0
  69. package/dist/report/integrations.js +165 -0
  70. package/dist/report/run.js +355 -0
  71. package/dist/util/fsx.js +50 -0
  72. package/dist/util/git.js +25 -0
  73. package/dist/util/json.js +56 -0
  74. package/dist/util/log.js +69 -0
  75. package/dist/util/pool.js +42 -0
  76. package/dist/util/slug.js +23 -0
  77. package/dist/util/tui.js +287 -0
  78. package/dist/validate/index.js +86 -0
  79. package/dist/validate/integrity.js +370 -0
  80. package/dist/validate/reference.js +120 -0
  81. package/dist/validate/schema.js +278 -0
  82. package/dist/workspace/agents.js +245 -0
  83. package/{src/workspace/docs.ts → dist/workspace/docs.js} +367 -407
  84. package/dist/workspace/flows.js +149 -0
  85. package/dist/workspace/layout.js +146 -0
  86. package/dist/workspace/lock.js +83 -0
  87. package/dist/workspace/read.js +168 -0
  88. package/dist/workspace/reference.js +24 -0
  89. package/dist/workspace/stamp.js +234 -0
  90. package/dist/workspace/write.js +194 -0
  91. package/package.json +37 -35
  92. package/src/apply/adopt.ts +0 -150
  93. package/src/apply/agentops.ts +0 -71
  94. package/src/apply/compile.ts +0 -875
  95. package/src/apply/execute.ts +0 -336
  96. package/src/apply/flowops.ts +0 -399
  97. package/src/apply/idmap.ts +0 -241
  98. package/src/apply/mutations.ts +0 -955
  99. package/src/apply/registry.ts +0 -430
  100. package/src/apply/types.ts +0 -134
  101. package/src/cli/args.ts +0 -88
  102. package/src/cli.ts +0 -211
  103. package/src/codec/flow.ts +0 -199
  104. package/src/codec/pack.ts +0 -103
  105. package/src/codec/roundtrip.ts +0 -94
  106. package/src/codec/unpack.ts +0 -198
  107. package/src/commands/agents.ts +0 -184
  108. package/src/commands/apply.ts +0 -1144
  109. package/src/commands/context.ts +0 -119
  110. package/src/commands/create.ts +0 -79
  111. package/src/commands/diff.ts +0 -314
  112. package/src/commands/flows.ts +0 -414
  113. package/src/commands/misc.ts +0 -644
  114. package/src/commands/plan.ts +0 -331
  115. package/src/commands/pull.ts +0 -567
  116. package/src/commands/runs.ts +0 -83
  117. package/src/commands/skills.ts +0 -137
  118. package/src/commands/verify.ts +0 -253
  119. package/src/config.ts +0 -168
  120. package/src/diff/agents.ts +0 -122
  121. package/src/diff/diff.ts +0 -1130
  122. package/src/diff/flow.ts +0 -318
  123. package/src/diff/render.ts +0 -101
  124. package/src/model/payload.ts +0 -154
  125. package/src/model/tree.ts +0 -99
  126. package/src/model/volatile.ts +0 -55
  127. package/src/pipefy/agents.ts +0 -165
  128. package/src/pipefy/automations.ts +0 -219
  129. package/src/pipefy/capability.ts +0 -119
  130. package/src/pipefy/client.ts +0 -267
  131. package/src/pipefy/discovery.ts +0 -209
  132. package/src/pipefy/internal.ts +0 -380
  133. package/src/pipefy/ipaas.ts +0 -365
  134. package/src/pipefy/reconstruct.ts +0 -775
  135. package/src/pipefy/reference.ts +0 -251
  136. package/src/pipefy/snapshot.ts +0 -245
  137. package/src/pipefy/toolkit.ts +0 -200
  138. package/src/report/integrations.ts +0 -231
  139. package/src/report/run.ts +0 -475
  140. package/src/util/fsx.ts +0 -45
  141. package/src/util/git.ts +0 -32
  142. package/src/util/json.ts +0 -55
  143. package/src/util/log.ts +0 -76
  144. package/src/util/pool.ts +0 -48
  145. package/src/util/slug.ts +0 -26
  146. package/src/util/tui.ts +0 -335
  147. package/src/validate/index.ts +0 -123
  148. package/src/validate/integrity.ts +0 -387
  149. package/src/validate/reference.ts +0 -136
  150. package/src/validate/schema.ts +0 -328
  151. package/src/workspace/agents.ts +0 -290
  152. package/src/workspace/flows.ts +0 -191
  153. package/src/workspace/layout.ts +0 -165
  154. package/src/workspace/lock.ts +0 -148
  155. package/src/workspace/read.ts +0 -165
  156. package/src/workspace/reference.ts +0 -24
  157. package/src/workspace/stamp.ts +0 -301
  158. package/src/workspace/write.ts +0 -225
  159. /package/{src → dist}/pipefy/toolkit_bearer.py +0 -0
@@ -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
+ };