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