@ansible-flow/core 0.85.0 → 0.86.0

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 (31) hide show
  1. package/README.md +1 -1
  2. package/dist/ai/handlers.js +8 -8
  3. package/dist/ai/scenarios.js +3 -3
  4. package/dist/awx/import.js +3 -3
  5. package/dist/chunks/{chunk-QMIXDOU6.js → chunk-5XGAGNH5.js} +5 -5
  6. package/dist/chunks/{chunk-22D2NC2L.js → chunk-DHU774ES.js} +5 -5
  7. package/dist/chunks/{chunk-6M3OG3E4.js → chunk-H7Z2FBRD.js} +4 -4
  8. package/dist/chunks/{chunk-V2SPY2EI.js → chunk-NT62IE3Y.js} +2 -2
  9. package/dist/chunks/{chunk-2HMLC2AO.js → chunk-TPRZZHXA.js} +3 -3
  10. package/dist/flow/merge.d.ts +27 -0
  11. package/dist/flow/merge.js +422 -0
  12. package/dist/flow/merge.js.map +7 -0
  13. package/dist/flow/mergeText.d.ts +3 -0
  14. package/dist/flow/mergeText.js +71 -0
  15. package/dist/flow/mergeText.js.map +7 -0
  16. package/dist/flow/valueText.d.ts +6 -0
  17. package/dist/flow/valueText.js +56 -0
  18. package/dist/flow/valueText.js.map +7 -0
  19. package/dist/import/outline.js +4 -4
  20. package/dist/import/playbook.js +2 -2
  21. package/dist/import/source.js +2 -2
  22. package/dist/lens/index.js +10 -10
  23. package/dist/messageCheck.js +2 -0
  24. package/dist/messageCheck.js.map +2 -2
  25. package/dist/protocol.d.ts +28 -0
  26. package/package.json +1 -1
  27. /package/dist/chunks/{chunk-QMIXDOU6.js.map → chunk-5XGAGNH5.js.map} +0 -0
  28. /package/dist/chunks/{chunk-22D2NC2L.js.map → chunk-DHU774ES.js.map} +0 -0
  29. /package/dist/chunks/{chunk-6M3OG3E4.js.map → chunk-H7Z2FBRD.js.map} +0 -0
  30. /package/dist/chunks/{chunk-V2SPY2EI.js.map → chunk-NT62IE3Y.js.map} +0 -0
  31. /package/dist/chunks/{chunk-2HMLC2AO.js.map → chunk-TPRZZHXA.js.map} +0 -0
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Ansible Flow Designer core: the flow model, commands, validation and playbook generation, for any JavaScript host.
4
4
 
5
- Built from [Ansible Flow Designer](https://github.com/ChistokhinSV/ansible-flow-designer) 0.85.0
5
+ Built from [Ansible Flow Designer](https://github.com/ChistokhinSV/ansible-flow-designer) 0.86.0
6
6
  and released with it. Modules are imported by their path in the source tree, for example
7
7
  `@ansible-flow/core/ai/contributions`.
8
8
 
@@ -2,10 +2,10 @@ import {
2
2
  generatedDirOf,
3
3
  paged,
4
4
  sourceOutline
5
- } from "../chunks/chunk-2HMLC2AO.js";
5
+ } from "../chunks/chunk-TPRZZHXA.js";
6
6
  import {
7
7
  readPlaybook
8
- } from "../chunks/chunk-QMIXDOU6.js";
8
+ } from "../chunks/chunk-5XGAGNH5.js";
9
9
  import {
10
10
  newFlowText
11
11
  } from "../chunks/chunk-CFNYFH2A.js";
@@ -32,14 +32,14 @@ import {
32
32
  scenarioProblemLines,
33
33
  scenariosLine,
34
34
  scenariosTool
35
- } from "../chunks/chunk-22D2NC2L.js";
35
+ } from "../chunks/chunk-DHU774ES.js";
36
+ import "../chunks/chunk-FTJ6VOQL.js";
37
+ import "../chunks/chunk-2PSB652B.js";
36
38
  import {
37
39
  checkLive
38
40
  } from "../chunks/chunk-CM23ODBB.js";
39
41
  import "../chunks/chunk-7GLSXIZ3.js";
40
42
  import "../chunks/chunk-23ISDYUS.js";
41
- import "../chunks/chunk-FTJ6VOQL.js";
42
- import "../chunks/chunk-2PSB652B.js";
43
43
  import "../chunks/chunk-TH6ICGA5.js";
44
44
  import "../chunks/chunk-KOG6OURH.js";
45
45
  import "../chunks/chunk-CXVJWFGT.js";
@@ -50,13 +50,13 @@ import "../chunks/chunk-ZAYNYPCX.js";
50
50
  import "../chunks/chunk-56VTJ6YF.js";
51
51
  import {
52
52
  importWorkflow
53
- } from "../chunks/chunk-V2SPY2EI.js";
53
+ } from "../chunks/chunk-NT62IE3Y.js";
54
54
  import {
55
55
  importPlaybook
56
- } from "../chunks/chunk-6M3OG3E4.js";
56
+ } from "../chunks/chunk-H7Z2FBRD.js";
57
+ import "../chunks/chunk-LNFMYJUJ.js";
57
58
  import "../chunks/chunk-5CD65KH3.js";
58
59
  import "../chunks/chunk-6L43PFIN.js";
59
- import "../chunks/chunk-LNFMYJUJ.js";
60
60
  import "../chunks/chunk-PDUQ3KMW.js";
61
61
  import {
62
62
  checksum
@@ -3,12 +3,12 @@ import {
3
3
  scenarioProblemLines,
4
4
  scenariosLine,
5
5
  scenariosTool
6
- } from "../chunks/chunk-22D2NC2L.js";
6
+ } from "../chunks/chunk-DHU774ES.js";
7
+ import "../chunks/chunk-FTJ6VOQL.js";
8
+ import "../chunks/chunk-2PSB652B.js";
7
9
  import "../chunks/chunk-CM23ODBB.js";
8
10
  import "../chunks/chunk-7GLSXIZ3.js";
9
11
  import "../chunks/chunk-23ISDYUS.js";
10
- import "../chunks/chunk-FTJ6VOQL.js";
11
- import "../chunks/chunk-2PSB652B.js";
12
12
  import "../chunks/chunk-TH6ICGA5.js";
13
13
  import "../chunks/chunk-KOG6OURH.js";
14
14
  import "../chunks/chunk-CXVJWFGT.js";
@@ -1,11 +1,11 @@
1
1
  import {
2
2
  expandable,
3
3
  importWorkflow
4
- } from "../chunks/chunk-V2SPY2EI.js";
5
- import "../chunks/chunk-6M3OG3E4.js";
4
+ } from "../chunks/chunk-NT62IE3Y.js";
5
+ import "../chunks/chunk-H7Z2FBRD.js";
6
+ import "../chunks/chunk-LNFMYJUJ.js";
6
7
  import "../chunks/chunk-5CD65KH3.js";
7
8
  import "../chunks/chunk-6L43PFIN.js";
8
- import "../chunks/chunk-LNFMYJUJ.js";
9
9
  import "../chunks/chunk-PDUQ3KMW.js";
10
10
  import "../chunks/chunk-BAZIS67B.js";
11
11
  import "../chunks/chunk-TC7FOG4T.js";
