@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
package/src/diff/flow.ts
DELETED
|
@@ -1,318 +0,0 @@
|
|
|
1
|
-
import { canonicalString } from '../util/json.ts';
|
|
2
|
-
import type { FlowTree, FlowNode } from '../codec/flow.ts';
|
|
3
|
-
import type { FlowsSection } from '../workspace/flows.ts';
|
|
4
|
-
import { flowIdOf, isNewFlow } from '../workspace/flows.ts';
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Diffing flows.
|
|
8
|
-
*
|
|
9
|
-
* Node-by-node for the human, because that is how a person thinks about a flow and
|
|
10
|
-
* how the compiler will send it — the granular operations are per node. The
|
|
11
|
-
* whole-tree replace the API also offers is not used (see `IpaasClient.operate`),
|
|
12
|
-
* so there is no reason to diff whole-tree either.
|
|
13
|
-
*
|
|
14
|
-
* Coverage is respected exactly as it is for pipe entities: a workspace whose
|
|
15
|
-
* `flows` group is `unknown` is not diffed at all. Comparing against a group the
|
|
16
|
-
* read path could not see would propose deleting every flow in it.
|
|
17
|
-
*/
|
|
18
|
-
|
|
19
|
-
export type FlowChangeOp =
|
|
20
|
-
| 'flow.create'
|
|
21
|
-
| 'flow.delete'
|
|
22
|
-
| 'flow.rename'
|
|
23
|
-
| 'flow.status'
|
|
24
|
-
| 'node.create'
|
|
25
|
-
| 'node.update'
|
|
26
|
-
| 'node.delete'
|
|
27
|
-
| 'trigger.update';
|
|
28
|
-
|
|
29
|
-
export type FlowChange = {
|
|
30
|
-
op: FlowChangeOp;
|
|
31
|
-
/** Stable key: the flow, plus the node when the change is a node's. */
|
|
32
|
-
key: string;
|
|
33
|
-
label: string;
|
|
34
|
-
flowId: string | null;
|
|
35
|
-
/** The flow's display name, for the label when there is no id yet. */
|
|
36
|
-
flowName: string;
|
|
37
|
-
nodeName?: string;
|
|
38
|
-
before?: FlowNode | null;
|
|
39
|
-
after?: FlowNode | null;
|
|
40
|
-
/** For flow-level changes. */
|
|
41
|
-
beforeFlow?: FlowTree | null;
|
|
42
|
-
afterFlow?: FlowTree | null;
|
|
43
|
-
deltas: Array<{ key: string; before: unknown; after: unknown }>;
|
|
44
|
-
flags: string[];
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
export type FlowChangeSet = {
|
|
48
|
-
changes: FlowChange[];
|
|
49
|
-
skipped: Array<{ reason: string }>;
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Keys that never carry intent and must not produce a change.
|
|
54
|
-
*
|
|
55
|
-
* `sampleData` is generated by testing a step, `lastUpdatedDate` moves on every
|
|
56
|
-
* write, and the server-minted ids move whenever a version is created. Comparing
|
|
57
|
-
* them would make every flow permanently dirty.
|
|
58
|
-
*/
|
|
59
|
-
const VOLATILE_NODE_KEYS = new Set(['sampleData', 'lastUpdatedDate']);
|
|
60
|
-
const VOLATILE_FLOW_KEYS = new Set([
|
|
61
|
-
'created',
|
|
62
|
-
'updated',
|
|
63
|
-
'publishedVersionId',
|
|
64
|
-
'operationStatus',
|
|
65
|
-
'timeSavedPerRun',
|
|
66
|
-
'createdBy',
|
|
67
|
-
'updatedBy',
|
|
68
|
-
'ownerId',
|
|
69
|
-
'externalId',
|
|
70
|
-
'id',
|
|
71
|
-
'flowId',
|
|
72
|
-
'projectId',
|
|
73
|
-
'templateId',
|
|
74
|
-
'backupFiles',
|
|
75
|
-
/**
|
|
76
|
-
* Derived by the API from the inline `{{connections[...]}}` references rather
|
|
77
|
-
* than supplied, so it is an echo of the nodes and never an intent of its own.
|
|
78
|
-
*/
|
|
79
|
-
'connectionIds',
|
|
80
|
-
'state',
|
|
81
|
-
]);
|
|
82
|
-
|
|
83
|
-
const comparableNode = (n: FlowNode): Record<string, unknown> => {
|
|
84
|
-
const out: Record<string, unknown> = {};
|
|
85
|
-
for (const [k, v] of Object.entries(n)) {
|
|
86
|
-
if (VOLATILE_NODE_KEYS.has(k)) continue;
|
|
87
|
-
if (k.startsWith('_')) continue; // link metadata is compared separately, as structure
|
|
88
|
-
if (k === 'settings' && v && typeof v === 'object') {
|
|
89
|
-
const s: Record<string, unknown> = {};
|
|
90
|
-
for (const [sk, sv] of Object.entries(v as Record<string, unknown>)) {
|
|
91
|
-
if (VOLATILE_NODE_KEYS.has(sk)) continue;
|
|
92
|
-
s[sk] = sv;
|
|
93
|
-
}
|
|
94
|
-
out[k] = s;
|
|
95
|
-
continue;
|
|
96
|
-
}
|
|
97
|
-
out[k] = v;
|
|
98
|
-
}
|
|
99
|
-
return out;
|
|
100
|
-
};
|
|
101
|
-
|
|
102
|
-
const nodeDeltas = (before: FlowNode, after: FlowNode) => {
|
|
103
|
-
const a = comparableNode(before);
|
|
104
|
-
const b = comparableNode(after);
|
|
105
|
-
const keys = [...new Set([...Object.keys(a), ...Object.keys(b)])].sort();
|
|
106
|
-
const out: Array<{ key: string; before: unknown; after: unknown }> = [];
|
|
107
|
-
for (const k of keys) {
|
|
108
|
-
if (canonicalString(a[k]) !== canonicalString(b[k])) out.push({ key: k, before: a[k], after: b[k] });
|
|
109
|
-
}
|
|
110
|
-
// Structure: a node whose place in the chain moved is a change even when its
|
|
111
|
-
// settings are identical.
|
|
112
|
-
for (const link of ['_nextAction', '_firstLoopAction', '_children'] as const) {
|
|
113
|
-
if (canonicalString(before[link]) !== canonicalString(after[link])) {
|
|
114
|
-
out.push({ key: link, before: before[link], after: after[link] });
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
return out;
|
|
118
|
-
};
|
|
119
|
-
|
|
120
|
-
const flowLabel = (tree: FlowTree) => String(tree.version['displayName'] ?? flowIdOf(tree) ?? 'flow');
|
|
121
|
-
|
|
122
|
-
/** Match flows by id, then by display name for one the workspace is creating. */
|
|
123
|
-
const matchFlows = (before: FlowTree[], after: FlowTree[]) => {
|
|
124
|
-
const pairs: Array<[FlowTree, FlowTree]> = [];
|
|
125
|
-
const added: FlowTree[] = [];
|
|
126
|
-
const removed: FlowTree[] = [...before];
|
|
127
|
-
|
|
128
|
-
for (const a of after) {
|
|
129
|
-
const id = flowIdOf(a);
|
|
130
|
-
let hitAt = id ? removed.findIndex((b) => flowIdOf(b) === id) : -1;
|
|
131
|
-
if (hitAt < 0 && isNewFlow(a)) {
|
|
132
|
-
hitAt = removed.findIndex((b) => flowLabel(b) === flowLabel(a));
|
|
133
|
-
}
|
|
134
|
-
if (hitAt >= 0) {
|
|
135
|
-
pairs.push([removed[hitAt] as FlowTree, a]);
|
|
136
|
-
removed.splice(hitAt, 1);
|
|
137
|
-
} else {
|
|
138
|
-
added.push(a);
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
return { pairs, added, removed };
|
|
142
|
-
};
|
|
143
|
-
|
|
144
|
-
export const diffFlows = (before: FlowsSection | null, after: FlowsSection | null): FlowChangeSet => {
|
|
145
|
-
const changes: FlowChange[] = [];
|
|
146
|
-
const skipped: Array<{ reason: string }> = [];
|
|
147
|
-
|
|
148
|
-
if (!before || !after) {
|
|
149
|
-
return { changes, skipped: [{ reason: 'one side has no flows section, so flows are not diffed' }] };
|
|
150
|
-
}
|
|
151
|
-
if (before.meta.coverage !== 'complete' || after.meta.coverage !== 'complete') {
|
|
152
|
-
/**
|
|
153
|
-
* The whole reason coverage exists. A pipe whose iPaaS could not be reached
|
|
154
|
-
* reports zero flows, and diffing that against a workspace that has some would
|
|
155
|
-
* propose deleting every one of them.
|
|
156
|
-
*/
|
|
157
|
-
return {
|
|
158
|
-
changes,
|
|
159
|
-
skipped: [
|
|
160
|
-
{
|
|
161
|
-
reason:
|
|
162
|
-
`flows coverage is ${before.meta.coverage}/${after.meta.coverage}, not complete — ` +
|
|
163
|
-
'not diffed, because absence here is not evidence of absence',
|
|
164
|
-
},
|
|
165
|
-
],
|
|
166
|
-
};
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
const { pairs, added, removed } = matchFlows(before.flows, after.flows);
|
|
170
|
-
|
|
171
|
-
for (const tree of added) {
|
|
172
|
-
changes.push({
|
|
173
|
-
op: 'flow.create',
|
|
174
|
-
key: `flow:new:${flowLabel(tree)}`,
|
|
175
|
-
label: `create flow "${flowLabel(tree)}" (${tree.nodes.length} nodes)`,
|
|
176
|
-
flowId: null,
|
|
177
|
-
flowName: flowLabel(tree),
|
|
178
|
-
afterFlow: tree,
|
|
179
|
-
deltas: [],
|
|
180
|
-
flags: [],
|
|
181
|
-
});
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
for (const tree of removed) {
|
|
185
|
-
changes.push({
|
|
186
|
-
op: 'flow.delete',
|
|
187
|
-
key: `flow:${flowIdOf(tree)}`,
|
|
188
|
-
label: `delete flow "${flowLabel(tree)}"`,
|
|
189
|
-
flowId: flowIdOf(tree),
|
|
190
|
-
flowName: flowLabel(tree),
|
|
191
|
-
beforeFlow: tree,
|
|
192
|
-
deltas: [],
|
|
193
|
-
flags: ['destructive'],
|
|
194
|
-
});
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
for (const [b, a] of pairs) {
|
|
198
|
-
const flowId = flowIdOf(a) ?? flowIdOf(b);
|
|
199
|
-
const name = flowLabel(a);
|
|
200
|
-
|
|
201
|
-
if (flowLabel(b) !== flowLabel(a)) {
|
|
202
|
-
changes.push({
|
|
203
|
-
op: 'flow.rename',
|
|
204
|
-
key: `flow:${flowId}:name`,
|
|
205
|
-
label: `rename flow "${flowLabel(b)}" → "${flowLabel(a)}"`,
|
|
206
|
-
flowId,
|
|
207
|
-
flowName: name,
|
|
208
|
-
deltas: [{ key: 'displayName', before: flowLabel(b), after: flowLabel(a) }],
|
|
209
|
-
flags: [],
|
|
210
|
-
});
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
if (String(b.flow['status'] ?? '') !== String(a.flow['status'] ?? '')) {
|
|
214
|
-
changes.push({
|
|
215
|
-
op: 'flow.status',
|
|
216
|
-
key: `flow:${flowId}:status`,
|
|
217
|
-
label: `flow "${name}" ${b.flow['status']} → ${a.flow['status']}`,
|
|
218
|
-
flowId,
|
|
219
|
-
flowName: name,
|
|
220
|
-
deltas: [{ key: 'status', before: b.flow['status'], after: a.flow['status'] }],
|
|
221
|
-
// Enabling a flow arms it against live traffic. That is never incidental.
|
|
222
|
-
flags: String(a.flow['status']) === 'ENABLED' ? ['arms-flow'] : [],
|
|
223
|
-
});
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
const beforeNodes = new Map(b.nodes.map((n) => [n.name, n]));
|
|
227
|
-
const afterNodes = new Map(a.nodes.map((n) => [n.name, n]));
|
|
228
|
-
|
|
229
|
-
for (const [nodeName, node] of afterNodes) {
|
|
230
|
-
const prior = beforeNodes.get(nodeName);
|
|
231
|
-
if (!prior) {
|
|
232
|
-
changes.push({
|
|
233
|
-
op: 'node.create',
|
|
234
|
-
key: `flow:${flowId}:node:${nodeName}`,
|
|
235
|
-
label: `flow "${name}": add ${nodeName} [${node.type}] ${String(node['displayName'] ?? '')}`.trim(),
|
|
236
|
-
flowId,
|
|
237
|
-
flowName: name,
|
|
238
|
-
nodeName,
|
|
239
|
-
after: node,
|
|
240
|
-
/**
|
|
241
|
-
* The whole intended tree, not just the node: `ADD_ACTION` needs the
|
|
242
|
-
* parent this node hangs from, and that is a property of where `node`
|
|
243
|
-
* sits in the chain — found by scanning every node's `_nextAction` /
|
|
244
|
-
* `_firstLoopAction` / `_children` for `nodeName` (apply/flowops.ts
|
|
245
|
-
* `parentOf`), the same lookup a brand-new flow's build already uses.
|
|
246
|
-
*/
|
|
247
|
-
afterFlow: a,
|
|
248
|
-
deltas: [],
|
|
249
|
-
flags: [],
|
|
250
|
-
});
|
|
251
|
-
continue;
|
|
252
|
-
}
|
|
253
|
-
const deltas = nodeDeltas(prior, node);
|
|
254
|
-
if (!deltas.length) continue;
|
|
255
|
-
const isTrigger = nodeName === 'trigger';
|
|
256
|
-
changes.push({
|
|
257
|
-
op: isTrigger ? 'trigger.update' : 'node.update',
|
|
258
|
-
key: `flow:${flowId}:node:${nodeName}`,
|
|
259
|
-
label: `flow "${name}": ${isTrigger ? 'trigger' : nodeName} — ${deltas.map((d) => d.key).join(', ')}`,
|
|
260
|
-
flowId,
|
|
261
|
-
flowName: name,
|
|
262
|
-
nodeName,
|
|
263
|
-
before: prior,
|
|
264
|
-
after: node,
|
|
265
|
-
deltas,
|
|
266
|
-
flags: [],
|
|
267
|
-
});
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
for (const [nodeName, node] of beforeNodes) {
|
|
271
|
-
if (afterNodes.has(nodeName)) continue;
|
|
272
|
-
changes.push({
|
|
273
|
-
op: 'node.delete',
|
|
274
|
-
key: `flow:${flowId}:node:${nodeName}`,
|
|
275
|
-
label: `flow "${name}": delete ${nodeName} [${node.type}]`,
|
|
276
|
-
flowId,
|
|
277
|
-
flowName: name,
|
|
278
|
-
nodeName,
|
|
279
|
-
before: node,
|
|
280
|
-
deltas: [],
|
|
281
|
-
flags: ['destructive'],
|
|
282
|
-
});
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
return { changes, skipped };
|
|
287
|
-
};
|
|
288
|
-
|
|
289
|
-
export const renderFlowChanges = (cs: FlowChangeSet): string => {
|
|
290
|
-
const lines: string[] = [];
|
|
291
|
-
for (const s of cs.skipped) lines.push(` flows not diffed: ${s.reason}`);
|
|
292
|
-
if (!cs.changes.length) {
|
|
293
|
-
if (!cs.skipped.length) lines.push(' flows: no changes');
|
|
294
|
-
return lines.join('\n');
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
const byFlow = new Map<string, FlowChange[]>();
|
|
298
|
-
for (const c of cs.changes) {
|
|
299
|
-
const k = c.flowId ?? `new:${c.flowName}`;
|
|
300
|
-
byFlow.set(k, [...(byFlow.get(k) ?? []), c]);
|
|
301
|
-
}
|
|
302
|
-
for (const [, group] of byFlow) {
|
|
303
|
-
lines.push('');
|
|
304
|
-
lines.push(` ${group[0]?.flowName ?? 'flow'}`);
|
|
305
|
-
for (const c of group) {
|
|
306
|
-
const mark = c.flags.includes('destructive') ? '-' : c.op.endsWith('create') ? '+' : '~';
|
|
307
|
-
lines.push(` ${mark} ${c.label}`);
|
|
308
|
-
for (const d of c.deltas.slice(0, 6)) {
|
|
309
|
-
const short = (v: unknown) => {
|
|
310
|
-
const s = canonicalString(v);
|
|
311
|
-
return s.length > 90 ? `${s.slice(0, 90)}…` : s;
|
|
312
|
-
};
|
|
313
|
-
lines.push(` ${d.key}: ${short(d.before)} -> ${short(d.after)}`);
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
return lines.join('\n');
|
|
318
|
-
};
|
package/src/diff/render.ts
DELETED
|
@@ -1,101 +0,0 @@
|
|
|
1
|
-
import { bold, dim, green, red, yellow, blue, magenta } from '../util/log.ts';
|
|
2
|
-
import type { Change, ChangeSet, FieldDelta } from './diff.ts';
|
|
3
|
-
|
|
4
|
-
/** Text rendering of a ChangeSet, for the CLI. */
|
|
5
|
-
|
|
6
|
-
const OP_MARK: Record<Change['op'], string> = {
|
|
7
|
-
create: '+',
|
|
8
|
-
delete: '-',
|
|
9
|
-
update: '~',
|
|
10
|
-
reorder: '>',
|
|
11
|
-
archive: 'a',
|
|
12
|
-
unarchive: 'u',
|
|
13
|
-
};
|
|
14
|
-
|
|
15
|
-
const colorFor = (op: Change['op']) =>
|
|
16
|
-
op === 'create' ? green : op === 'delete' ? red : op === 'reorder' ? magenta : yellow;
|
|
17
|
-
|
|
18
|
-
const short = (v: unknown, max = 90): string => {
|
|
19
|
-
if (v === null || v === undefined) return 'null';
|
|
20
|
-
if (typeof v === 'string') return v.length > max ? JSON.stringify(v.slice(0, max) + '…') : JSON.stringify(v);
|
|
21
|
-
const s = JSON.stringify(v);
|
|
22
|
-
return s.length > max ? s.slice(0, max) + '…' : s;
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
const renderDelta = (d: FieldDelta, indent: string): string =>
|
|
26
|
-
`${indent}${dim(d.key)}: ${red(short(d.before))} ${dim('->')} ${green(short(d.after))}`;
|
|
27
|
-
|
|
28
|
-
export const renderChange = (c: Change, opts: { verbose?: boolean } = {}): string[] => {
|
|
29
|
-
const paint = colorFor(c.op);
|
|
30
|
-
const lines: string[] = [];
|
|
31
|
-
const flags = c.flags.length ? ' ' + dim(`[${c.flags.join(' ')}]`) : '';
|
|
32
|
-
lines.push(` ${paint(OP_MARK[c.op])} ${bold(c.entity)} ${c.label}${flags}`);
|
|
33
|
-
|
|
34
|
-
const deltas = opts.verbose ? c.deltas : c.deltas.slice(0, 6);
|
|
35
|
-
for (const d of deltas) lines.push(renderDelta(d, ' '));
|
|
36
|
-
if (c.deltas.length > deltas.length) {
|
|
37
|
-
lines.push(` ${dim(`…and ${c.deltas.length - deltas.length} more field${c.deltas.length - deltas.length === 1 ? '' : 's'}`)}`);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
if (c.op === 'create' && c.after && !c.deltas.length) {
|
|
41
|
-
const keys = Object.keys(c.after).filter((k) => !k.startsWith('_')).slice(0, 8);
|
|
42
|
-
lines.push(` ${dim(keys.map((k) => `${k}=${short(c.after?.[k], 30)}`).join(' '))}`);
|
|
43
|
-
}
|
|
44
|
-
return lines;
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
export const renderChangeSet = (cs: ChangeSet, opts: { verbose?: boolean } = {}): string => {
|
|
48
|
-
const lines: string[] = [];
|
|
49
|
-
const header =
|
|
50
|
-
cs.mode === 'cross-repo'
|
|
51
|
-
? `${cs.from.name} (${cs.from.repoId}) -> ${cs.to.name} (${cs.to.repoId})`
|
|
52
|
-
: `${cs.to.name} (${cs.to.repoId})`;
|
|
53
|
-
lines.push(bold(`${cs.mode} diff — ${header}`));
|
|
54
|
-
lines.push(dim(`baseline read from ${cs.from.source}; compared tree read from ${cs.to.source}`));
|
|
55
|
-
lines.push('');
|
|
56
|
-
|
|
57
|
-
if (!cs.changes.length) {
|
|
58
|
-
lines.push(green(' no changes'));
|
|
59
|
-
} else {
|
|
60
|
-
const byEntity = new Map<string, Change[]>();
|
|
61
|
-
for (const c of cs.changes) {
|
|
62
|
-
const arr = byEntity.get(c.entity) ?? [];
|
|
63
|
-
arr.push(c);
|
|
64
|
-
byEntity.set(c.entity, arr);
|
|
65
|
-
}
|
|
66
|
-
for (const [entity, group] of byEntity) {
|
|
67
|
-
lines.push(blue(`${entity} (${group.length})`));
|
|
68
|
-
for (const c of group) lines.push(...renderChange(c, opts));
|
|
69
|
-
lines.push('');
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
if (cs.skipped.length) {
|
|
74
|
-
lines.push(yellow('not diffed — no read path on one side:'));
|
|
75
|
-
for (const s of cs.skipped) lines.push(` ${s.entity}: ${dim(s.reason)}`);
|
|
76
|
-
lines.push('');
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
const counts = cs.changes.reduce<Record<string, number>>((acc, c) => {
|
|
80
|
-
acc[c.op] = (acc[c.op] ?? 0) + 1;
|
|
81
|
-
return acc;
|
|
82
|
-
}, {});
|
|
83
|
-
const summary = Object.entries(counts)
|
|
84
|
-
.map(([op, n]) => `${n} ${op}`)
|
|
85
|
-
.join(', ');
|
|
86
|
-
lines.push(bold(summary || 'nothing to do'));
|
|
87
|
-
return lines.join('\n');
|
|
88
|
-
};
|
|
89
|
-
|
|
90
|
-
/** Machine-readable form, for `--json` and for the HTML view. */
|
|
91
|
-
export const toJson = (cs: ChangeSet) => ({
|
|
92
|
-
mode: cs.mode,
|
|
93
|
-
from: cs.from,
|
|
94
|
-
to: cs.to,
|
|
95
|
-
summary: cs.changes.reduce<Record<string, number>>((acc, c) => {
|
|
96
|
-
acc[c.op] = (acc[c.op] ?? 0) + 1;
|
|
97
|
-
return acc;
|
|
98
|
-
}, {}),
|
|
99
|
-
skipped: cs.skipped,
|
|
100
|
-
changes: cs.changes,
|
|
101
|
-
});
|
package/src/model/payload.ts
DELETED
|
@@ -1,154 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The snapshot payload: the `repos` row (snake_case DB columns) plus
|
|
3
|
-
* `relations` with twenty flat entity arrays. This shape is the canonical
|
|
4
|
-
* intermediate for BOTH read paths — snapshot download and GraphQL
|
|
5
|
-
* reconstruction — so everything downstream (codec, diff, plan) is written
|
|
6
|
-
* once. PLAN.md §1.3.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
export type Row = Record<string, unknown>;
|
|
10
|
-
|
|
11
|
-
export const ENTITY_NAMES = [
|
|
12
|
-
'phases',
|
|
13
|
-
'phase_team_memberships',
|
|
14
|
-
'phase_jumps',
|
|
15
|
-
'fields',
|
|
16
|
-
'field_conditions',
|
|
17
|
-
'field_condition_actions',
|
|
18
|
-
'automations',
|
|
19
|
-
'conditions',
|
|
20
|
-
'condition_expressions',
|
|
21
|
-
'field_maps',
|
|
22
|
-
'distribute_assignments',
|
|
23
|
-
'response_schemas',
|
|
24
|
-
'repo_preferences',
|
|
25
|
-
'labels',
|
|
26
|
-
'pipe_relations',
|
|
27
|
-
'email_inboxes',
|
|
28
|
-
'webhooks',
|
|
29
|
-
'public_forms',
|
|
30
|
-
'email_templates',
|
|
31
|
-
'visibilities',
|
|
32
|
-
] as const;
|
|
33
|
-
|
|
34
|
-
export type EntityName = (typeof ENTITY_NAMES)[number];
|
|
35
|
-
|
|
36
|
-
export type Relations = Record<EntityName, Row[]>;
|
|
37
|
-
|
|
38
|
-
export type RepoPayload = Row & { id: number; relations: Relations };
|
|
39
|
-
|
|
40
|
-
export const emptyRelations = (): Relations =>
|
|
41
|
-
Object.fromEntries(ENTITY_NAMES.map((n) => [n, [] as Row[]])) as Relations;
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* How much of an entity array a given read actually knows about.
|
|
45
|
-
*
|
|
46
|
-
* This is the safety mechanism a non-snapshot read requires and that neither
|
|
47
|
-
* PLAN.md nor PLAN-ROUTE-B.md has: a reconstructed payload cannot distinguish
|
|
48
|
-
* "this pipe has no webhooks" from "I have no way to read webhooks". Recording
|
|
49
|
-
* `unknown` keeps pack and diff from ever reading absence as deletion
|
|
50
|
-
* (PLAN-ROUTE-B.md §7 — "a dropped entity means the importer deletes it").
|
|
51
|
-
*/
|
|
52
|
-
export type Coverage = 'complete' | 'partial' | 'unknown';
|
|
53
|
-
|
|
54
|
-
export type PayloadEnvelope = {
|
|
55
|
-
/** Where the structure came from. */
|
|
56
|
-
source: 'snapshot' | 'reconstructed';
|
|
57
|
-
/** Snapshot version id, when source === 'snapshot'. */
|
|
58
|
-
versionId?: string | null;
|
|
59
|
-
/** ISO time of the read. */
|
|
60
|
-
readAt: string;
|
|
61
|
-
repoKind: 'pipe' | 'table';
|
|
62
|
-
coverage: Record<EntityName, Coverage>;
|
|
63
|
-
/** Whether the repo row's own columns were all readable. */
|
|
64
|
-
repo_columns?: Coverage;
|
|
65
|
-
/** What the read path could not see, for the docs and the diff header. */
|
|
66
|
-
notes: string[];
|
|
67
|
-
payload: RepoPayload;
|
|
68
|
-
};
|
|
69
|
-
|
|
70
|
-
export const fullCoverage = (c: Coverage): Record<EntityName, Coverage> =>
|
|
71
|
-
Object.fromEntries(ENTITY_NAMES.map((n) => [n, c])) as Record<EntityName, Coverage>;
|
|
72
|
-
|
|
73
|
-
export type Identity = 'uuid' | 'id' | 'owned';
|
|
74
|
-
|
|
75
|
-
export type EntitySpec = {
|
|
76
|
-
name: EntityName;
|
|
77
|
-
/**
|
|
78
|
-
* How an entity is recognised across two reads.
|
|
79
|
-
* `uuid` is present on only 4 of 20 arrays (PLAN.md §1.3); `owned` entities
|
|
80
|
-
* have no identity of their own and are diffed as a replace-set under their
|
|
81
|
-
* owner, exactly as ROUTE-B-V2.md §4 proposes the importer treat them.
|
|
82
|
-
*/
|
|
83
|
-
identity: Identity;
|
|
84
|
-
/** For `owned`: the columns naming the owner, in precedence order. */
|
|
85
|
-
ownerKeys?: string[];
|
|
86
|
-
/** Fallback natural key, used when matching across two different repos. */
|
|
87
|
-
naturalKey?: string[];
|
|
88
|
-
/** No public write path exists — PLAN.md §2. */
|
|
89
|
-
readonly?: boolean;
|
|
90
|
-
/** Written through the internal API rather than public GraphQL. */
|
|
91
|
-
internal?: boolean;
|
|
92
|
-
/** Folded into the owner's file by the codec rather than getting its own. */
|
|
93
|
-
foldedInto?: EntityName;
|
|
94
|
-
};
|
|
95
|
-
|
|
96
|
-
export const ENTITIES: Record<EntityName, EntitySpec> = {
|
|
97
|
-
phases: { name: 'phases', identity: 'uuid', naturalKey: ['name'] },
|
|
98
|
-
fields: { name: 'fields', identity: 'uuid', naturalKey: ['phase_id', 'label'], foldedInto: 'phases' },
|
|
99
|
-
automations: { name: 'automations', identity: 'uuid', naturalKey: ['name'] },
|
|
100
|
-
labels: { name: 'labels', identity: 'uuid', naturalKey: ['name'] },
|
|
101
|
-
|
|
102
|
-
field_conditions: { name: 'field_conditions', identity: 'id', naturalKey: ['phase_id', 'name'] },
|
|
103
|
-
webhooks: { name: 'webhooks', identity: 'id', naturalKey: ['name', 'url'] },
|
|
104
|
-
pipe_relations: { name: 'pipe_relations', identity: 'id', naturalKey: ['parent_id', 'child_id', 'name'] },
|
|
105
|
-
|
|
106
|
-
conditions: { name: 'conditions', identity: 'owned', ownerKeys: ['source_type', 'source_id'] },
|
|
107
|
-
condition_expressions: { name: 'condition_expressions', identity: 'owned', ownerKeys: ['condition_id'] },
|
|
108
|
-
field_maps: { name: 'field_maps', identity: 'owned', ownerKeys: ['owner_type', 'owner_id'] },
|
|
109
|
-
field_condition_actions: {
|
|
110
|
-
name: 'field_condition_actions',
|
|
111
|
-
identity: 'owned',
|
|
112
|
-
ownerKeys: ['field_condition_id'],
|
|
113
|
-
foldedInto: 'field_conditions',
|
|
114
|
-
},
|
|
115
|
-
phase_jumps: { name: 'phase_jumps', identity: 'owned', ownerKeys: ['source_phase_id'], internal: true },
|
|
116
|
-
phase_team_memberships: {
|
|
117
|
-
name: 'phase_team_memberships',
|
|
118
|
-
identity: 'owned',
|
|
119
|
-
ownerKeys: ['phase_id'],
|
|
120
|
-
foldedInto: 'phases',
|
|
121
|
-
},
|
|
122
|
-
distribute_assignments: {
|
|
123
|
-
name: 'distribute_assignments',
|
|
124
|
-
identity: 'owned',
|
|
125
|
-
ownerKeys: ['automation_id'],
|
|
126
|
-
foldedInto: 'automations',
|
|
127
|
-
},
|
|
128
|
-
response_schemas: {
|
|
129
|
-
name: 'response_schemas',
|
|
130
|
-
identity: 'owned',
|
|
131
|
-
ownerKeys: ['automation_id'],
|
|
132
|
-
foldedInto: 'automations',
|
|
133
|
-
},
|
|
134
|
-
|
|
135
|
-
public_forms: { name: 'public_forms', identity: 'owned', ownerKeys: ['repo_id'] },
|
|
136
|
-
repo_preferences: { name: 'repo_preferences', identity: 'owned', ownerKeys: ['repo_id'] },
|
|
137
|
-
|
|
138
|
-
visibilities: { name: 'visibilities', identity: 'owned', ownerKeys: ['resource_type', 'resource_id'], readonly: true },
|
|
139
|
-
email_templates: { name: 'email_templates', identity: 'id', readonly: true },
|
|
140
|
-
email_inboxes: { name: 'email_inboxes', identity: 'id', readonly: true },
|
|
141
|
-
};
|
|
142
|
-
|
|
143
|
-
export const READONLY_ENTITIES = ENTITY_NAMES.filter((n) => ENTITIES[n].readonly);
|
|
144
|
-
|
|
145
|
-
export const rel = (p: RepoPayload, n: EntityName): Row[] => p.relations?.[n] ?? [];
|
|
146
|
-
|
|
147
|
-
export const numId = (r: Row): number | null => {
|
|
148
|
-
const v = r['id'];
|
|
149
|
-
if (typeof v === 'number') return v;
|
|
150
|
-
if (typeof v === 'string' && /^\d+$/.test(v)) return Number(v);
|
|
151
|
-
return null;
|
|
152
|
-
};
|
|
153
|
-
|
|
154
|
-
export const strId = (r: Row): string => String(r['id'] ?? '');
|
package/src/model/tree.ts
DELETED
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
import type { Coverage, EntityName, Row } from './payload.ts';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* The workspace tree — what Claude and the FDE actually edit. PLAN.md §4.
|
|
5
|
-
*
|
|
6
|
-
* Folding is *movement only*: a folded row is the payload row, unchanged, moved
|
|
7
|
-
* under its owner. Nothing is renamed or reshaped, which is what makes
|
|
8
|
-
* `pack(unpack(P))` equal to `P` by construction rather than by care.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
export type ConditionNode = Row & {
|
|
12
|
-
/** condition_expressions rows belonging to this condition. */
|
|
13
|
-
expressions: Row[];
|
|
14
|
-
};
|
|
15
|
-
|
|
16
|
-
export type PhaseNode = Row & {
|
|
17
|
-
/** fields rows whose phase_id is this phase, ordered by index. */
|
|
18
|
-
fields: Row[];
|
|
19
|
-
/**
|
|
20
|
-
* phase_jumps targets for this phase, as a set. The internal settings
|
|
21
|
-
* endpoint is set-replacing (PLAN.md §2.1), so a set is the honest shape and
|
|
22
|
-
* there is no add/remove diffing to get wrong.
|
|
23
|
-
*/
|
|
24
|
-
jump_target_ids: Array<number | string>;
|
|
25
|
-
/** phase_team_memberships, flattened to the user ids phaseSettings takes. */
|
|
26
|
-
team_member_ids: Array<number | string>;
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
export type AutomationNode = Row & {
|
|
30
|
-
condition?: ConditionNode | null;
|
|
31
|
-
field_maps: Row[];
|
|
32
|
-
distribute_assignments: Row[];
|
|
33
|
-
response_schemas: Row[];
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
export type FieldConditionNode = Row & {
|
|
37
|
-
actions: Row[];
|
|
38
|
-
condition?: ConditionNode | null;
|
|
39
|
-
};
|
|
40
|
-
|
|
41
|
-
export type TreeMeta = {
|
|
42
|
-
source: 'snapshot' | 'reconstructed';
|
|
43
|
-
versionId?: string | null;
|
|
44
|
-
readAt: string;
|
|
45
|
-
repoKind: 'pipe' | 'table';
|
|
46
|
-
coverage: Record<EntityName, Coverage>;
|
|
47
|
-
/**
|
|
48
|
-
* Whether the repo row itself was read in full. The public API has no
|
|
49
|
-
* `color_uuid`, `authorization` or `draft`, so a reconstructed root is
|
|
50
|
-
* partial — and the diff must not read those absences as "set to null".
|
|
51
|
-
*/
|
|
52
|
-
repo_columns: Coverage;
|
|
53
|
-
notes: string[];
|
|
54
|
-
};
|
|
55
|
-
|
|
56
|
-
export type Tree = {
|
|
57
|
-
meta: TreeMeta;
|
|
58
|
-
/** The `repos` row, volatile columns stripped. */
|
|
59
|
-
repo: Row;
|
|
60
|
-
phases: PhaseNode[];
|
|
61
|
-
automations: AutomationNode[];
|
|
62
|
-
field_conditions: FieldConditionNode[];
|
|
63
|
-
labels: Row[];
|
|
64
|
-
webhooks: Row[];
|
|
65
|
-
pipe_relations: Row[];
|
|
66
|
-
public_form: Row | null;
|
|
67
|
-
preferences: Row | null;
|
|
68
|
-
/** PLAN.md §4: first-class, so the planner can reject edits by path alone. */
|
|
69
|
-
readonly: {
|
|
70
|
-
email_templates: Row[];
|
|
71
|
-
email_inboxes: Row[];
|
|
72
|
-
visibilities: Row[];
|
|
73
|
-
/**
|
|
74
|
-
* `field_maps` owned by a **field**, not an automation — a connector
|
|
75
|
-
* field's own "copy this value from the connected card" configuration.
|
|
76
|
-
* `unpack`/`pack` only ever modelled the automation-owned half of this
|
|
77
|
-
* entity (PLAN.md, ROUTE-B-V2.md both describe it that way), so these rows
|
|
78
|
-
* were silently dropped on every pull until the round-trip check caught it.
|
|
79
|
-
* `owner_type`/`owner_id` are kept, unlike the automation-nested form,
|
|
80
|
-
* because this is a flat bucket spanning however many fields own one.
|
|
81
|
-
*/
|
|
82
|
-
field_maps: Row[];
|
|
83
|
-
};
|
|
84
|
-
};
|
|
85
|
-
|
|
86
|
-
export const repoId = (t: Tree): number => Number(t.repo['id']);
|
|
87
|
-
export const repoName = (t: Tree): string => String(t.repo['name'] ?? `repo ${repoId(t)}`);
|
|
88
|
-
|
|
89
|
-
/** The start form is a phase (id/index 0). PLAN.md §4 — deleting it is catastrophic. */
|
|
90
|
-
export const startFormPhase = (t: Tree): PhaseNode | undefined =>
|
|
91
|
-
t.phases.find((p) => Number(p['index']) === 0) ?? t.phases[0];
|
|
92
|
-
|
|
93
|
-
export const allFields = (t: Tree): Row[] => t.phases.flatMap((p) => p.fields);
|
|
94
|
-
|
|
95
|
-
export const fieldById = (t: Tree, id: string | number): Row | undefined =>
|
|
96
|
-
allFields(t).find((f) => String(f['id']) === String(id));
|
|
97
|
-
|
|
98
|
-
export const phaseById = (t: Tree, id: string | number): PhaseNode | undefined =>
|
|
99
|
-
t.phases.find((p) => String(p['id']) === String(id));
|