@@ -1,3 +1,7 @@
1
+ import {
2
+ builtinFqcn,
3
+ shortName
4
+ } from "./chunk-LNFMYJUJ.js";
1
5
  import {
2
6
  basename,
3
7
  dirname,
@@ -12,10 +16,6 @@ import {
12
16
  parseConfig,
13
17
  toValue
14
18
  } from "./chunk-6L43PFIN.js";
15
- import {
16
- builtinFqcn,
17
- shortName
18
- } from "./chunk-LNFMYJUJ.js";
19
19
  import {
20
20
  isAllowedKeyword
21
21
  } from "./chunk-OEQXGULI.js";
@@ -833,4 +833,4 @@ export {
833
833
  readPlaybook,
834
834
  scanFolder
835
835
  };
836
- //# sourceMappingURL=chunk-QMIXDOU6.js.map
836
+ //# sourceMappingURL=chunk-5XGAGNH5.js.map
@@ -1,7 +1,3 @@
1
- import {
2
- checkLive,
3
- simulatedEnds
4
- } from "./chunk-CM23ODBB.js";
5
1
  import {
6
2
  endWords,
7
3
  scenarioMisses
@@ -10,6 +6,10 @@ import {
10
6
  ScenarioEditError,
11
7
  planScenarioEdit
12
8
  } from "./chunk-2PSB652B.js";
9
+ import {
10
+ checkLive,
11
+ simulatedEnds
12
+ } from "./chunk-CM23ODBB.js";
13
13
  import {
14
14
  blocking,
15
15
  checkScenarios,
@@ -218,4 +218,4 @@ export {
218
218
  scenarioProblemLines,
219
219
  scenariosTool
220
220
  };
221
- //# sourceMappingURL=chunk-22D2NC2L.js.map
221
+ //# sourceMappingURL=chunk-DHU774ES.js.map
@@ -1,3 +1,6 @@
1
+ import {
2
+ shortName
3
+ } from "./chunk-LNFMYJUJ.js";
1
4
  import {
2
5
  fromRoot,
3
6
  localize
@@ -6,9 +9,6 @@ import {
6
9
  basename,
7
10
  dirname
8
11
  } from "./chunk-6L43PFIN.js";
9
- import {
10
- shortName
11
- } from "./chunk-LNFMYJUJ.js";
12
12
  import {
13
13
  LevelBuilder,
14
14
  importedFlow
@@ -989,4 +989,4 @@ export {
989
989
  flatPlays,
990
990
  importPlaybook
991
991
  };
992
- //# sourceMappingURL=chunk-6M3OG3E4.js.map
992
+ //# sourceMappingURL=chunk-H7Z2FBRD.js.map
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  flatPlays,
3
3
  importPlaybook
4
- } from "./chunk-6M3OG3E4.js";
4
+ } from "./chunk-H7Z2FBRD.js";
5
5
  import {
6
6
  LevelBuilder,
7
7
  importedFlow
@@ -405,4 +405,4 @@ export {
405
405
  expandable,
406
406
  importWorkflow
407
407
  };
408
- //# sourceMappingURL=chunk-V2SPY2EI.js.map
408
+ //# sourceMappingURL=chunk-NT62IE3Y.js.map
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  readPlaybook,
3
3
  scanFolder
4
- } from "./chunk-QMIXDOU6.js";
4
+ } from "./chunk-5XGAGNH5.js";
5
5
  import {
6
6
  exportTemplates,
7
7
  jobTemplateFromExport,
@@ -10,7 +10,7 @@ import {
10
10
  } from "./chunk-IYVI5MSS.js";
11
11
  import {
12
12
  importPlaybook
13
- } from "./chunk-6M3OG3E4.js";
13
+ } from "./chunk-H7Z2FBRD.js";
14
14
  import {
15
15
  fromRoot,
16
16
  localize
@@ -455,4 +455,4 @@ export {
455
455
  paged,
456
456
  sourceOutline
457
457
  };
458
- //# sourceMappingURL=chunk-2HMLC2AO.js.map
458
+ //# sourceMappingURL=chunk-TPRZZHXA.js.map
@@ -0,0 +1,27 @@
1
+ import type { FlowDocument, Ulid } from './types';
2
+ export type MergeSide = 'ours' | 'theirs';
3
+ export interface MergeConflict {
4
+ /** Stable across merges of the same versions: choices are given by it. */
5
+ id: string;
6
+ kind: 'value' | 'removed' | 'wire' | 'format';
7
+ /** Where and what, in words: "Say hello: settings.timeout". */
8
+ label: string;
9
+ node?: Ulid;
10
+ annotation?: Ulid;
11
+ edge?: Ulid;
12
+ /** The field, dotted (`settings.timeout`); absent for a whole step, wire or the format. */
13
+ field?: string;
14
+ base?: unknown;
15
+ ours?: unknown;
16
+ theirs?: unknown;
17
+ }
18
+ export interface FlowMerge {
19
+ /** The merged flow, each conflict as its choice says (ours until chosen). */
20
+ doc: FlowDocument;
21
+ conflicts: MergeConflict[];
22
+ }
23
+ /**
24
+ * Merges what `ours` and `theirs` changed since `base`. `choices` settle conflicts by id (ours
25
+ * when not given). Flows without IDs are matched by the IDs their keys give (Complete IDs).
26
+ */
27
+ export declare function mergeFlows(baseDoc: FlowDocument, oursDoc: FlowDocument, theirsDoc: FlowDocument, choices?: Record<string, MergeSide>): FlowMerge;
@@ -0,0 +1,422 @@
1
+ import {
2
+ canonical
3
+ } from "../chunks/chunk-TG3C3GIL.js";
4
+ import {
5
+ withIds
6
+ } from "../chunks/chunk-XRGZNPPO.js";
7
+ import "../chunks/chunk-VG3RKETK.js";
8
+ import "../chunks/chunk-TS4N2EIO.js";
9
+ import "../chunks/chunk-BCX7P6SV.js";
10
+ import "../chunks/chunk-3Z2U3LN7.js";
11
+ import "../chunks/chunk-GHG3VINX.js";
12
+ import "../chunks/chunk-5TUAMVIV.js";
13
+ import "../chunks/chunk-45CE2RUH.js";
14
+ import "../chunks/chunk-E2YZJWO7.js";
15
+ import "../chunks/chunk-ZP5LQ4XC.js";
16
+ import {
17
+ ROOT
18
+ } from "../chunks/chunk-2OD7UZ7V.js";
19
+ import "../chunks/chunk-AGWQ3ANE.js";
20
+
21
+ // src/core/flow/merge.ts
22
+ function flatten(doc) {
23
+ const flat = {
24
+ doc,
25
+ nodes: /* @__PURE__ */ new Map(),
26
+ edges: /* @__PURE__ */ new Map(),
27
+ annotations: /* @__PURE__ */ new Map(),
28
+ rootKeys: ["annotations", "layout", "decisions"].filter((k) => k in doc)
29
+ };
30
+ const visit = (level, nodes, edges, annotations, layout) => {
31
+ for (const node of nodes ?? []) {
32
+ if (!node?.id) continue;
33
+ const { subflow, ...own } = node;
34
+ flat.nodes.set(node.id, {
35
+ level,
36
+ own,
37
+ ...subflow ? {
38
+ body: {
39
+ ...subflow.boundary ? { boundary: subflow.boundary } : {},
40
+ keys: Object.keys(subflow)
41
+ }
42
+ } : {},
43
+ ...layout?.[node.id] ? { position: layout[node.id] } : {}
44
+ });
45
+ if (subflow)
46
+ visit(node.id, subflow.nodes, subflow.edges, subflow.annotations, subflow.layout);
47
+ }
48
+ for (const edge of edges ?? []) if (edge?.id) flat.edges.set(edge.id, { level, edge });
49
+ for (const a of annotations ?? [])
50
+ if (a?.id) flat.annotations.set(a.id, { level, annotation: a });
51
+ };
52
+ visit(ROOT, doc.nodes, doc.edges, doc.annotations, doc.layout);
53
+ return flat;
54
+ }
55
+ var same = (a, b) => JSON.stringify(canonical(a)) === JSON.stringify(canonical(b));
56
+ var isRecord = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
57
+ var Merger = class {
58
+ constructor(choices) {
59
+ this.choices = choices;
60
+ }
61
+ choices;
62
+ conflicts = [];
63
+ pick(id) {
64
+ return this.choices[id] ?? "ours";
65
+ }
66
+ conflict(c) {
67
+ this.conflicts.push(c);
68
+ return this.pick(c.id);
69
+ }
70
+ /**
71
+ * One value, three ways. Records are merged key by key `depth` levels down; a value both sides
72
+ * changed differently is a conflict named by `id` (`<owner>:<field>`).
73
+ */
74
+ value(base, ours, theirs, at, depth) {
75
+ if (same(ours, theirs)) return ours;
76
+ if (same(base, ours)) return theirs;
77
+ if (same(base, theirs)) return ours;
78
+ if (depth > 0 && isRecord(ours) && isRecord(theirs) && (base === void 0 || isRecord(base))) {
79
+ const out = {};
80
+ for (const key of /* @__PURE__ */ new Set([...Object.keys(ours), ...Object.keys(theirs)])) {
81
+ const v = this.value(
82
+ base?.[key],
83
+ ours[key],
84
+ theirs[key],
85
+ {
86
+ ...at,
87
+ id: `${at.id}.${key}`,
88
+ field: at.field ? `${at.field}.${key}` : key
89
+ },
90
+ depth - 1
91
+ );
92
+ if (v !== void 0) out[key] = v;
93
+ }
94
+ return out;
95
+ }
96
+ const side = this.conflict({
97
+ ...at.owner,
98
+ id: at.id,
99
+ kind: "value",
100
+ label: at.field ? `${at.label}: ${at.field}` : at.label,
101
+ ...at.field ? { field: at.field } : {},
102
+ base,
103
+ ours,
104
+ theirs
105
+ });
106
+ return side === "ours" ? ours : theirs;
107
+ }
108
+ /** The fields of a record, each merged on its own (`depth` levels down inside each). */
109
+ fields(base, ours, theirs, at, depth) {
110
+ const out = {};
111
+ for (const key of /* @__PURE__ */ new Set([...Object.keys(ours), ...Object.keys(theirs)])) {
112
+ const v = this.value(
113
+ base?.[key],
114
+ ours[key],
115
+ theirs[key],
116
+ { ...at, id: `${at.id}:${key}`, field: key },
117
+ depth
118
+ );
119
+ if (v !== void 0) out[key] = v;
120
+ }
121
+ return out;
122
+ }
123
+ };
124
+ function quiet(base, ours, theirs) {
125
+ return same(base, ours) ? theirs : ours;
126
+ }
127
+ function mergeFlows(baseDoc, oursDoc, theirsDoc, choices = {}) {
128
+ const m = new Merger(choices);
129
+ if (oursDoc.schema_version !== theirsDoc.schema_version) {
130
+ const side = m.conflict({
131
+ id: "format",
132
+ kind: "format",
133
+ label: `The flow's format: ${oursDoc.schema_version} here, ${theirsDoc.schema_version} in theirs (upgrade both first)`,
134
+ ours: oursDoc.schema_version,
135
+ theirs: theirsDoc.schema_version
136
+ });
137
+ return { doc: side === "ours" ? oursDoc : theirsDoc, conflicts: m.conflicts };
138
+ }
139
+ const base = flatten(withIds(baseDoc));
140
+ const ours = flatten(withIds(oursDoc));
141
+ const theirs = flatten(withIds(theirsDoc));
142
+ const titleOf = (id) => (ours.nodes.get(id) ?? theirs.nodes.get(id) ?? base.nodes.get(id))?.own.title ?? id;
143
+ const touched = (side, id, seen = /* @__PURE__ */ new Set()) => {
144
+ if (seen.has(id)) return false;
145
+ seen.add(id);
146
+ const now = side.nodes.get(id);
147
+ const was = base.nodes.get(id);
148
+ if (!now) return false;
149
+ if (!was || !same(now.own, was.own) || now.level !== was.level || !same(now.body, was.body))
150
+ return true;
151
+ for (const [eid, e] of side.edges)
152
+ if ((e.edge.from.node === id || e.edge.to.node === id) && !same(e, base.edges.get(eid)))
153
+ return true;
154
+ for (const [aid, a] of side.annotations)
155
+ if ((a.annotation.on === id || a.level === id) && !same(a, base.annotations.get(aid)))
156
+ return true;
157
+ for (const [cid, c] of side.nodes) if (c.level === id && touched(side, cid, seen)) return true;
158
+ return false;
159
+ };
160
+ const order = (key) => [
161
+ .../* @__PURE__ */ new Set([...ours[key].keys(), ...theirs[key].keys(), ...base[key].keys()])
162
+ ];
163
+ const nodes = /* @__PURE__ */ new Map();
164
+ for (const id of order("nodes")) {
165
+ const b = base.nodes.get(id);
166
+ const o = ours.nodes.get(id);
167
+ const t = theirs.nodes.get(id);
168
+ if (!o && !t) continue;
169
+ if (b && (!o || !t)) {
170
+ const kept = o ?? t;
171
+ if (!touched(o ? ours : theirs, id)) continue;
172
+ const side = m.conflict({
173
+ id: `node:${id}`,
174
+ kind: "removed",
175
+ node: id,
176
+ label: `${titleOf(id)}: removed ${o ? "in theirs" : "here"}, changed ${o ? "here" : "in theirs"}`,
177
+ ours: o ? "changed" : "removed",
178
+ theirs: o ? "removed" : "changed"
179
+ });
180
+ if (side === "ours" === !!o) nodes.set(id, kept);
181
+ continue;
182
+ }
183
+ if (!o || !t) {
184
+ nodes.set(id, o ?? t);
185
+ continue;
186
+ }
187
+ const owner = { node: id };
188
+ const label = titleOf(id);
189
+ const own = m.fields(
190
+ b?.own,
191
+ o.own,
192
+ t.own,
193
+ { id: `node:${id}`, label, owner },
194
+ 3
195
+ );
196
+ const level2 = m.value(
197
+ b?.level,
198
+ o.level,
199
+ t.level,
200
+ { id: `node:${id}:level`, label, field: "level (moved into another subflow)", owner },
201
+ 0
202
+ );
203
+ let body;
204
+ const hasBody = m.value(
205
+ !!b?.body,
206
+ !!o.body,
207
+ !!t.body,
208
+ { id: `node:${id}:subflow`, label, field: "subflow", owner },
209
+ 0
210
+ );
211
+ if (hasBody) {
212
+ const boundary = m.value(
213
+ b?.body?.boundary,
214
+ o.body?.boundary,
215
+ t.body?.boundary,
216
+ { id: `node:${id}:boundary`, label, field: "boundary", owner },
217
+ 2
218
+ );
219
+ body = {
220
+ ...boundary ? { boundary } : {},
221
+ keys: [.../* @__PURE__ */ new Set([...o.body?.keys ?? [], ...t.body?.keys ?? []])]
222
+ };
223
+ }
224
+ const position = quiet(b?.position, o.position, t.position);
225
+ nodes.set(id, {
226
+ level: level2,
227
+ own,
228
+ ...body ? { body } : {},
229
+ ...position ? { position } : {}
230
+ });
231
+ }
232
+ for (let again = true; again; ) {
233
+ again = false;
234
+ for (const [id, n] of nodes)
235
+ if (n.level !== ROOT && !nodes.get(n.level)?.body) {
236
+ nodes.delete(id);
237
+ again = true;
238
+ }
239
+ }
240
+ const edges = /* @__PURE__ */ new Map();
241
+ for (const id of order("edges")) {
242
+ const b = base.edges.get(id);
243
+ const o = ours.edges.get(id);
244
+ const t = theirs.edges.get(id);
245
+ if (!o && !t) continue;
246
+ if (b && (!o || !t)) {
247
+ const kept = o ?? t;
248
+ if (same(kept, b)) continue;
249
+ const side = m.conflict({
250
+ id: `wire:${id}`,
251
+ kind: "wire",
252
+ edge: id,
253
+ label: `The wire from ${titleOf(kept.edge.from.node)} (${kept.edge.from.port}): removed ${o ? "in theirs" : "here"}, changed ${o ? "here" : "in theirs"}`,
254
+ ours: o ? kept.edge : void 0,
255
+ theirs: o ? void 0 : kept.edge
256
+ });
257
+ if (side === "ours" === !!o) edges.set(id, { ...kept, from: o ? "ours" : "theirs" });
258
+ continue;
259
+ }
260
+ if (!o || !t) {
261
+ edges.set(id, { ...o ?? t, from: o ? "ours" : "theirs" });
262
+ continue;
263
+ }
264
+ const merged = m.value(
265
+ b,
266
+ o,
267
+ t,
268
+ {
269
+ id: `wire:${id}`,
270
+ label: `The wire from ${titleOf(o.edge.from.node)} (${o.edge.from.port})`,
271
+ field: "",
272
+ owner: { edge: id }
273
+ },
274
+ 0
275
+ );
276
+ edges.set(id, { ...merged, from: "both" });
277
+ }
278
+ const ends = /* @__PURE__ */ new Set();
279
+ for (const [id, e] of edges) {
280
+ const a = nodes.get(e.edge.from.node);
281
+ const z = nodes.get(e.edge.to.node);
282
+ const key = `${e.level}|${e.edge.from.node}.${e.edge.from.port}>${e.edge.to.node}.${e.edge.to.port}`;
283
+ if (!a || !z || a.level !== e.level || z.level !== e.level || ends.has(key)) edges.delete(id);
284
+ else ends.add(key);
285
+ }
286
+ const byPort = /* @__PURE__ */ new Map();
287
+ for (const [id, e] of edges) {
288
+ const key = `${e.edge.from.node}|${e.edge.from.port}`;
289
+ byPort.set(key, [...byPort.get(key) ?? [], id]);
290
+ }
291
+ for (const [key, ids] of byPort) {
292
+ if (ids.length < 2) continue;
293
+ const sides = new Set(ids.map((id) => edges.get(id)?.from));
294
+ if (!sides.has("ours") && !sides.has("theirs")) continue;
295
+ const [node = "", port = ""] = key.split("|");
296
+ const target = (s) => ids.filter((id) => (s === "ours" ? ours : theirs).edges.has(id)).map((id) => titleOf(edges.get(id)?.edge.to.node ?? "")).join(", ");
297
+ const side = m.conflict({
298
+ id: `port:${node}.${port}`,
299
+ kind: "wire",
300
+ node,
301
+ label: `${titleOf(node)}: port ${port} wired to ${target("ours") || "nothing"} here, to ${target("theirs") || "nothing"} in theirs`,
302
+ field: `ports.${port}`,
303
+ ours: target("ours"),
304
+ theirs: target("theirs")
305
+ });
306
+ const keep = side === "ours" ? ours : theirs;
307
+ for (const id of ids) if (!keep.edges.has(id)) edges.delete(id);
308
+ }
309
+ const annotations = /* @__PURE__ */ new Map();
310
+ const PLACE = ["x", "y", "width", "height"];
311
+ for (const id of order("annotations")) {
312
+ const b = base.annotations.get(id);
313
+ const o = ours.annotations.get(id);
314
+ const t = theirs.annotations.get(id);
315
+ if (!o && !t) continue;
316
+ if (b && (!o || !t)) {
317
+ const kept = o ?? t;
318
+ if (same(kept, b)) continue;
319
+ const side = m.conflict({
320
+ id: `note:${id}`,
321
+ kind: "removed",
322
+ annotation: id,
323
+ label: `The note "${kept.annotation.text.split("\n")[0]}": removed ${o ? "in theirs" : "here"}, changed ${o ? "here" : "in theirs"}`,
324
+ ours: o ? "changed" : "removed",
325
+ theirs: o ? "removed" : "changed"
326
+ });
327
+ if (side === "ours" === !!o) annotations.set(id, kept);
328
+ continue;
329
+ }
330
+ if (!o || !t) {
331
+ annotations.set(id, o ?? t);
332
+ continue;
333
+ }
334
+ const strip = (a) => a && Object.fromEntries(Object.entries(a).filter(([k]) => !PLACE.includes(k)));
335
+ const label = `The note "${o.annotation.text.split("\n")[0]}"`;
336
+ const fields = m.fields(
337
+ strip(b?.annotation),
338
+ strip(o.annotation),
339
+ strip(t.annotation),
340
+ { id: `note:${id}`, label, owner: { annotation: id } },
341
+ 1
342
+ );
343
+ const place = {};
344
+ for (const k of PLACE) {
345
+ const v = quiet(
346
+ b?.annotation?.[k],
347
+ o.annotation[k],
348
+ t.annotation[k]
349
+ );
350
+ if (v !== void 0) place[k] = v;
351
+ }
352
+ const level2 = quiet(b?.level, o.level, t.level) ?? o.level;
353
+ annotations.set(id, { level: level2, annotation: { ...fields, ...place } });
354
+ }
355
+ for (const [id, a] of annotations)
356
+ if (a.level !== ROOT && !nodes.get(a.level)?.body || a.annotation.on && !nodes.has(a.annotation.on))
357
+ annotations.delete(id);
358
+ const { version: vb, ...hb } = base.doc.flow;
359
+ const { version: vo, ...ho } = ours.doc.flow;
360
+ const { version: vt, ...ht } = theirs.doc.flow;
361
+ const header = m.fields(
362
+ hb,
363
+ ho,
364
+ ht,
365
+ { id: "flow", label: "The flow", owner: {} },
366
+ 2
367
+ );
368
+ const version = vo === vt ? vo : vo === vb ? vt : vt === vb ? vo : Math.max(vo, vt) + 1;
369
+ const ident = (d) => JSON.stringify(canonical(d));
370
+ const baseDecisions = new Set((base.doc.decisions ?? []).map(ident));
371
+ const theirsDecisions = new Set((theirs.doc.decisions ?? []).map(ident));
372
+ const decisions = [
373
+ ...(ours.doc.decisions ?? []).filter(
374
+ (d) => !baseDecisions.has(ident(d)) || theirsDecisions.has(ident(d))
375
+ ),
376
+ ...(theirs.doc.decisions ?? []).filter(
377
+ (d) => !baseDecisions.has(ident(d)) && !(ours.doc.decisions ?? []).some((x) => ident(x) === ident(d))
378
+ )
379
+ ];
380
+ const level = (id) => {
381
+ const levelNodes = [];
382
+ const layout = {};
383
+ for (const [nid, n] of nodes) {
384
+ if (n.level !== id) continue;
385
+ const node = { ...n.own };
386
+ if (n.body) {
387
+ const inner = level(nid);
388
+ const keys2 = new Set(n.body.keys);
389
+ const body = { nodes: inner.nodes, edges: inner.edges };
390
+ if (inner.annotations.length || keys2.has("annotations"))
391
+ body.annotations = inner.annotations;
392
+ if (Object.keys(inner.layout).length || keys2.has("layout")) body.layout = inner.layout;
393
+ if (n.body.boundary) body.boundary = n.body.boundary;
394
+ node.subflow = body;
395
+ }
396
+ levelNodes.push(node);
397
+ if (n.position) layout[nid] = n.position;
398
+ }
399
+ return {
400
+ nodes: levelNodes,
401
+ edges: [...edges.values()].filter((e) => e.level === id).map((e) => e.edge),
402
+ annotations: [...annotations.values()].filter((a) => a.level === id).map((a) => a.annotation),
403
+ layout
404
+ };
405
+ };
406
+ const top = level(ROOT);
407
+ const keys = /* @__PURE__ */ new Set([...ours.rootKeys, ...theirs.rootKeys]);
408
+ const doc = {
409
+ schema_version: oursDoc.schema_version,
410
+ flow: { ...header, version },
411
+ nodes: top.nodes,
412
+ edges: top.edges,
413
+ ...top.annotations.length || keys.has("annotations") ? { annotations: top.annotations } : {},
414
+ ...Object.keys(top.layout).length || keys.has("layout") ? { layout: top.layout } : {},
415
+ ...decisions.length || keys.has("decisions") ? { decisions } : {}
416
+ };
417
+ return { doc, conflicts: m.conflicts };
418
+ }
419
+ export {
420
+ mergeFlows
421
+ };
422
+ //# sourceMappingURL=merge.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../../src/core/flow/merge.ts"],
4
+ "sourcesContent": ["// AFD-283: a three-way merge of a flow, by the nodes' IDs rather than the YAML's lines. Two people\n// change one flow apart (two clones, a push through the server, a live session whose branch moved\n// on); what they changed in different places comes together on its own, and what they changed in\n// the same place is a conflict with both sides named. Pure: the merge driver, the designer and\n// the server call it with the three versions and the choices made so far.\n//\n// The flow is taken apart into its nodes, wires and Notes with the level each is on, so a step\n// one side moved into a subflow is still the same step. Fields are merged one by one (records a\n// few levels deep: a step's settings, inputs, its binding's parameters); a value both sides\n// changed differently is a conflict, kept as \"ours\" until someone chooses. Positions never\n// conflict: ours wins. Rules a merge could break on its own are conflicts too: a step one side\n// removed and the other changed or wired, and two wires on one output port.\nimport { withIds } from './completeIds';\nimport { ROOT } from './model';\nimport { canonical } from './semantic';\nimport type {\n Annotation,\n Decision,\n Edge,\n FlowDocument,\n FlowHeader,\n FlowNode,\n Position,\n SubflowBody,\n Ulid,\n} from './types';\n\nexport type MergeSide = 'ours' | 'theirs';\n\nexport interface MergeConflict {\n /** Stable across merges of the same versions: choices are given by it. */\n id: string;\n kind: 'value' | 'removed' | 'wire' | 'format';\n /** Where and what, in words: \"Say hello: settings.timeout\". */\n label: string;\n node?: Ulid;\n annotation?: Ulid;\n edge?: Ulid;\n /** The field, dotted (`settings.timeout`); absent for a whole step, wire or the format. */\n field?: string;\n base?: unknown;\n ours?: unknown;\n theirs?: unknown;\n}\n\nexport interface FlowMerge {\n /** The merged flow, each conflict as its choice says (ours until chosen). */\n doc: FlowDocument;\n conflicts: MergeConflict[];\n}\n\n// --- taking a flow apart\n\ninterface FlatNode {\n level: string;\n /** The node without its inner level. */\n own: Omit<FlowNode, 'subflow'>;\n /** Present for a subflow: its boundary, and which keys its body had (kept as they were). */\n body?: { boundary?: SubflowBody['boundary']; keys: string[] };\n position?: Position;\n}\ninterface FlatEdge {\n level: string;\n edge: Edge;\n}\ninterface FlatAnnotation {\n level: string;\n annotation: Annotation;\n}\ninterface Flat {\n doc: FlowDocument;\n nodes: Map<Ulid, FlatNode>;\n edges: Map<Ulid, FlatEdge>;\n annotations: Map<Ulid, FlatAnnotation>;\n /** Which optional keys the top level had (kept as they were). */\n rootKeys: string[];\n}\n\nfunction flatten(doc: FlowDocument): Flat {\n const flat: Flat = {\n doc,\n nodes: new Map(),\n edges: new Map(),\n annotations: new Map(),\n rootKeys: ['annotations', 'layout', 'decisions'].filter((k) => k in doc),\n };\n const visit = (\n level: string,\n nodes: FlowNode[] | undefined,\n edges: Edge[] | undefined,\n annotations: Annotation[] | undefined,\n layout: Record<string, Position> | undefined,\n ) => {\n for (const node of nodes ?? []) {\n if (!node?.id) continue;\n const { subflow, ...own } = node;\n flat.nodes.set(node.id, {\n level,\n own,\n ...(subflow\n ? {\n body: {\n ...(subflow.boundary ? { boundary: subflow.boundary } : {}),\n keys: Object.keys(subflow),\n },\n }\n : {}),\n ...(layout?.[node.id] ? { position: layout[node.id] } : {}),\n });\n if (subflow)\n visit(node.id, subflow.nodes, subflow.edges, subflow.annotations, subflow.layout);\n }\n for (const edge of edges ?? []) if (edge?.id) flat.edges.set(edge.id, { level, edge });\n for (const a of annotations ?? [])\n if (a?.id) flat.annotations.set(a.id, { level, annotation: a });\n };\n visit(ROOT, doc.nodes, doc.edges, doc.annotations, doc.layout);\n return flat;\n}\n\n// --- merging values\n\nconst same = (a: unknown, b: unknown) =>\n JSON.stringify(canonical(a)) === JSON.stringify(canonical(b));\nconst isRecord = (v: unknown): v is Record<string, unknown> =>\n typeof v === 'object' && v !== null && !Array.isArray(v);\n\nclass Merger {\n readonly conflicts: MergeConflict[] = [];\n constructor(private readonly choices: Record<string, MergeSide>) {}\n\n pick(id: string): MergeSide {\n return this.choices[id] ?? 'ours';\n }\n\n conflict(c: MergeConflict): MergeSide {\n this.conflicts.push(c);\n return this.pick(c.id);\n }\n\n /**\n * One value, three ways. Records are merged key by key `depth` levels down; a value both sides\n * changed differently is a conflict named by `id` (`<owner>:<field>`).\n */\n value(\n base: unknown,\n ours: unknown,\n theirs: unknown,\n at: { id: string; label: string; field: string; owner: Partial<MergeConflict> },\n depth: number,\n ): unknown {\n if (same(ours, theirs)) return ours;\n if (same(base, ours)) return theirs;\n if (same(base, theirs)) return ours;\n if (depth > 0 && isRecord(ours) && isRecord(theirs) && (base === undefined || isRecord(base))) {\n const out: Record<string, unknown> = {};\n for (const key of new Set([...Object.keys(ours), ...Object.keys(theirs)])) {\n const v = this.value(\n (base as Record<string, unknown> | undefined)?.[key],\n ours[key],\n theirs[key],\n {\n ...at,\n id: `${at.id}.${key}`,\n field: at.field ? `${at.field}.${key}` : key,\n },\n depth - 1,\n );\n if (v !== undefined) out[key] = v;\n }\n return out;\n }\n const side = this.conflict({\n ...at.owner,\n id: at.id,\n kind: 'value',\n label: at.field ? `${at.label}: ${at.field}` : at.label,\n ...(at.field ? { field: at.field } : {}),\n base,\n ours,\n theirs,\n });\n return side === 'ours' ? ours : theirs;\n }\n\n /** The fields of a record, each merged on its own (`depth` levels down inside each). */\n fields(\n base: Record<string, unknown> | undefined,\n ours: Record<string, unknown>,\n theirs: Record<string, unknown>,\n at: { id: string; label: string; owner: Partial<MergeConflict> },\n depth: number,\n ): Record<string, unknown> {\n const out: Record<string, unknown> = {};\n for (const key of new Set([...Object.keys(ours), ...Object.keys(theirs)])) {\n const v = this.value(\n base?.[key],\n ours[key],\n theirs[key],\n { ...at, id: `${at.id}:${key}`, field: key },\n depth,\n );\n if (v !== undefined) out[key] = v;\n }\n return out;\n }\n}\n\n/** A position, or another value that never conflicts: the side that moved it, else ours. */\nfunction quiet<T>(base: T | undefined, ours: T | undefined, theirs: T | undefined): T | undefined {\n return same(base, ours) ? theirs : ours;\n}\n\n// --- the merge\n\n/**\n * Merges what `ours` and `theirs` changed since `base`. `choices` settle conflicts by id (ours\n * when not given). Flows without IDs are matched by the IDs their keys give (Complete IDs).\n */\nexport function mergeFlows(\n baseDoc: FlowDocument,\n oursDoc: FlowDocument,\n theirsDoc: FlowDocument,\n choices: Record<string, MergeSide> = {},\n): FlowMerge {\n const m = new Merger(choices);\n if (oursDoc.schema_version !== theirsDoc.schema_version) {\n const side = m.conflict({\n id: 'format',\n kind: 'format',\n label: `The flow's format: ${oursDoc.schema_version} here, ${theirsDoc.schema_version} in theirs (upgrade both first)`,\n ours: oursDoc.schema_version,\n theirs: theirsDoc.schema_version,\n });\n return { doc: side === 'ours' ? oursDoc : theirsDoc, conflicts: m.conflicts };\n }\n const base = flatten(withIds(baseDoc));\n const ours = flatten(withIds(oursDoc));\n const theirs = flatten(withIds(theirsDoc));\n const titleOf = (id: Ulid) =>\n (ours.nodes.get(id) ?? theirs.nodes.get(id) ?? base.nodes.get(id))?.own.title ?? id;\n\n // What a side did to a step, its wires, or anything inside it: a removal by the other side then\n // loses work and is a conflict.\n const touched = (side: Flat, id: Ulid, seen = new Set<Ulid>()): boolean => {\n if (seen.has(id)) return false;\n seen.add(id);\n const now = side.nodes.get(id);\n const was = base.nodes.get(id);\n if (!now) return false;\n if (!was || !same(now.own, was.own) || now.level !== was.level || !same(now.body, was.body))\n return true;\n for (const [eid, e] of side.edges)\n if ((e.edge.from.node === id || e.edge.to.node === id) && !same(e, base.edges.get(eid)))\n return true;\n for (const [aid, a] of side.annotations)\n if ((a.annotation.on === id || a.level === id) && !same(a, base.annotations.get(aid)))\n return true;\n for (const [cid, c] of side.nodes) if (c.level === id && touched(side, cid, seen)) return true;\n return false;\n };\n\n // Steps.\n const order = (key: 'nodes' | 'edges' | 'annotations') => [\n ...new Set([...ours[key].keys(), ...theirs[key].keys(), ...base[key].keys()]),\n ];\n const nodes = new Map<Ulid, FlatNode>();\n for (const id of order('nodes')) {\n const b = base.nodes.get(id);\n const o = ours.nodes.get(id);\n const t = theirs.nodes.get(id);\n if (!o && !t) continue;\n if (b && (!o || !t)) {\n // One side removed it: gone, unless the other side changed it meanwhile.\n const kept = (o ?? t) as FlatNode;\n if (!touched(o ? ours : theirs, id)) continue;\n const side = m.conflict({\n id: `node:${id}`,\n kind: 'removed',\n node: id,\n label: `${titleOf(id)}: removed ${o ? 'in theirs' : 'here'}, changed ${o ? 'here' : 'in theirs'}`,\n ours: o ? 'changed' : 'removed',\n theirs: o ? 'removed' : 'changed',\n });\n if ((side === 'ours') === !!o) nodes.set(id, kept);\n continue;\n }\n if (!o || !t) {\n nodes.set(id, (o ?? t) as FlatNode);\n continue;\n }\n const owner = { node: id };\n const label = titleOf(id);\n const own = m.fields(\n b?.own as Record<string, unknown> | undefined,\n o.own as Record<string, unknown>,\n t.own as Record<string, unknown>,\n { id: `node:${id}`, label, owner },\n 3,\n ) as FlatNode['own'];\n const level = m.value(\n b?.level,\n o.level,\n t.level,\n { id: `node:${id}:level`, label, field: 'level (moved into another subflow)', owner },\n 0,\n ) as string;\n let body: FlatNode['body'];\n const hasBody = m.value(\n !!b?.body,\n !!o.body,\n !!t.body,\n { id: `node:${id}:subflow`, label, field: 'subflow', owner },\n 0,\n );\n if (hasBody) {\n const boundary = m.value(\n b?.body?.boundary,\n o.body?.boundary,\n t.body?.boundary,\n { id: `node:${id}:boundary`, label, field: 'boundary', owner },\n 2,\n ) as SubflowBody['boundary'];\n body = {\n ...(boundary ? { boundary } : {}),\n keys: [...new Set([...(o.body?.keys ?? []), ...(t.body?.keys ?? [])])],\n };\n }\n const position = quiet(b?.position, o.position, t.position);\n nodes.set(id, {\n level,\n own,\n ...(body ? { body } : {}),\n ...(position ? { position } : {}),\n });\n }\n // A step whose subflow is gone goes with it.\n for (let again = true; again; ) {\n again = false;\n for (const [id, n] of nodes)\n if (n.level !== ROOT && !nodes.get(n.level)?.body) {\n nodes.delete(id);\n again = true;\n }\n }\n\n // Wires.\n const edges = new Map<Ulid, FlatEdge & { from: MergeSide | 'both' }>();\n for (const id of order('edges')) {\n const b = base.edges.get(id);\n const o = ours.edges.get(id);\n const t = theirs.edges.get(id);\n if (!o && !t) continue;\n if (b && (!o || !t)) {\n const kept = (o ?? t) as FlatEdge;\n if (same(kept, b)) continue;\n // Removed on one side, rewired on the other.\n const side = m.conflict({\n id: `wire:${id}`,\n kind: 'wire',\n edge: id,\n label: `The wire from ${titleOf(kept.edge.from.node)} (${kept.edge.from.port}): removed ${o ? 'in theirs' : 'here'}, changed ${o ? 'here' : 'in theirs'}`,\n ours: o ? kept.edge : undefined,\n theirs: o ? undefined : kept.edge,\n });\n if ((side === 'ours') === !!o) edges.set(id, { ...kept, from: o ? 'ours' : 'theirs' });\n continue;\n }\n if (!o || !t) {\n edges.set(id, { ...((o ?? t) as FlatEdge), from: o ? 'ours' : 'theirs' });\n continue;\n }\n const merged = m.value(\n b,\n o,\n t,\n {\n id: `wire:${id}`,\n label: `The wire from ${titleOf(o.edge.from.node)} (${o.edge.from.port})`,\n field: '',\n owner: { edge: id },\n },\n 0,\n ) as FlatEdge;\n edges.set(id, { ...merged, from: 'both' });\n }\n // Wires whose ends are gone or now on another level go; the same wire added twice is one.\n const ends = new Set<string>();\n for (const [id, e] of edges) {\n const a = nodes.get(e.edge.from.node);\n const z = nodes.get(e.edge.to.node);\n const key = `${e.level}|${e.edge.from.node}.${e.edge.from.port}>${e.edge.to.node}.${e.edge.to.port}`;\n if (!a || !z || a.level !== e.level || z.level !== e.level || ends.has(key)) edges.delete(id);\n else ends.add(key);\n }\n // One wire per output port: two from different sides are a conflict.\n const byPort = new Map<string, Ulid[]>();\n for (const [id, e] of edges) {\n const key = `${e.edge.from.node}|${e.edge.from.port}`;\n byPort.set(key, [...(byPort.get(key) ?? []), id]);\n }\n for (const [key, ids] of byPort) {\n if (ids.length < 2) continue;\n const sides = new Set(ids.map((id) => edges.get(id)?.from));\n if (!sides.has('ours') && !sides.has('theirs')) continue;\n const [node = '', port = ''] = key.split('|');\n const target = (s: MergeSide) =>\n ids\n .filter((id) => (s === 'ours' ? ours : theirs).edges.has(id))\n .map((id) => titleOf(edges.get(id)?.edge.to.node ?? ''))\n .join(', ');\n const side = m.conflict({\n id: `port:${node}.${port}`,\n kind: 'wire',\n node,\n label: `${titleOf(node)}: port ${port} wired to ${target('ours') || 'nothing'} here, to ${target('theirs') || 'nothing'} in theirs`,\n field: `ports.${port}`,\n ours: target('ours'),\n theirs: target('theirs'),\n });\n const keep = side === 'ours' ? ours : theirs;\n for (const id of ids) if (!keep.edges.has(id)) edges.delete(id);\n }\n\n // Notes, Sections and Text.\n const annotations = new Map<Ulid, FlatAnnotation>();\n const PLACE = ['x', 'y', 'width', 'height'];\n for (const id of order('annotations')) {\n const b = base.annotations.get(id);\n const o = ours.annotations.get(id);\n const t = theirs.annotations.get(id);\n if (!o && !t) continue;\n if (b && (!o || !t)) {\n const kept = (o ?? t) as FlatAnnotation;\n if (same(kept, b)) continue;\n const side = m.conflict({\n id: `note:${id}`,\n kind: 'removed',\n annotation: id,\n label: `The note \"${kept.annotation.text.split('\\n')[0]}\": removed ${o ? 'in theirs' : 'here'}, changed ${o ? 'here' : 'in theirs'}`,\n ours: o ? 'changed' : 'removed',\n theirs: o ? 'removed' : 'changed',\n });\n if ((side === 'ours') === !!o) annotations.set(id, kept);\n continue;\n }\n if (!o || !t) {\n annotations.set(id, (o ?? t) as FlatAnnotation);\n continue;\n }\n const strip = (a?: Annotation) =>\n a && Object.fromEntries(Object.entries(a).filter(([k]) => !PLACE.includes(k)));\n const label = `The note \"${o.annotation.text.split('\\n')[0]}\"`;\n const fields = m.fields(\n strip(b?.annotation),\n strip(o.annotation) as Record<string, unknown>,\n strip(t.annotation) as Record<string, unknown>,\n { id: `note:${id}`, label, owner: { annotation: id } },\n 1,\n );\n const place: Record<string, unknown> = {};\n for (const k of PLACE) {\n const v = quiet(\n (b?.annotation as unknown as Record<string, unknown> | undefined)?.[k],\n (o.annotation as unknown as Record<string, unknown>)[k],\n (t.annotation as unknown as Record<string, unknown>)[k],\n );\n if (v !== undefined) place[k] = v;\n }\n const level = quiet(b?.level, o.level, t.level) ?? o.level;\n annotations.set(id, { level, annotation: { ...fields, ...place } as unknown as Annotation });\n }\n for (const [id, a] of annotations)\n if (\n (a.level !== ROOT && !nodes.get(a.level)?.body) ||\n (a.annotation.on && !nodes.has(a.annotation.on))\n )\n annotations.delete(id);\n\n // The flow itself: its fields; its version moves on when both sides moved it.\n const { version: vb, ...hb } = base.doc.flow;\n const { version: vo, ...ho } = ours.doc.flow;\n const { version: vt, ...ht } = theirs.doc.flow;\n const header = m.fields(\n hb as Record<string, unknown>,\n ho as Record<string, unknown>,\n ht as Record<string, unknown>,\n { id: 'flow', label: 'The flow', owner: {} },\n 2,\n ) as Omit<FlowHeader, 'version'>;\n const version = vo === vt ? vo : vo === vb ? vt : vt === vb ? vo : Math.max(vo, vt) + 1;\n\n // Decisions: a set; what a side added is in, what a side took out is out.\n const ident = (d: Decision) => JSON.stringify(canonical(d));\n const baseDecisions = new Set((base.doc.decisions ?? []).map(ident));\n const theirsDecisions = new Set((theirs.doc.decisions ?? []).map(ident));\n const decisions = [\n ...(ours.doc.decisions ?? []).filter(\n (d) => !baseDecisions.has(ident(d)) || theirsDecisions.has(ident(d)),\n ),\n ...(theirs.doc.decisions ?? []).filter(\n (d) =>\n !baseDecisions.has(ident(d)) &&\n !(ours.doc.decisions ?? []).some((x) => ident(x) === ident(d)),\n ),\n ];\n\n // Put it back together, level by level, in ours's order with theirs's new parts after.\n const level = (id: string) => {\n const levelNodes: FlowNode[] = [];\n const layout: Record<string, Position> = {};\n for (const [nid, n] of nodes) {\n if (n.level !== id) continue;\n const node = { ...n.own } as FlowNode;\n if (n.body) {\n const inner = level(nid);\n const keys = new Set(n.body.keys);\n const body: SubflowBody = { nodes: inner.nodes, edges: inner.edges };\n if (inner.annotations.length || keys.has('annotations'))\n body.annotations = inner.annotations;\n if (Object.keys(inner.layout).length || keys.has('layout')) body.layout = inner.layout;\n if (n.body.boundary) body.boundary = n.body.boundary;\n node.subflow = body;\n }\n levelNodes.push(node);\n if (n.position) layout[nid] = n.position;\n }\n return {\n nodes: levelNodes,\n edges: [...edges.values()].filter((e) => e.level === id).map((e) => e.edge),\n annotations: [...annotations.values()].filter((a) => a.level === id).map((a) => a.annotation),\n layout,\n };\n };\n const top = level(ROOT);\n const keys = new Set([...ours.rootKeys, ...theirs.rootKeys]);\n const doc: FlowDocument = {\n schema_version: oursDoc.schema_version,\n flow: { ...header, version } as FlowHeader,\n nodes: top.nodes,\n edges: top.edges,\n ...(top.annotations.length || keys.has('annotations') ? { annotations: top.annotations } : {}),\n ...(Object.keys(top.layout).length || keys.has('layout') ? { layout: top.layout } : {}),\n ...(decisions.length || keys.has('decisions') ? { decisions } : {}),\n };\n return { doc, conflicts: m.conflicts };\n}\n"],
5
+ "mappings": "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",
6
+ "names": ["level", "keys"]
7
+ }
@@ -0,0 +1,3 @@
1
+ import type { FlowDocument } from './types';
2
+ /** `text` (ours) rewritten to say `doc`. */
3
+ export declare function writeMergedFlow(text: string, doc: FlowDocument): string;
@@ -0,0 +1,71 @@
1
+ import {
2
+ canonical
3
+ } from "../chunks/chunk-TG3C3GIL.js";
4
+ import "../chunks/chunk-VG3RKETK.js";
5
+ import {
6
+ YamlTransaction
7
+ } from "../chunks/chunk-3Z2U3LN7.js";
8
+ import "../chunks/chunk-GHG3VINX.js";
9
+ import {
10
+ deleteKey,
11
+ insertItem,
12
+ removeItem,
13
+ setIn
14
+ } from "../chunks/chunk-5TUAMVIV.js";
15
+ import {
16
+ parseYaml
17
+ } from "../chunks/chunk-45CE2RUH.js";
18
+ import "../chunks/chunk-E2YZJWO7.js";
19
+ import "../chunks/chunk-AGWQ3ANE.js";
20
+
21
+ // src/core/flow/mergeText.ts
22
+ var same = (a, b) => JSON.stringify(canonical(a)) === JSON.stringify(canonical(b));
23
+ var isRecord = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
24
+ var idOf = (v) => isRecord(v) && typeof v.id === "string" ? v.id : void 0;
25
+ var byId = (list) => list.length > 0 && list.every((v) => idOf(v) !== void 0);
26
+ function writeMergedFlow(text, doc) {
27
+ const tx = new YamlTransaction(text);
28
+ const current = parseYaml(text).doc.toJS();
29
+ sync(tx, [], current, doc);
30
+ return tx.text;
31
+ }
32
+ function sync(tx, path, was, now) {
33
+ if (same(was, now)) return;
34
+ if (isRecord(was) && isRecord(now)) {
35
+ for (const key of Object.keys(was))
36
+ if (!(key in now) || now[key] === void 0) tx.apply((f) => deleteKey(f, path, key));
37
+ for (const [key, value] of Object.entries(now)) {
38
+ if (value === void 0) continue;
39
+ if (key in was) sync(tx, [...path, key], was[key], value);
40
+ else tx.apply((f) => setIn(f, [...path, key], value));
41
+ }
42
+ return;
43
+ }
44
+ if (Array.isArray(was) && Array.isArray(now) && byId(was) && byId(now)) {
45
+ const wanted = new Set(now.map(idOf));
46
+ const list = [...was];
47
+ for (let i = list.length - 1; i >= 0; i--)
48
+ if (!wanted.has(idOf(list[i]))) {
49
+ tx.apply((f) => removeItem(f, path, i));
50
+ list.splice(i, 1);
51
+ }
52
+ now.forEach((item, i) => {
53
+ const at = list.findIndex((x) => idOf(x) === idOf(item));
54
+ if (at >= 0) {
55
+ sync(tx, [...path, at], list[at], item);
56
+ return;
57
+ }
58
+ const index = Math.min(i, list.length);
59
+ tx.apply(
60
+ (f) => insertItem(f, path, index === list.length ? "end" : index, item)
61
+ );
62
+ list.splice(index, 0, item);
63
+ });
64
+ return;
65
+ }
66
+ tx.apply((f) => setIn(f, path, now));
67
+ }
68
+ export {
69
+ writeMergedFlow
70
+ };
71
+ //# sourceMappingURL=mergeText.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../../src/core/flow/mergeText.ts"],
4
+ "sourcesContent": ["// AFD-283: a merged flow written over \"ours\" text with the fewest edits, so its comments, quoting\n// and order stay where they were and the file's diff shows only what the merge brought in. Lists\n// of things with IDs (steps, wires, notes) are matched by ID; anything else changed is replaced.\n\nimport { deleteKey, insertItem, removeItem, setIn } from '../yaml/ops';\nimport { type Path, parseYaml } from '../yaml/parse';\nimport type { YamlValue } from '../yaml/render';\nimport { YamlTransaction } from '../yaml/transaction';\nimport { canonical } from './semantic';\nimport type { FlowDocument } from './types';\n\nconst same = (a: unknown, b: unknown) =>\n JSON.stringify(canonical(a)) === JSON.stringify(canonical(b));\nconst isRecord = (v: unknown): v is Record<string, unknown> =>\n typeof v === 'object' && v !== null && !Array.isArray(v);\nconst idOf = (v: unknown) => (isRecord(v) && typeof v.id === 'string' ? v.id : undefined);\nconst byId = (list: unknown[]) => list.length > 0 && list.every((v) => idOf(v) !== undefined);\n\n/** `text` (ours) rewritten to say `doc`. */\nexport function writeMergedFlow(text: string, doc: FlowDocument): string {\n const tx = new YamlTransaction(text);\n const current = parseYaml(text).doc.toJS() as unknown;\n sync(tx, [], current, doc);\n return tx.text;\n}\n\nfunction sync(tx: YamlTransaction, path: Path, was: unknown, now: unknown): void {\n if (same(was, now)) return;\n if (isRecord(was) && isRecord(now)) {\n for (const key of Object.keys(was))\n if (!(key in now) || now[key] === undefined) tx.apply((f) => deleteKey(f, path, key));\n for (const [key, value] of Object.entries(now)) {\n if (value === undefined) continue;\n if (key in was) sync(tx, [...path, key], was[key], value);\n else tx.apply((f) => setIn(f, [...path, key], value as YamlValue));\n }\n return;\n }\n if (Array.isArray(was) && Array.isArray(now) && byId(was) && byId(now)) {\n const wanted = new Set(now.map(idOf));\n // Removals first, from the end, so the earlier indexes hold.\n const list = [...was];\n for (let i = list.length - 1; i >= 0; i--)\n if (!wanted.has(idOf(list[i]))) {\n tx.apply((f) => removeItem(f, path, i));\n list.splice(i, 1);\n }\n now.forEach((item, i) => {\n const at = list.findIndex((x) => idOf(x) === idOf(item));\n if (at >= 0) {\n sync(tx, [...path, at], list[at], item);\n return;\n }\n const index = Math.min(i, list.length);\n tx.apply((f) =>\n insertItem(f, path, index === list.length ? 'end' : index, item as YamlValue),\n );\n list.splice(index, 0, item);\n });\n return;\n }\n tx.apply((f) => setIn(f, path, now as YamlValue));\n}\n"],
5
+ "mappings": ";;;;;;;;;;;;;;;;;;;;;AAWA,IAAM,OAAO,CAAC,GAAY,MACxB,KAAK,UAAU,UAAU,CAAC,CAAC,MAAM,KAAK,UAAU,UAAU,CAAC,CAAC;AAC9D,IAAM,WAAW,CAAC,MAChB,OAAO,MAAM,YAAY,MAAM,QAAQ,CAAC,MAAM,QAAQ,CAAC;AACzD,IAAM,OAAO,CAAC,MAAgB,SAAS,CAAC,KAAK,OAAO,EAAE,OAAO,WAAW,EAAE,KAAK;AAC/E,IAAM,OAAO,CAAC,SAAoB,KAAK,SAAS,KAAK,KAAK,MAAM,CAAC,MAAM,KAAK,CAAC,MAAM,MAAS;AAGrF,SAAS,gBAAgB,MAAc,KAA2B;AACvE,QAAM,KAAK,IAAI,gBAAgB,IAAI;AACnC,QAAM,UAAU,UAAU,IAAI,EAAE,IAAI,KAAK;AACzC,OAAK,IAAI,CAAC,GAAG,SAAS,GAAG;AACzB,SAAO,GAAG;AACZ;AAEA,SAAS,KAAK,IAAqB,MAAY,KAAc,KAAoB;AAC/E,MAAI,KAAK,KAAK,GAAG,EAAG;AACpB,MAAI,SAAS,GAAG,KAAK,SAAS,GAAG,GAAG;AAClC,eAAW,OAAO,OAAO,KAAK,GAAG;AAC/B,UAAI,EAAE,OAAO,QAAQ,IAAI,GAAG,MAAM,OAAW,IAAG,MAAM,CAAC,MAAM,UAAU,GAAG,MAAM,GAAG,CAAC;AACtF,eAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,GAAG,GAAG;AAC9C,UAAI,UAAU,OAAW;AACzB,UAAI,OAAO,IAAK,MAAK,IAAI,CAAC,GAAG,MAAM,GAAG,GAAG,IAAI,GAAG,GAAG,KAAK;AAAA,UACnD,IAAG,MAAM,CAAC,MAAM,MAAM,GAAG,CAAC,GAAG,MAAM,GAAG,GAAG,KAAkB,CAAC;AAAA,IACnE;AACA;AAAA,EACF;AACA,MAAI,MAAM,QAAQ,GAAG,KAAK,MAAM,QAAQ,GAAG,KAAK,KAAK,GAAG,KAAK,KAAK,GAAG,GAAG;AACtE,UAAM,SAAS,IAAI,IAAI,IAAI,IAAI,IAAI,CAAC;AAEpC,UAAM,OAAO,CAAC,GAAG,GAAG;AACpB,aAAS,IAAI,KAAK,SAAS,GAAG,KAAK,GAAG;AACpC,UAAI,CAAC,OAAO,IAAI,KAAK,KAAK,CAAC,CAAC,CAAC,GAAG;AAC9B,WAAG,MAAM,CAAC,MAAM,WAAW,GAAG,MAAM,CAAC,CAAC;AACtC,aAAK,OAAO,GAAG,CAAC;AAAA,MAClB;AACF,QAAI,QAAQ,CAAC,MAAM,MAAM;AACvB,YAAM,KAAK,KAAK,UAAU,CAAC,MAAM,KAAK,CAAC,MAAM,KAAK,IAAI,CAAC;AACvD,UAAI,MAAM,GAAG;AACX,aAAK,IAAI,CAAC,GAAG,MAAM,EAAE,GAAG,KAAK,EAAE,GAAG,IAAI;AACtC;AAAA,MACF;AACA,YAAM,QAAQ,KAAK,IAAI,GAAG,KAAK,MAAM;AACrC,SAAG;AAAA,QAAM,CAAC,MACR,WAAW,GAAG,MAAM,UAAU,KAAK,SAAS,QAAQ,OAAO,IAAiB;AAAA,MAC9E;AACA,WAAK,OAAO,OAAO,GAAG,IAAI;AAAA,IAC5B,CAAC;AACD;AAAA,EACF;AACA,KAAG,MAAM,CAAC,MAAM,MAAM,GAAG,MAAM,GAAgB,CAAC;AAClD;",
6
+ "names": []
7
+ }
@@ -0,0 +1,6 @@
1
+ /** What a typed line means: a list or map in YAML flow syntax, else its text. */
2
+ export declare function valueOfText(text: string): unknown;
3
+ /** The same value, whatever the text: a field keeps what was typed while it means this. */
4
+ export declare function sameValue(a: unknown, b: unknown): boolean;
5
+ /** The line that means `value` again in `valueOfText`, or undefined when it needs several lines. */
6
+ export declare function textOfValue(value: unknown): string | undefined;
@@ -0,0 +1,56 @@
1
+ // src/core/flow/valueText.ts
2
+ import { parse, stringify } from "yaml";
3
+ var TEMPLATE = /^\s*\{[{%]/;
4
+ var QUOTED = /^(".*"|'.*')$/s;
5
+ var isRecord = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
6
+ function yaml(text) {
7
+ try {
8
+ return parse(text);
9
+ } catch {
10
+ return void 0;
11
+ }
12
+ }
13
+ function valueOfText(text) {
14
+ const t = text.trim();
15
+ if (TEMPLATE.test(t)) return text;
16
+ if (t.startsWith("[") || t.startsWith("{")) {
17
+ const v = yaml(t);
18
+ if (Array.isArray(v) || isRecord(v)) return v;
19
+ return text;
20
+ }
21
+ if (QUOTED.test(t)) {
22
+ const v = yaml(t);
23
+ if (typeof v === "string" && needsQuotes(v.trim())) return v;
24
+ }
25
+ return text;
26
+ }
27
+ function needsQuotes(t) {
28
+ if (QUOTED.test(t)) return /^.\s*[[{]/.test(t);
29
+ if (TEMPLATE.test(t) || !/^[[{]/.test(t)) return false;
30
+ const v = yaml(t);
31
+ return Array.isArray(v) || isRecord(v);
32
+ }
33
+ function sameValue(a, b) {
34
+ return JSON.stringify(a) === JSON.stringify(b);
35
+ }
36
+ function textOfValue(value) {
37
+ if (value === void 0 || value === null) return "";
38
+ if (typeof value === "string") {
39
+ return needsQuotes(value.trim()) ? JSON.stringify(value) : value;
40
+ }
41
+ if (Array.isArray(value) || isRecord(value)) {
42
+ const line = stringify(value, {
43
+ collectionStyle: "flow",
44
+ flowCollectionPadding: false,
45
+ lineWidth: 0
46
+ }).trim();
47
+ return line.includes("\n") ? void 0 : line;
48
+ }
49
+ return String(value);
50
+ }
51
+ export {
52
+ sameValue,
53
+ textOfValue,
54
+ valueOfText
55
+ };
56
+ //# sourceMappingURL=valueText.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../../src/core/flow/valueText.ts"],
4
+ "sourcesContent": ["// AFD-286: a value typed in a one-line field, read the way YAML (and so Ansible) reads it, and\n// shown back so it reads the same again. Unquoted `[a, b]` is a list and `{k: v}` a map; a quoted\n// value is the text inside the quotes; `{{ ... }}` and `{% ... %}` stay templates. Anything else\n// is text, as the field always kept it (numbers too: Ansible converts where it needs).\nimport { parse, stringify } from 'yaml';\n\nconst TEMPLATE = /^\\s*\\{[{%]/;\nconst QUOTED = /^(\".*\"|'.*')$/s;\nconst isRecord = (v: unknown): v is Record<string, unknown> =>\n typeof v === 'object' && v !== null && !Array.isArray(v);\n\nfunction yaml(text: string): unknown {\n try {\n return parse(text);\n } catch {\n return undefined;\n }\n}\n\n/** What a typed line means: a list or map in YAML flow syntax, else its text. */\nexport function valueOfText(text: string): unknown {\n const t = text.trim();\n if (TEMPLATE.test(t)) return text;\n if (t.startsWith('[') || t.startsWith('{')) {\n const v = yaml(t);\n if (Array.isArray(v) || isRecord(v)) return v;\n return text;\n }\n // A quoted list or map is the text inside the quotes: the way to keep \"[a, b]\" as text. Other\n // quotes stay as typed (they always did).\n if (QUOTED.test(t)) {\n const v = yaml(t);\n if (typeof v === 'string' && needsQuotes(v.trim())) return v;\n }\n return text;\n}\n\n/** Text that `valueOfText` would not give back as it is: quoted so it reads as text again. */\nfunction needsQuotes(t: string): boolean {\n if (QUOTED.test(t)) return /^.\\s*[[{]/.test(t);\n if (TEMPLATE.test(t) || !/^[[{]/.test(t)) return false;\n const v = yaml(t);\n return Array.isArray(v) || isRecord(v);\n}\n\n/** The same value, whatever the text: a field keeps what was typed while it means this. */\nexport function sameValue(a: unknown, b: unknown): boolean {\n return JSON.stringify(a) === JSON.stringify(b);\n}\n\n/** The line that means `value` again in `valueOfText`, or undefined when it needs several lines. */\nexport function textOfValue(value: unknown): string | undefined {\n if (value === undefined || value === null) return '';\n if (typeof value === 'string') {\n return needsQuotes(value.trim()) ? JSON.stringify(value) : value;\n }\n if (Array.isArray(value) || isRecord(value)) {\n const line = stringify(value, {\n collectionStyle: 'flow',\n flowCollectionPadding: false,\n lineWidth: 0,\n }).trim();\n return line.includes('\\n') ? undefined : line;\n }\n return String(value);\n}\n"],
5
+ "mappings": ";AAIA,SAAS,OAAO,iBAAiB;AAEjC,IAAM,WAAW;AACjB,IAAM,SAAS;AACf,IAAM,WAAW,CAAC,MAChB,OAAO,MAAM,YAAY,MAAM,QAAQ,CAAC,MAAM,QAAQ,CAAC;AAEzD,SAAS,KAAK,MAAuB;AACnC,MAAI;AACF,WAAO,MAAM,IAAI;AAAA,EACnB,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAGO,SAAS,YAAY,MAAuB;AACjD,QAAM,IAAI,KAAK,KAAK;AACpB,MAAI,SAAS,KAAK,CAAC,EAAG,QAAO;AAC7B,MAAI,EAAE,WAAW,GAAG,KAAK,EAAE,WAAW,GAAG,GAAG;AAC1C,UAAM,IAAI,KAAK,CAAC;AAChB,QAAI,MAAM,QAAQ,CAAC,KAAK,SAAS,CAAC,EAAG,QAAO;AAC5C,WAAO;AAAA,EACT;AAGA,MAAI,OAAO,KAAK,CAAC,GAAG;AAClB,UAAM,IAAI,KAAK,CAAC;AAChB,QAAI,OAAO,MAAM,YAAY,YAAY,EAAE,KAAK,CAAC,EAAG,QAAO;AAAA,EAC7D;AACA,SAAO;AACT;AAGA,SAAS,YAAY,GAAoB;AACvC,MAAI,OAAO,KAAK,CAAC,EAAG,QAAO,YAAY,KAAK,CAAC;AAC7C,MAAI,SAAS,KAAK,CAAC,KAAK,CAAC,QAAQ,KAAK,CAAC,EAAG,QAAO;AACjD,QAAM,IAAI,KAAK,CAAC;AAChB,SAAO,MAAM,QAAQ,CAAC,KAAK,SAAS,CAAC;AACvC;AAGO,SAAS,UAAU,GAAY,GAAqB;AACzD,SAAO,KAAK,UAAU,CAAC,MAAM,KAAK,UAAU,CAAC;AAC/C;AAGO,SAAS,YAAY,OAAoC;AAC9D,MAAI,UAAU,UAAa,UAAU,KAAM,QAAO;AAClD,MAAI,OAAO,UAAU,UAAU;AAC7B,WAAO,YAAY,MAAM,KAAK,CAAC,IAAI,KAAK,UAAU,KAAK,IAAI;AAAA,EAC7D;AACA,MAAI,MAAM,QAAQ,KAAK,KAAK,SAAS,KAAK,GAAG;AAC3C,UAAM,OAAO,UAAU,OAAO;AAAA,MAC5B,iBAAiB;AAAA,MACjB,uBAAuB;AAAA,MACvB,WAAW;AAAA,IACb,CAAC,EAAE,KAAK;AACR,WAAO,KAAK,SAAS,IAAI,IAAI,SAAY;AAAA,EAC3C;AACA,SAAO,OAAO,KAAK;AACrB;",
6
+ "names": []
7
+ }
@@ -4,13 +4,13 @@ import {
4
4
  paged,
5
5
  sourceOutline,
6
6
  taskBinding
7
- } from "../chunks/chunk-2HMLC2AO.js";
8
- import "../chunks/chunk-QMIXDOU6.js";
7
+ } from "../chunks/chunk-TPRZZHXA.js";
8
+ import "../chunks/chunk-5XGAGNH5.js";
9
9
  import "../chunks/chunk-IYVI5MSS.js";
10
- import "../chunks/chunk-6M3OG3E4.js";
10
+ import "../chunks/chunk-H7Z2FBRD.js";
11
+ import "../chunks/chunk-LNFMYJUJ.js";
11
12
  import "../chunks/chunk-5CD65KH3.js";
12
13
  import "../chunks/chunk-6L43PFIN.js";
13
- import "../chunks/chunk-LNFMYJUJ.js";
14
14
  import "../chunks/chunk-PDUQ3KMW.js";
15
15
  import "../chunks/chunk-BAZIS67B.js";
16
16
  import "../chunks/chunk-TC7FOG4T.js";
@@ -3,10 +3,10 @@ import {
3
3
  flatPlays,
4
4
  importPlaybook,
5
5
  mainPlay
6
- } from "../chunks/chunk-6M3OG3E4.js";
6
+ } from "../chunks/chunk-H7Z2FBRD.js";
7
+ import "../chunks/chunk-LNFMYJUJ.js";
7
8
  import "../chunks/chunk-5CD65KH3.js";
8
9
  import "../chunks/chunk-6L43PFIN.js";
9
- import "../chunks/chunk-LNFMYJUJ.js";
10
10
  import "../chunks/chunk-PDUQ3KMW.js";
11
11
  import "../chunks/chunk-BAZIS67B.js";
12
12
  import "../chunks/chunk-TC7FOG4T.js";
@@ -2,9 +2,9 @@ import {
2
2
  parseKv,
3
3
  readPlaybook,
4
4
  scanFolder
5
- } from "../chunks/chunk-QMIXDOU6.js";
6
- import "../chunks/chunk-6L43PFIN.js";
5
+ } from "../chunks/chunk-5XGAGNH5.js";
7
6
  import "../chunks/chunk-LNFMYJUJ.js";
7
+ import "../chunks/chunk-6L43PFIN.js";
8
8
  import "../chunks/chunk-TC7FOG4T.js";
9
9
  import "../chunks/chunk-OEQXGULI.js";
10
10
  import "../chunks/chunk-VHCA4A7K.js";
@@ -1,16 +1,6 @@
1
- import {
2
- dataLens
3
- } from "../chunks/chunk-BXHI5EQM.js";
4
1
  import {
5
2
  timingLens
6
3
  } from "../chunks/chunk-VEYR53BH.js";
7
- import {
8
- accessLens
9
- } from "../chunks/chunk-WS52COFG.js";
10
- import {
11
- declaredTarget,
12
- inventoryLens
13
- } from "../chunks/chunk-KWEFZFLC.js";
14
4
  import {
15
5
  LENSES
16
6
  } from "../chunks/chunk-XOPGOWNQ.js";
@@ -20,6 +10,16 @@ import {
20
10
  import {
21
11
  codeLens
22
12
  } from "../chunks/chunk-4N3AN5CJ.js";
13
+ import {
14
+ dataLens
15
+ } from "../chunks/chunk-BXHI5EQM.js";
16
+ import {
17
+ accessLens
18
+ } from "../chunks/chunk-WS52COFG.js";
19
+ import {
20
+ declaredTarget,
21
+ inventoryLens
22
+ } from "../chunks/chunk-KWEFZFLC.js";
23
23
  import {
24
24
  incoming,
25
25
  title
@@ -35,6 +35,8 @@ function messageProblem(m) {
35
35
  return isMap(m.request) ? void 0 : "scenarioEdit: bad request";
36
36
  case "runScenarios":
37
37
  return strList(m.names) ? void 0 : "runScenarios: bad names";
38
+ case "mergeChoose":
39
+ return str(m.id, 500) && (m.side === "ours" || m.side === "theirs") ? void 0 : "mergeChoose: bad choice";
38
40
  case "lens":
39
41
  return m.lens === null || str(m.lens, 64) ? void 0 : "lens: bad lens";
40
42
  default:
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/core/messageCheck.ts"],
4
- "sourcesContent": ["// AFD-165: the host checks what a designer webview sends before acting on it. The webview runs\n// under a strict CSP with no HTML sinks, so a compromised page is unlikely; these checks keep one\n// from turning a message into a git option, a file outside the workspace or a probe argument.\n// Shape only: what a value means (a known ref, a path inside the folder) is checked where it is\n// used.\n\nconst isMap = (v: unknown): v is Record<string, unknown> =>\n !!v && typeof v === 'object' && !Array.isArray(v);\n\nconst str = (v: unknown, max = 4096): v is string => typeof v === 'string' && v.length <= max;\nconst optNum = (v: unknown) => v === undefined || (typeof v === 'number' && Number.isFinite(v));\nconst strList = (v: unknown) =>\n v === undefined || (Array.isArray(v) && v.every((x) => str(x, 200)));\n\n/** A plugin name the probe looks up: dotted words, no option or path syntax. */\nexport const PLUGIN_NAME = /^[A-Za-z0-9_][A-Za-z0-9_.]{0,199}$/;\n\n/** A git revision a designer may ask to compare with: never an option (`-\u2026`), no spaces. */\nexport const GIT_REF = /^[A-Za-z0-9_][A-Za-z0-9_./@{}^~-]{0,199}$/;\n\nconst CATALOG_OPS = new Set(['search', 'doc', 'roles', 'playbooks', 'rebuild']);\n\nfunction catalogProblem(r: unknown): string | undefined {\n if (!isMap(r) || !CATALOG_OPS.has(String(r.op))) return 'catalog: unknown request';\n if (r.op === 'search' && (!str(r.query, 500) || !strList(r.types))) return 'catalog: bad search';\n if (r.op === 'doc' && (!str(r.fqcn) || !PLUGIN_NAME.test(r.fqcn)))\n return 'catalog: not a plugin name';\n return undefined;\n}\n\n/** Why a message from the webview is refused, or undefined when its shape is right. */\nexport function messageProblem(m: unknown): string | undefined {\n if (!isMap(m) || !str(m.type, 64)) return 'not a designer message';\n if ('reqId' in m && !optNum(m.reqId)) return `${m.type}: bad reqId`;\n switch (m.type) {\n case 'openCode':\n return str(m.path) && m.path !== '' && optNum(m.line) ? undefined : 'openCode: bad path';\n case 'openText':\n return m.path === undefined ||\n (Array.isArray(m.path) &&\n m.path.every((p) => str(p, 1000) || (typeof p === 'number' && Number.isInteger(p))))\n ? undefined\n : 'openText: bad path';\n case 'compareWith':\n return str(m.ref, 200) && GIT_REF.test(m.ref) ? undefined : 'compareWith: bad ref';\n case 'catalog':\n return catalogProblem(m.request);\n case 'openLink':\n return str(m.url) ? undefined : 'openLink: bad url';\n case 'copyText':\n return str(m.text, 10_000_000) && optNum(m.count) ? undefined : 'copyText: bad text';\n case 'command':\n return isMap(m.command) && str(m.command.kind, 64) ? undefined : 'command: bad command';\n case 'scenarioEdit':\n return isMap(m.request) ? undefined : 'scenarioEdit: bad request';\n case 'runScenarios':\n return strList(m.names) ? undefined : 'runScenarios: bad names';\n case 'lens':\n return m.lens === null || str(m.lens, 64) ? undefined : 'lens: bad lens';\n default:\n return undefined;\n }\n}\n"],
5
- "mappings": ";AAMA,IAAM,QAAQ,CAAC,MACb,CAAC,CAAC,KAAK,OAAO,MAAM,YAAY,CAAC,MAAM,QAAQ,CAAC;AAElD,IAAM,MAAM,CAAC,GAAY,MAAM,SAAsB,OAAO,MAAM,YAAY,EAAE,UAAU;AAC1F,IAAM,SAAS,CAAC,MAAe,MAAM,UAAc,OAAO,MAAM,YAAY,OAAO,SAAS,CAAC;AAC7F,IAAM,UAAU,CAAC,MACf,MAAM,UAAc,MAAM,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,IAAI,GAAG,GAAG,CAAC;AAG7D,IAAM,cAAc;AAGpB,IAAM,UAAU;AAEvB,IAAM,cAAc,oBAAI,IAAI,CAAC,UAAU,OAAO,SAAS,aAAa,SAAS,CAAC;AAE9E,SAAS,eAAe,GAAgC;AACtD,MAAI,CAAC,MAAM,CAAC,KAAK,CAAC,YAAY,IAAI,OAAO,EAAE,EAAE,CAAC,EAAG,QAAO;AACxD,MAAI,EAAE,OAAO,aAAa,CAAC,IAAI,EAAE,OAAO,GAAG,KAAK,CAAC,QAAQ,EAAE,KAAK,GAAI,QAAO;AAC3E,MAAI,EAAE,OAAO,UAAU,CAAC,IAAI,EAAE,IAAI,KAAK,CAAC,YAAY,KAAK,EAAE,IAAI;AAC7D,WAAO;AACT,SAAO;AACT;AAGO,SAAS,eAAe,GAAgC;AAC7D,MAAI,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,EAAG,QAAO;AAC1C,MAAI,WAAW,KAAK,CAAC,OAAO,EAAE,KAAK,EAAG,QAAO,GAAG,EAAE,IAAI;AACtD,UAAQ,EAAE,MAAM;AAAA,IACd,KAAK;AACH,aAAO,IAAI,EAAE,IAAI,KAAK,EAAE,SAAS,MAAM,OAAO,EAAE,IAAI,IAAI,SAAY;AAAA,IACtE,KAAK;AACH,aAAO,EAAE,SAAS,UACf,MAAM,QAAQ,EAAE,IAAI,KACnB,EAAE,KAAK,MAAM,CAAC,MAAM,IAAI,GAAG,GAAI,KAAM,OAAO,MAAM,YAAY,OAAO,UAAU,CAAC,CAAE,IAClF,SACA;AAAA,IACN,KAAK;AACH,aAAO,IAAI,EAAE,KAAK,GAAG,KAAK,QAAQ,KAAK,EAAE,GAAG,IAAI,SAAY;AAAA,IAC9D,KAAK;AACH,aAAO,eAAe,EAAE,OAAO;AAAA,IACjC,KAAK;AACH,aAAO,IAAI,EAAE,GAAG,IAAI,SAAY;AAAA,IAClC,KAAK;AACH,aAAO,IAAI,EAAE,MAAM,GAAU,KAAK,OAAO,EAAE,KAAK,IAAI,SAAY;AAAA,IAClE,KAAK;AACH,aAAO,MAAM,EAAE,OAAO,KAAK,IAAI,EAAE,QAAQ,MAAM,EAAE,IAAI,SAAY;AAAA,IACnE,KAAK;AACH,aAAO,MAAM,EAAE,OAAO,IAAI,SAAY;AAAA,IACxC,KAAK;AACH,aAAO,QAAQ,EAAE,KAAK,IAAI,SAAY;AAAA,IACxC,KAAK;AACH,aAAO,EAAE,SAAS,QAAQ,IAAI,EAAE,MAAM,EAAE,IAAI,SAAY;AAAA,IAC1D;AACE,aAAO;AAAA,EACX;AACF;",
4
+ "sourcesContent": ["// AFD-165: the host checks what a designer webview sends before acting on it. The webview runs\n// under a strict CSP with no HTML sinks, so a compromised page is unlikely; these checks keep one\n// from turning a message into a git option, a file outside the workspace or a probe argument.\n// Shape only: what a value means (a known ref, a path inside the folder) is checked where it is\n// used.\n\nconst isMap = (v: unknown): v is Record<string, unknown> =>\n !!v && typeof v === 'object' && !Array.isArray(v);\n\nconst str = (v: unknown, max = 4096): v is string => typeof v === 'string' && v.length <= max;\nconst optNum = (v: unknown) => v === undefined || (typeof v === 'number' && Number.isFinite(v));\nconst strList = (v: unknown) =>\n v === undefined || (Array.isArray(v) && v.every((x) => str(x, 200)));\n\n/** A plugin name the probe looks up: dotted words, no option or path syntax. */\nexport const PLUGIN_NAME = /^[A-Za-z0-9_][A-Za-z0-9_.]{0,199}$/;\n\n/** A git revision a designer may ask to compare with: never an option (`-\u2026`), no spaces. */\nexport const GIT_REF = /^[A-Za-z0-9_][A-Za-z0-9_./@{}^~-]{0,199}$/;\n\nconst CATALOG_OPS = new Set(['search', 'doc', 'roles', 'playbooks', 'rebuild']);\n\nfunction catalogProblem(r: unknown): string | undefined {\n if (!isMap(r) || !CATALOG_OPS.has(String(r.op))) return 'catalog: unknown request';\n if (r.op === 'search' && (!str(r.query, 500) || !strList(r.types))) return 'catalog: bad search';\n if (r.op === 'doc' && (!str(r.fqcn) || !PLUGIN_NAME.test(r.fqcn)))\n return 'catalog: not a plugin name';\n return undefined;\n}\n\n/** Why a message from the webview is refused, or undefined when its shape is right. */\nexport function messageProblem(m: unknown): string | undefined {\n if (!isMap(m) || !str(m.type, 64)) return 'not a designer message';\n if ('reqId' in m && !optNum(m.reqId)) return `${m.type}: bad reqId`;\n switch (m.type) {\n case 'openCode':\n return str(m.path) && m.path !== '' && optNum(m.line) ? undefined : 'openCode: bad path';\n case 'openText':\n return m.path === undefined ||\n (Array.isArray(m.path) &&\n m.path.every((p) => str(p, 1000) || (typeof p === 'number' && Number.isInteger(p))))\n ? undefined\n : 'openText: bad path';\n case 'compareWith':\n return str(m.ref, 200) && GIT_REF.test(m.ref) ? undefined : 'compareWith: bad ref';\n case 'catalog':\n return catalogProblem(m.request);\n case 'openLink':\n return str(m.url) ? undefined : 'openLink: bad url';\n case 'copyText':\n return str(m.text, 10_000_000) && optNum(m.count) ? undefined : 'copyText: bad text';\n case 'command':\n return isMap(m.command) && str(m.command.kind, 64) ? undefined : 'command: bad command';\n case 'scenarioEdit':\n return isMap(m.request) ? undefined : 'scenarioEdit: bad request';\n case 'runScenarios':\n return strList(m.names) ? undefined : 'runScenarios: bad names';\n case 'mergeChoose':\n return str(m.id, 500) && (m.side === 'ours' || m.side === 'theirs')\n ? undefined\n : 'mergeChoose: bad choice';\n case 'lens':\n return m.lens === null || str(m.lens, 64) ? undefined : 'lens: bad lens';\n default:\n return undefined;\n }\n}\n"],
5
+ "mappings": ";AAMA,IAAM,QAAQ,CAAC,MACb,CAAC,CAAC,KAAK,OAAO,MAAM,YAAY,CAAC,MAAM,QAAQ,CAAC;AAElD,IAAM,MAAM,CAAC,GAAY,MAAM,SAAsB,OAAO,MAAM,YAAY,EAAE,UAAU;AAC1F,IAAM,SAAS,CAAC,MAAe,MAAM,UAAc,OAAO,MAAM,YAAY,OAAO,SAAS,CAAC;AAC7F,IAAM,UAAU,CAAC,MACf,MAAM,UAAc,MAAM,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,IAAI,GAAG,GAAG,CAAC;AAG7D,IAAM,cAAc;AAGpB,IAAM,UAAU;AAEvB,IAAM,cAAc,oBAAI,IAAI,CAAC,UAAU,OAAO,SAAS,aAAa,SAAS,CAAC;AAE9E,SAAS,eAAe,GAAgC;AACtD,MAAI,CAAC,MAAM,CAAC,KAAK,CAAC,YAAY,IAAI,OAAO,EAAE,EAAE,CAAC,EAAG,QAAO;AACxD,MAAI,EAAE,OAAO,aAAa,CAAC,IAAI,EAAE,OAAO,GAAG,KAAK,CAAC,QAAQ,EAAE,KAAK,GAAI,QAAO;AAC3E,MAAI,EAAE,OAAO,UAAU,CAAC,IAAI,EAAE,IAAI,KAAK,CAAC,YAAY,KAAK,EAAE,IAAI;AAC7D,WAAO;AACT,SAAO;AACT;AAGO,SAAS,eAAe,GAAgC;AAC7D,MAAI,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,EAAG,QAAO;AAC1C,MAAI,WAAW,KAAK,CAAC,OAAO,EAAE,KAAK,EAAG,QAAO,GAAG,EAAE,IAAI;AACtD,UAAQ,EAAE,MAAM;AAAA,IACd,KAAK;AACH,aAAO,IAAI,EAAE,IAAI,KAAK,EAAE,SAAS,MAAM,OAAO,EAAE,IAAI,IAAI,SAAY;AAAA,IACtE,KAAK;AACH,aAAO,EAAE,SAAS,UACf,MAAM,QAAQ,EAAE,IAAI,KACnB,EAAE,KAAK,MAAM,CAAC,MAAM,IAAI,GAAG,GAAI,KAAM,OAAO,MAAM,YAAY,OAAO,UAAU,CAAC,CAAE,IAClF,SACA;AAAA,IACN,KAAK;AACH,aAAO,IAAI,EAAE,KAAK,GAAG,KAAK,QAAQ,KAAK,EAAE,GAAG,IAAI,SAAY;AAAA,IAC9D,KAAK;AACH,aAAO,eAAe,EAAE,OAAO;AAAA,IACjC,KAAK;AACH,aAAO,IAAI,EAAE,GAAG,IAAI,SAAY;AAAA,IAClC,KAAK;AACH,aAAO,IAAI,EAAE,MAAM,GAAU,KAAK,OAAO,EAAE,KAAK,IAAI,SAAY;AAAA,IAClE,KAAK;AACH,aAAO,MAAM,EAAE,OAAO,KAAK,IAAI,EAAE,QAAQ,MAAM,EAAE,IAAI,SAAY;AAAA,IACnE,KAAK;AACH,aAAO,MAAM,EAAE,OAAO,IAAI,SAAY;AAAA,IACxC,KAAK;AACH,aAAO,QAAQ,EAAE,KAAK,IAAI,SAAY;AAAA,IACxC,KAAK;AACH,aAAO,IAAI,EAAE,IAAI,GAAG,MAAM,EAAE,SAAS,UAAU,EAAE,SAAS,YACtD,SACA;AAAA,IACN,KAAK;AACH,aAAO,EAAE,SAAS,QAAQ,IAAI,EAAE,MAAM,EAAE,IAAI,SAAY;AAAA,IAC1D;AACE,aAAO;AAAA,EACX;AACF;",
6
6
  "names": []
7
7
  }
@@ -1,6 +1,7 @@
1
1
  import type { CatalogOption, CatalogPlugin, PluginType, SearchHit } from './catalog/types';
2
2
  import type { CodeScan, DriftAction } from './drift/types';
3
3
  import type { FlowCommand } from './flow/commands';
4
+ import type { MergeConflict, MergeSide } from './flow/merge';
4
5
  import type { OpenPort } from './flow/openPorts';
5
6
  import type { FlowDocument, Position, Ulid } from './flow/types';
6
7
  import type { Finding } from './flow/validate';
@@ -347,6 +348,11 @@ export type HostMessage = {
347
348
  type: 'outsideMarks';
348
349
  marks: OutsideMarks | null;
349
350
  }
351
+ /** AFD-283: a merge of this flow waiting for choices (git left it to the designer); null: none. */
352
+ | {
353
+ type: 'mergeConflicts';
354
+ merge: MergeView | null;
355
+ }
350
356
  /** AFD-93: the workspace's flows at a glance, for the flow panel's line. */
351
357
  | {
352
358
  type: 'repoState';
@@ -489,6 +495,16 @@ export type WebviewMessage = {
489
495
  | {
490
496
  type: 'clearOutside';
491
497
  }
498
+ /** AFD-283: a side chosen for one conflict of the waiting merge. */
499
+ | {
500
+ type: 'mergeChoose';
501
+ id: string;
502
+ side: MergeSide;
503
+ }
504
+ /** AFD-283: the waiting merge is done: the file is taken as merged. */
505
+ | {
506
+ type: 'mergeFinish';
507
+ }
492
508
  /** AFD-65: a link in an annotation; the host opens web links this flow's annotations hold. */
493
509
  | {
494
510
  type: 'openLink';
@@ -646,6 +662,8 @@ export interface DomSummary {
646
662
  banner: number | null;
647
663
  /** AFD-19: items the written-by-hand banner says need IDs (null: no banner). */
648
664
  idGaps: number | null;
665
+ /** AFD-283: conflicts the Merge note lists (null: no note). */
666
+ merge?: number | null;
649
667
  /** Inside a subflow: frame ports shown (enabled ones) and boundary wires drawn. */
650
668
  frame: {
651
669
  ports: string[];
@@ -666,3 +684,13 @@ export interface DomSummary {
666
684
  card: string[];
667
685
  };
668
686
  }
687
+ /** AFD-283: a merge git left to the designer: its conflicts with what is chosen for each. */
688
+ export interface MergeView {
689
+ /** The file's path in the repository. */
690
+ file: string;
691
+ conflicts: (MergeConflict & {
692
+ chosen: MergeSide;
693
+ })[];
694
+ /** Rules the merged flow breaks that neither side broke. */
695
+ broken: string[];
696
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ansible-flow/core",
3
- "version": "0.85.0",
3
+ "version": "0.86.0",
4
4
  "description": "Ansible Flow Designer core: the flow model, commands, validation and playbook generation, for any JavaScript host.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://github.com/ChistokhinSV/ansible-flow-designer/blob/main/docs/packages.md",