@syntax-syllogism/flow-delta 0.2.1 → 0.3.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 (2) hide show
  1. package/dist/cli.js +618 -311
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -714,10 +714,10 @@ function classifyNode(oldNode, newNode) {
714
714
  throw new Error("Cannot classify absent node");
715
715
  }
716
716
  if (!oldNode) {
717
- return { id: newNode.id, type: newNode.type, label: newNode.label, status: "added" };
717
+ return { id: newNode.id, type: newNode.type, label: newNode.label, status: "added", after: newNode.properties };
718
718
  }
719
719
  if (!newNode) {
720
- return { id: oldNode.id, type: oldNode.type, label: oldNode.label, status: "deleted" };
720
+ return { id: oldNode.id, type: oldNode.type, label: oldNode.label, status: "deleted", before: oldNode.properties };
721
721
  }
722
722
  const changes = [
723
723
  ...deepDiff(oldNode.label, newNode.label, "label"),
@@ -880,7 +880,37 @@ var PARAM_COLUMNS = [
880
880
  { path: "value", label: "Value" },
881
881
  { path: "assignToReference", label: "Assign To" }
882
882
  ];
883
+ var SCREEN_SETTING_PATHS = [
884
+ "allowBack",
885
+ "allowFinish",
886
+ "allowPause",
887
+ "showFooter",
888
+ "showHeader"
889
+ ];
883
890
  var SECTION_SCHEMAS = {
891
+ screen: [
892
+ { name: "Screen Settings", paths: SCREEN_SETTING_PATHS, render: "lines" }
893
+ ],
894
+ start: [
895
+ {
896
+ name: "Trigger",
897
+ paths: ["object", "triggerType", "recordTriggerType", "schedule", "filterLogic"],
898
+ render: "lines"
899
+ },
900
+ { name: "Entry Conditions", paths: ["filters"], render: "table", columns: FILTER_COLUMNS },
901
+ {
902
+ name: "Scheduled Paths",
903
+ paths: ["scheduledPaths"],
904
+ render: "table",
905
+ columns: [
906
+ { path: "name", label: "Name" },
907
+ { path: "label", label: "Label" },
908
+ { path: "offsetNumber", label: "Offset" },
909
+ { path: "offsetUnit", label: "Unit" },
910
+ { path: "timeSource", label: "Time Source" }
911
+ ]
912
+ }
913
+ ],
884
914
  decision: [{
885
915
  name: "Outcomes",
886
916
  paths: ["rules"],
@@ -907,6 +937,7 @@ var SECTION_SCHEMAS = {
907
937
  recordUpdate: [{ name: "Field Mappings", paths: ["inputAssignments"], render: "table", columns: MAPPING_COLUMNS }],
908
938
  recordLookup: [{ name: "Filters", paths: ["filters"], render: "table", columns: FILTER_COLUMNS }],
909
939
  recordDelete: [{ name: "Filters", paths: ["filters"], render: "table", columns: FILTER_COLUMNS }],
940
+ subflow: [{ name: "Parameters", paths: ["inputAssignments", "outputAssignments"], render: "table", columns: PARAM_COLUMNS }],
910
941
  actionCall: [{ name: "Parameters", paths: ["inputParameters", "outputParameters"], render: "table", columns: PARAM_COLUMNS }],
911
942
  apexPluginCall: [{ name: "Parameters", paths: ["inputParameters", "outputParameters"], render: "table", columns: PARAM_COLUMNS }],
912
943
  wait: [{
@@ -919,12 +950,571 @@ var SECTION_SCHEMAS = {
919
950
  { path: "offsetUnit", label: "Unit" },
920
951
  { path: "resumeDateReference", label: "Resume Date" }
921
952
  ]
922
- }]
953
+ }],
954
+ customError: [{
955
+ name: "Error Messages",
956
+ paths: ["customErrorMessages"],
957
+ render: "table",
958
+ columns: [
959
+ { path: "name", label: "Name" },
960
+ { path: "errorMessage", label: "Message" }
961
+ ]
962
+ }],
963
+ transform: [
964
+ {
965
+ name: "Value Mappings",
966
+ paths: ["transformValues", "valueMappings"],
967
+ render: "table",
968
+ columns: [
969
+ { path: "name", label: "Name" },
970
+ { path: "sourceDataType", label: "Source Type" },
971
+ { path: "targetDataType", label: "Target Type" },
972
+ { path: "value", label: "Value" }
973
+ ]
974
+ },
975
+ {
976
+ name: "Data Type Mappings",
977
+ paths: ["dataTypeMappings"],
978
+ render: "table",
979
+ columns: [
980
+ { path: "name", label: "Name" },
981
+ { path: "sourceDataType", label: "Source Type" },
982
+ { path: "targetDataType", label: "Target Type" }
983
+ ]
984
+ }
985
+ ],
986
+ collectionProcessor: [
987
+ { name: "Collection Settings", paths: ["collectionReference", "operation"], render: "lines" },
988
+ { name: "Filters", paths: ["filters"], render: "table", columns: FILTER_COLUMNS },
989
+ {
990
+ name: "Sort Options",
991
+ paths: ["sortOptions"],
992
+ render: "table",
993
+ columns: [
994
+ { path: "field", label: "Field" },
995
+ { path: "sortOrder", label: "Order" }
996
+ ]
997
+ }
998
+ ],
999
+ orchestratedStage: [
1000
+ { name: "Stage Settings", paths: ["stageLabel", "stageOrder"], render: "lines" }
1001
+ ]
923
1002
  };
924
1003
  function getSectionSchemas(type) {
925
1004
  return SECTION_SCHEMAS[type] ?? [];
926
1005
  }
927
1006
 
1007
+ // src/render/snapshot-panel.ts
1008
+ var LONG_TEXT_THRESHOLD = 160;
1009
+ var COLLECTION_KEY_NAMES = [
1010
+ "assignmentItems",
1011
+ "choiceReferences",
1012
+ "choices",
1013
+ "conditions",
1014
+ "customErrorMessages",
1015
+ "dataTypeMappings",
1016
+ "fields",
1017
+ "filters",
1018
+ "inputAssignments",
1019
+ "inputParameters",
1020
+ "outputParameters",
1021
+ "outputAssignments",
1022
+ "processMetadataValues",
1023
+ "rules",
1024
+ "scheduledPaths",
1025
+ "sortOptions",
1026
+ "stageSteps",
1027
+ "steps",
1028
+ "storeOutputParameters",
1029
+ "transformValues",
1030
+ "valueMappings",
1031
+ "waitEvents"
1032
+ ];
1033
+ var COLLECTION_KEYS = new Set(COLLECTION_KEY_NAMES);
1034
+ function renderNodePanelBody(node, schemas = []) {
1035
+ if (node.status === "unchanged") {
1036
+ return "<div class='empty'>No property changes.</div>";
1037
+ }
1038
+ const before = node.status === "added" ? void 0 : node.before;
1039
+ const after = node.status === "deleted" ? void 0 : node.after;
1040
+ const options = { showUnchanged: node.status !== "modified", nodeStatus: node.status };
1041
+ const sections = renderSections(before, after, schemas || [], options);
1042
+ if (!sections.length) {
1043
+ return "<div class='empty'>No configuration.</div>";
1044
+ }
1045
+ return sections.map((section, index) => {
1046
+ const open = node.status === "modified" || index === 0 ? " open" : "";
1047
+ return "<details class='detail-section snapshot-section " + node.status + "'" + open + "><summary>" + escapeHtml(section.name) + " <span class='section-count'>(" + section.count + ")</span></summary><div class='section-body'>" + section.html + "</div></details>";
1048
+ }).join("");
1049
+ }
1050
+ function renderSections(before, after, schemas, options) {
1051
+ const sections = [];
1052
+ const owned = /* @__PURE__ */ new Set();
1053
+ for (const schema of schemas) {
1054
+ for (const path of schema.paths) owned.add(path);
1055
+ const rendered = renderSchemaSection(before, after, schema, options);
1056
+ if (rendered) sections.push(rendered);
1057
+ }
1058
+ const keys = unionKeys(before, after).filter((key) => !ownsAny(owned, key));
1059
+ const settingsRows = [];
1060
+ let settingsCount = 0;
1061
+ const nestedSections = [];
1062
+ for (const key of keys) {
1063
+ const childBefore = getField(before, key);
1064
+ const childAfter = getField(after, key);
1065
+ const childChanged = hasChange(childBefore, childAfter, key);
1066
+ const scalar = scalarPair(childBefore, childAfter);
1067
+ if (scalar) {
1068
+ if (options.showUnchanged || childChanged) {
1069
+ settingsRows.push(scalarRow(key, scalar.before, scalar.after, statusOf(childBefore, childAfter), options.nodeStatus));
1070
+ }
1071
+ if (childChanged) settingsCount += 1;
1072
+ continue;
1073
+ }
1074
+ const rendered = renderValue(getField(before, key), getField(after, key), {
1075
+ ...options,
1076
+ label: humanizePath(key),
1077
+ pathKey: key,
1078
+ depth: 0
1079
+ });
1080
+ if (rendered.html) {
1081
+ nestedSections.push({ name: humanizePath(key), html: rendered.html, count: rendered.count });
1082
+ }
1083
+ }
1084
+ if (settingsRows.length) {
1085
+ sections.push({
1086
+ name: "Settings",
1087
+ html: "<table class='snapshot-kv'><tbody>" + settingsRows.join("") + "</tbody></table>",
1088
+ count: settingsCount
1089
+ });
1090
+ }
1091
+ sections.push(...nestedSections);
1092
+ return sections;
1093
+ }
1094
+ function renderSchemaSection(before, after, schema, options) {
1095
+ let rendered;
1096
+ if (schema.render === "table" && schema.columns) {
1097
+ rendered = renderTable(before, after, schema.paths, schema.columns, options);
1098
+ } else if (schema.render === "grouped-table" && schema.innerArray && schema.innerColumns) {
1099
+ rendered = renderGroupedTable(before, after, schema, options);
1100
+ } else {
1101
+ const sectionBefore = pickSectionObject(before, schema.paths);
1102
+ const sectionAfter = pickSectionObject(after, schema.paths);
1103
+ rendered = renderValue(sectionBefore, sectionAfter, {
1104
+ ...options,
1105
+ label: schema.name,
1106
+ pathKey: schema.paths[0] || schema.name,
1107
+ depth: 0
1108
+ });
1109
+ }
1110
+ if (!rendered.html) return null;
1111
+ return { name: schema.name, html: rendered.html, count: rendered.count };
1112
+ }
1113
+ function renderValue(before, after, ctx) {
1114
+ const changed = hasChange(before, after, ctx.pathKey);
1115
+ if (!ctx.showUnchanged && !changed) return { html: "", changed: false, count: 0 };
1116
+ const scalar = scalarPair(before, after);
1117
+ if (scalar) {
1118
+ return {
1119
+ html: renderLine(ctx.label, scalar.before, scalar.after, statusOf(before, after), ctx.nodeStatus),
1120
+ changed,
1121
+ count: changed ? 1 : 0
1122
+ };
1123
+ }
1124
+ if (isCollection(before, after, ctx.pathKey)) {
1125
+ const rendered2 = renderArray(before, after, ctx);
1126
+ return { html: rendered2.html, changed, count: rendered2.count || (changed ? 1 : 0) };
1127
+ }
1128
+ const rendered = renderObject(before, after, ctx);
1129
+ return { html: rendered.html, changed, count: rendered.count || (changed ? 1 : 0) };
1130
+ }
1131
+ function renderObject(before, after, ctx) {
1132
+ const beforeObj = isObj(before) ? before : {};
1133
+ const afterObj = isObj(after) ? after : {};
1134
+ const keys = unionKeys(beforeObj, afterObj);
1135
+ const scalarRows = [];
1136
+ const nested = [];
1137
+ let count = 0;
1138
+ for (const key of keys) {
1139
+ const childBefore = getField(beforeObj, key);
1140
+ const childAfter = getField(afterObj, key);
1141
+ const childChanged = hasChange(childBefore, childAfter, key);
1142
+ const scalar = scalarPair(childBefore, childAfter);
1143
+ if (scalar) {
1144
+ if (ctx.showUnchanged || childChanged || ctx.depth > 0) {
1145
+ scalarRows.push(scalarRow(key, scalar.before, scalar.after, statusOf(childBefore, childAfter), ctx.nodeStatus));
1146
+ }
1147
+ if (childChanged) count += 1;
1148
+ continue;
1149
+ }
1150
+ const rendered = renderValue(childBefore, childAfter, {
1151
+ ...ctx,
1152
+ label: humanizePath(key),
1153
+ pathKey: key,
1154
+ depth: ctx.depth + 1
1155
+ });
1156
+ if (rendered.html) {
1157
+ nested.push("<div class='snapshot-nested'><div class='snapshot-nested-title'>" + escapeHtml(humanizePath(key)) + "</div>" + rendered.html + "</div>");
1158
+ count += rendered.count;
1159
+ }
1160
+ }
1161
+ const table = scalarRows.length ? "<table class='snapshot-kv'><tbody>" + scalarRows.join("") + "</tbody></table>" : "";
1162
+ return { html: table + nested.join(""), changed: count > 0, count };
1163
+ }
1164
+ function renderArray(before, after, ctx) {
1165
+ const beforeItems = arr(before);
1166
+ const afterItems = arr(after);
1167
+ if (looksLikeNameValueArray(beforeItems, afterItems)) {
1168
+ return renderTableFromItems(before, after, [""], [
1169
+ { path: "name", label: "Name" },
1170
+ { path: "value", label: "Value" }
1171
+ ], ctx);
1172
+ }
1173
+ const max = Math.max(beforeItems.length, afterItems.length);
1174
+ const cards = [];
1175
+ let count = 0;
1176
+ for (let i = 0; i < max; i += 1) {
1177
+ const childBefore = beforeItems[i];
1178
+ const childAfter = afterItems[i];
1179
+ const childChanged = hasChange(childBefore, childAfter, "");
1180
+ if (!ctx.showUnchanged && !childChanged) continue;
1181
+ const title = itemTitle(childBefore, childAfter, i);
1182
+ const rendered = renderValue(childBefore, childAfter, {
1183
+ ...ctx,
1184
+ label: title,
1185
+ pathKey: "",
1186
+ depth: ctx.depth + 1
1187
+ });
1188
+ if (!rendered.html) continue;
1189
+ count += rendered.count || (childChanged ? 1 : 0);
1190
+ cards.push("<div class='snapshot-card " + statusOf(childBefore, childAfter) + "'><div class='snapshot-card-title'>" + escapeHtml(title) + "</div>" + rendered.html + "</div>");
1191
+ }
1192
+ return { html: cards.join(""), changed: count > 0, count };
1193
+ }
1194
+ function renderTable(before, after, paths, columns, options) {
1195
+ return renderTableFromItems(before, after, paths, columns, { ...options, label: "", pathKey: paths[0] || "", depth: 0 });
1196
+ }
1197
+ function renderTableFromItems(before, after, paths, columns, options) {
1198
+ const rows = [];
1199
+ for (const path of paths) {
1200
+ const beforeItems = arr(resolvePath(before, path));
1201
+ const afterItems = arr(resolvePath(after, path));
1202
+ const max = Math.max(beforeItems.length, afterItems.length);
1203
+ for (let i = 0; i < max; i += 1) {
1204
+ const itemBefore = beforeItems[i];
1205
+ const itemAfter = afterItems[i];
1206
+ if (!options.showUnchanged && !hasChange(itemBefore, itemAfter, path)) continue;
1207
+ const cells = {};
1208
+ for (const col of columns) {
1209
+ const cellBefore = resolvePath(itemBefore, col.path);
1210
+ const cellAfter = resolvePath(itemAfter, col.path);
1211
+ cells[col.path] = {
1212
+ before: unwrapValue(cellBefore),
1213
+ after: unwrapValue(cellAfter),
1214
+ changed: hasChange(cellBefore, cellAfter, col.path)
1215
+ };
1216
+ }
1217
+ rows.push({ idx: i, kind: statusOf(itemBefore, itemAfter), cells });
1218
+ }
1219
+ }
1220
+ if (!rows.length) return { html: "", changed: false, count: 0 };
1221
+ const showChange = options.nodeStatus === "modified" && !options.suppressChangeColumn;
1222
+ const head = "<tr><th class='row-idx'>#</th>" + columns.map((col) => "<th>" + escapeHtml(col.label) + "</th>").join("") + (showChange ? "<th>Change</th>" : "") + "</tr>";
1223
+ const body = rows.map((row) => renderRow(row, columns, showChange, options.nodeStatus)).join("");
1224
+ return {
1225
+ html: "<table class='change-table'><thead>" + head + "</thead><tbody>" + body + "</tbody></table>",
1226
+ changed: rows.some((row) => row.kind !== "unchanged" || Object.values(row.cells).some((cell) => cell.changed)),
1227
+ count: rows.filter((row) => row.kind !== "unchanged" || Object.values(row.cells).some((cell) => cell.changed)).length
1228
+ };
1229
+ }
1230
+ function renderRow(row, columns, showChange, nodeStatus) {
1231
+ const cells = columns.map((col) => {
1232
+ const cell = row.cells[col.path];
1233
+ return "<td>" + renderValueCell(cell.before, cell.after, cell.changed ? row.kind : "unchanged", nodeStatus) + "</td>";
1234
+ }).join("");
1235
+ const badge = showChange ? "<td><span class='change-kind " + row.kind + "'>" + capitalize(row.kind) + "</span></td>" : "";
1236
+ return "<tr class='row-" + row.kind + "'><td class='row-idx'>" + (row.idx + 1) + "</td>" + cells + badge + "</tr>";
1237
+ }
1238
+ function renderGroupedTable(before, after, schema, options) {
1239
+ const groups = [];
1240
+ let count = 0;
1241
+ for (const path of schema.paths) {
1242
+ const beforeGroups = arr(resolvePath(before, path));
1243
+ const afterGroups = arr(resolvePath(after, path));
1244
+ const max = Math.max(beforeGroups.length, afterGroups.length);
1245
+ for (let i = 0; i < max; i += 1) {
1246
+ const groupBefore = beforeGroups[i];
1247
+ const groupAfter = afterGroups[i];
1248
+ if (!options.showUnchanged && !hasChange(groupBefore, groupAfter, path)) continue;
1249
+ const kind = statusOf(groupBefore, groupAfter);
1250
+ const present = groupAfter !== void 0 ? groupAfter : groupBefore;
1251
+ const labelValue = schema.groupLabelPath && present ? resolvePath(present, schema.groupLabelPath) : void 0;
1252
+ const label = labelValue != null ? stripHtml(String(labelValue)) : "Outcome " + (i + 1);
1253
+ const scalarRows = renderGroupScalars(groupBefore, groupAfter, schema, options);
1254
+ const inner = renderTableFromItems(groupBefore, groupAfter, [schema.innerArray || ""], schema.innerColumns || [], {
1255
+ ...options,
1256
+ suppressChangeColumn: kind !== "modified",
1257
+ label,
1258
+ pathKey: schema.innerArray || "",
1259
+ depth: 1
1260
+ }).html;
1261
+ const head = "<div class='group-head'><span class='change-kind " + kind + "'>" + capitalize(kind) + "</span><span class='group-label'>" + escapeHtml(label) + "</span></div>";
1262
+ groups.push("<div class='outcome-group'>" + head + scalarRows + inner + "</div>");
1263
+ count += 1;
1264
+ }
1265
+ }
1266
+ return { html: groups.join(""), changed: count > 0, count };
1267
+ }
1268
+ function renderGroupScalars(before, after, schema, options) {
1269
+ const beforeObj = isObj(before) ? before : {};
1270
+ const afterObj = isObj(after) ? after : {};
1271
+ const skip = new Set([schema.innerArray, schema.groupLabelPath].filter(Boolean));
1272
+ const rows = unionKeys(beforeObj, afterObj).filter((key) => !skip.has(key)).map((key) => {
1273
+ const cellBefore = getField(beforeObj, key);
1274
+ const cellAfter = getField(afterObj, key);
1275
+ const scalar = scalarPair(cellBefore, cellAfter);
1276
+ if (!scalar) return "";
1277
+ if (!options.showUnchanged && !hasChange(cellBefore, cellAfter, key)) return "";
1278
+ return scalarRow(key, scalar.before, scalar.after, statusOf(cellBefore, cellAfter), options.nodeStatus);
1279
+ }).filter(Boolean).join("");
1280
+ return rows ? "<table class='snapshot-kv'><tbody>" + rows + "</tbody></table>" : "";
1281
+ }
1282
+ function scalarRow(key, before, after, kind, nodeStatus) {
1283
+ return "<tr><th>" + escapeHtml(humanizePath(key)) + "</th><td>" + renderValueCell(before, after, kind, nodeStatus) + "</td></tr>";
1284
+ }
1285
+ function renderLine(label, before, after, kind, nodeStatus) {
1286
+ const badge = nodeStatus === "modified" ? "<span class='change-kind " + kind + "'>" + capitalize(kind) + "</span>" : "";
1287
+ return "<ul class='changes'><li><div class='change-line-head'>" + badge + "<span class='change-line-label'>" + escapeHtml(label) + "</span></div><div class='change-line-value'>" + renderValueCell(before, after, kind, nodeStatus) + "</div></li></ul>";
1288
+ }
1289
+ function renderValueCell(before, after, kind, nodeStatus) {
1290
+ if (nodeStatus === "added") return oneSidedVal(after !== void 0 ? after : before);
1291
+ if (nodeStatus === "deleted") return oneSidedVal(before !== void 0 ? before : after);
1292
+ if (kind === "added") return insVal(after !== void 0 ? after : before);
1293
+ if (kind === "removed") return delVal(before !== void 0 ? before : after);
1294
+ if (kind === "modified") return diffValue(before, after);
1295
+ return faintVal(after !== void 0 ? after : before);
1296
+ }
1297
+ function statusOf(before, after) {
1298
+ if (before === void 0) return "added";
1299
+ if (after === void 0) return "removed";
1300
+ return valuesEqual(before, after) ? "unchanged" : "modified";
1301
+ }
1302
+ function hasChange(before, after, pathKey = "") {
1303
+ if (before === void 0 || after === void 0) return before !== after;
1304
+ const scalar = scalarPair(before, after);
1305
+ if (scalar) return !valuesEqual(scalar.before, scalar.after);
1306
+ if (isCollection(before, after, pathKey)) {
1307
+ const beforeItems = arr(before);
1308
+ const afterItems = arr(after);
1309
+ const max = Math.max(beforeItems.length, afterItems.length);
1310
+ for (let i = 0; i < max; i += 1) {
1311
+ if (hasChange(beforeItems[i], afterItems[i], "")) return true;
1312
+ }
1313
+ return false;
1314
+ }
1315
+ const beforeObj = isObj(before) ? before : {};
1316
+ const afterObj = isObj(after) ? after : {};
1317
+ for (const key of unionKeys(beforeObj, afterObj)) {
1318
+ if (hasChange(beforeObj[key], afterObj[key], key)) return true;
1319
+ }
1320
+ return false;
1321
+ }
1322
+ function scalarPair(before, after) {
1323
+ const beforeUnwrapped = unwrapValue(before);
1324
+ const afterUnwrapped = unwrapValue(after);
1325
+ const beforeScalar = beforeUnwrapped == null || typeof beforeUnwrapped !== "object";
1326
+ const afterScalar = afterUnwrapped == null || typeof afterUnwrapped !== "object";
1327
+ return beforeScalar && afterScalar ? { before: beforeUnwrapped, after: afterUnwrapped } : null;
1328
+ }
1329
+ function arr(value) {
1330
+ if (value === void 0 || value === null) return [];
1331
+ return Array.isArray(value) ? value : [value];
1332
+ }
1333
+ function isObj(value) {
1334
+ return !!value && typeof value === "object" && !Array.isArray(value);
1335
+ }
1336
+ function isCollection(before, after, pathKey) {
1337
+ return Array.isArray(before) || Array.isArray(after) || isCollectionKey(pathKey);
1338
+ }
1339
+ function isCollectionKey(key) {
1340
+ return COLLECTION_KEYS.has(key);
1341
+ }
1342
+ function unionKeys(before, after) {
1343
+ return [.../* @__PURE__ */ new Set([...Object.keys(before || {}), ...Object.keys(after || {})])].sort();
1344
+ }
1345
+ function ownsAny(paths, key) {
1346
+ for (const path of paths) {
1347
+ if (key === path || key.startsWith(path + ".") || key.startsWith(path + "[")) return true;
1348
+ }
1349
+ return false;
1350
+ }
1351
+ function pickSectionObject(obj, paths) {
1352
+ if (!obj) return void 0;
1353
+ const out = {};
1354
+ for (const path of paths) {
1355
+ const value = resolvePath(obj, path);
1356
+ if (value !== void 0) out[path] = value;
1357
+ }
1358
+ return Object.keys(out).length ? out : void 0;
1359
+ }
1360
+ function getField(obj, key) {
1361
+ return obj ? obj[key] : void 0;
1362
+ }
1363
+ function resolvePath(obj, path) {
1364
+ if (obj == null || !path) return obj;
1365
+ let cur = obj;
1366
+ const tokens = path.match(/[^.[\]]+|\[\d+\]/g) || [];
1367
+ for (const token of tokens) {
1368
+ if (cur == null || typeof cur !== "object") return void 0;
1369
+ cur = token[0] === "[" ? cur[Number(token.slice(1, -1))] : cur[token];
1370
+ }
1371
+ return cur;
1372
+ }
1373
+ function looksLikeNameValueArray(beforeItems, afterItems) {
1374
+ const items = [...beforeItems, ...afterItems].filter(isObj);
1375
+ return items.length > 0 && items.every((item) => "name" in item && "value" in item);
1376
+ }
1377
+ function itemTitle(before, after, index) {
1378
+ const item = isObj(after) ? after : before;
1379
+ if (item) {
1380
+ for (const key of ["fieldText", "label", "name", "fieldType", "field"]) {
1381
+ const value = unwrapValue(item[key]);
1382
+ if (value !== void 0 && value !== null && typeof value !== "object") {
1383
+ return stripHtml(String(value));
1384
+ }
1385
+ }
1386
+ }
1387
+ return "Item " + (index + 1);
1388
+ }
1389
+ function stripHtml(value) {
1390
+ return value.replace(/<[^>]*>/g, " ").replace(/\s+/g, " ").trim();
1391
+ }
1392
+ function capitalize(value) {
1393
+ return value.charAt(0).toUpperCase() + value.slice(1);
1394
+ }
1395
+ function humanizePath(path) {
1396
+ const names = {
1397
+ assignmentItems: "Assignment item",
1398
+ choiceReferences: "Choice reference",
1399
+ choices: "Choice",
1400
+ conditions: "Condition",
1401
+ customErrorMessages: "Custom error message",
1402
+ dataTypeMappings: "Data type mapping",
1403
+ fields: "Field",
1404
+ filters: "Filter",
1405
+ inputAssignments: "Input assignment",
1406
+ inputParameters: "Input parameter",
1407
+ outputParameters: "Output parameter",
1408
+ processMetadataValues: "Process metadata value",
1409
+ rules: "Rule",
1410
+ scheduledPaths: "Scheduled path",
1411
+ storeOutputParameters: "Store output parameter",
1412
+ transformValues: "Transform value",
1413
+ valueMappings: "Value mapping",
1414
+ waitEvents: "Wait event"
1415
+ };
1416
+ return path.split(".").map((segment) => {
1417
+ const match = segment.match(/^([^[]+)(?:\[(\d+)\])?$/);
1418
+ if (!match) return segment;
1419
+ const name = names[match[1]] || match[1].replace(/([a-z])([A-Z])/g, "$1 $2").replace(/^./, (char) => char.toUpperCase());
1420
+ return match[2] === void 0 ? name : name + " " + (Number(match[2]) + 1);
1421
+ }).join(" > ");
1422
+ }
1423
+ function unwrapValue(value) {
1424
+ if (value === null || value === void 0 || typeof value !== "object" || Array.isArray(value)) return value;
1425
+ if ("elementReference" in value) return "{!" + value.elementReference + "}";
1426
+ const keys = Object.keys(value);
1427
+ if (keys.length === 1) {
1428
+ const key = keys[0];
1429
+ const obj = value;
1430
+ if (key === "stringValue" || key === "numberValue" || key === "dateValue" || key === "dateTimeValue") return obj[key];
1431
+ if (key === "booleanValue") return String(obj[key]) === "true" ? "true" : "false";
1432
+ }
1433
+ return value;
1434
+ }
1435
+ function valuesEqual(a, b) {
1436
+ if (a === b) return true;
1437
+ if (a && b && typeof a === "object" && typeof b === "object") return JSON.stringify(a) === JSON.stringify(b);
1438
+ return false;
1439
+ }
1440
+ function renderScalar(value) {
1441
+ if (value === void 0) return "<em>-</em>";
1442
+ if (value === null) return "<em>empty</em>";
1443
+ if (typeof value === "object") return "<pre>" + escapeHtml(JSON.stringify(value, null, 2)) + "</pre>";
1444
+ const text = String(value);
1445
+ if (text.length > LONG_TEXT_THRESHOLD || text.includes("\n")) {
1446
+ return "<pre>" + escapeHtml(text) + "</pre>";
1447
+ }
1448
+ return escapeHtml(text);
1449
+ }
1450
+ function insVal(value) {
1451
+ return "<span class='val ins'>" + renderScalar(value) + "</span>";
1452
+ }
1453
+ function delVal(value) {
1454
+ return "<span class='val del'>" + renderScalar(value) + "</span>";
1455
+ }
1456
+ function faintVal(value) {
1457
+ return "<span class='val faint'>" + renderScalar(value) + "</span>";
1458
+ }
1459
+ function oneSidedVal(value) {
1460
+ return "<span class='val one-sided'>" + renderScalar(value) + "</span>";
1461
+ }
1462
+ function diffValue(before, after) {
1463
+ before = unwrapValue(before);
1464
+ after = unwrapValue(after);
1465
+ if (before === void 0) return insVal(after);
1466
+ if (after === void 0) return delVal(before);
1467
+ return delVal(before) + " <span class='arrow'>-></span> " + insVal(after);
1468
+ }
1469
+ function escapeHtml(value) {
1470
+ return String(value).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
1471
+ }
1472
+ function snapshotPanelClientScript() {
1473
+ const functions = [
1474
+ renderNodePanelBody,
1475
+ renderSections,
1476
+ renderSchemaSection,
1477
+ renderValue,
1478
+ renderObject,
1479
+ renderArray,
1480
+ renderTable,
1481
+ renderTableFromItems,
1482
+ renderRow,
1483
+ renderGroupedTable,
1484
+ renderGroupScalars,
1485
+ scalarRow,
1486
+ renderLine,
1487
+ renderValueCell,
1488
+ statusOf,
1489
+ hasChange,
1490
+ scalarPair,
1491
+ arr,
1492
+ isObj,
1493
+ isCollection,
1494
+ isCollectionKey,
1495
+ unionKeys,
1496
+ ownsAny,
1497
+ pickSectionObject,
1498
+ getField,
1499
+ resolvePath,
1500
+ looksLikeNameValueArray,
1501
+ itemTitle,
1502
+ stripHtml,
1503
+ capitalize,
1504
+ humanizePath,
1505
+ unwrapValue,
1506
+ valuesEqual,
1507
+ renderScalar,
1508
+ insVal,
1509
+ delVal,
1510
+ faintVal,
1511
+ oneSidedVal,
1512
+ diffValue,
1513
+ escapeHtml
1514
+ ].map((fn) => fn.toString()).join("\n\n");
1515
+ return "const LONG_TEXT_THRESHOLD = " + LONG_TEXT_THRESHOLD + ";\nconst COLLECTION_KEYS = new Set(" + JSON.stringify(COLLECTION_KEY_NAMES) + ");\n\n" + functions;
1516
+ }
1517
+
928
1518
  // src/render/render-html.ts
929
1519
  function renderHtml(layout) {
930
1520
  const data = {
@@ -947,7 +1537,7 @@ function renderHtml(layout) {
947
1537
  <head>
948
1538
  <meta charset="utf-8" />
949
1539
  <meta name="viewport" content="width=device-width, initial-scale=1" />
950
- <title>${escapeHtml(layout.diff.flowName)}</title>
1540
+ <title>${escapeHtml2(layout.diff.flowName)}</title>
951
1541
  <style>
952
1542
  :root {
953
1543
  color-scheme: light;
@@ -1035,6 +1625,11 @@ function renderHtml(layout) {
1035
1625
  .panel.modified .badge { background: var(--modified-fill); color: var(--modified); }
1036
1626
  .panel .empty { color: var(--muted); font-size: 13px; }
1037
1627
  .detail-section { margin: 0 0 12px; border: 1px solid var(--border); border-radius: 8px; background: #fbfcfe; overflow: hidden; }
1628
+ .detail-section.snapshot-section { border-left-width: 3px; }
1629
+ .detail-section.snapshot-section.added { border-left-color: rgba(22, 163, 74, 0.5); }
1630
+ .detail-section.snapshot-section.deleted { border-left-color: rgba(220, 38, 38, 0.45); }
1631
+ .detail-section.snapshot-section.added summary { background: rgba(220, 252, 231, 0.35); }
1632
+ .detail-section.snapshot-section.deleted summary { background: rgba(254, 226, 226, 0.35); }
1038
1633
  .detail-section summary { cursor: pointer; padding: 11px 13px; font-weight: 650; }
1039
1634
  .detail-section[open] summary { border-bottom: 1px solid var(--border); }
1040
1635
  .section-count { color: var(--muted); font-size: 11px; font-weight: 500; }
@@ -1048,8 +1643,9 @@ function renderHtml(layout) {
1048
1643
  .val.ins { color: #14532d; background: var(--added-fill); }
1049
1644
  .val.del { color: #7f1d1d; background: var(--deleted-fill); text-decoration: line-through; text-decoration-color: rgba(127, 29, 29, 0.5); }
1050
1645
  .val.faint { color: var(--muted); background: transparent; padding-left: 0; }
1646
+ .val.one-sided { color: var(--text); background: transparent; padding-left: 0; }
1051
1647
  .val em { font-style: normal; color: var(--faint); }
1052
- .val pre { margin: 4px 0 0; padding: 8px 10px; background: var(--surface); border: 1px solid var(--border); border-radius: 6px; white-space: pre-wrap; overflow-wrap: anywhere; font-size: 11.5px; line-height: 1.5; }
1648
+ .val pre { margin: 4px 0 0; padding: 8px 10px; background: var(--surface); border: 1px solid var(--border); border-radius: 6px; white-space: pre-wrap; overflow-wrap: anywhere; font-size: 11.5px; line-height: 1.5; max-height: 220px; overflow: auto; }
1053
1649
  .arrow { color: var(--faint); margin: 0 2px; }
1054
1650
  /* scalar / fallback change lines */
1055
1651
  .changes { list-style: none; padding: 0; margin: 0; }
@@ -1066,6 +1662,16 @@ function renderHtml(layout) {
1066
1662
  .change-table .row-idx { color: var(--faint); font-variant-numeric: tabular-nums; width: 1%; white-space: nowrap; }
1067
1663
  .change-table tr.row-added { background: rgba(22, 163, 74, 0.06); }
1068
1664
  .change-table tr.row-removed { background: rgba(220, 38, 38, 0.05); }
1665
+ .snapshot-kv { width: 100%; border-collapse: collapse; font-size: 12px; margin-bottom: 10px; }
1666
+ .snapshot-kv th, .snapshot-kv td { padding: 6px 7px; border-bottom: 1px solid var(--border); text-align: left; vertical-align: top; overflow-wrap: anywhere; }
1667
+ .snapshot-kv th { color: var(--muted); width: 34%; font-weight: 650; }
1668
+ .snapshot-kv tr:last-child th, .snapshot-kv tr:last-child td { border-bottom: 0; }
1669
+ .snapshot-nested { border-left: 2px solid var(--border); padding-left: 10px; margin: 10px 0; }
1670
+ .snapshot-nested-title { color: var(--muted); font-size: 11px; font-weight: 700; letter-spacing: 0.04em; text-transform: uppercase; margin-bottom: 7px; }
1671
+ .snapshot-card { border: 1px dashed var(--border); border-radius: 7px; padding: 9px 10px; margin: 8px 0; background: var(--surface); }
1672
+ .snapshot-card.added { border-left: 2px solid rgba(22, 163, 74, 0.5); }
1673
+ .snapshot-card.removed { border-left: 2px solid rgba(220, 38, 38, 0.45); }
1674
+ .snapshot-card-title { font-size: 12px; font-weight: 700; margin-bottom: 7px; overflow-wrap: anywhere; }
1069
1675
  /* outcome groups (decisions) */
1070
1676
  .outcome-group { margin-bottom: 12px; padding: 10px 11px; border: 1px solid var(--border); border-radius: 7px; background: var(--surface); }
1071
1677
  .outcome-group:last-child { margin-bottom: 0; }
@@ -1110,7 +1716,7 @@ function renderHtml(layout) {
1110
1716
  <body>
1111
1717
  <header>
1112
1718
  <div>
1113
- <h1>${escapeHtml(layout.diff.flowName)}</h1>
1719
+ <h1>${escapeHtml2(layout.diff.flowName)}</h1>
1114
1720
  <div class="meta">${metaHtml}</div>
1115
1721
  </div>
1116
1722
  <div class="header-actions">
@@ -1178,14 +1784,6 @@ function renderHtml(layout) {
1178
1784
  let dragStart = null;
1179
1785
  let selectedNodeId = null;
1180
1786
 
1181
- function escapeHtml(value) {
1182
- return String(value)
1183
- .replace(/&/g, "&amp;")
1184
- .replace(/</g, "&lt;")
1185
- .replace(/>/g, "&gt;")
1186
- .replace(/"/g, "&quot;");
1187
- }
1188
-
1189
1787
  function isNodeVisible(node, mode) {
1190
1788
  if (mode === "after") return node.status !== "deleted";
1191
1789
  if (mode === "before") return node.status !== "added";
@@ -1311,237 +1909,10 @@ function renderHtml(layout) {
1311
1909
  panel.className = "panel " + node.status;
1312
1910
  panelTitle.textContent = node.label;
1313
1911
  panelBadge.textContent = node.status.toUpperCase();
1314
- const changes = node.changes || [];
1315
- panelBody.innerHTML = changes.length
1316
- ? organizeChanges(node).map((section) => formatSection(section, node)).join("")
1317
- : node.status === "added"
1318
- ? "<div class='empty'>This node was added in the new version.</div>"
1319
- : node.status === "deleted"
1320
- ? "<div class='empty'>This node was removed in the new version.</div>"
1321
- : "<div class='empty'>No property changes.</div>";
1322
- }
1323
-
1324
- function owns(prefix, path) {
1325
- return path === prefix || path.startsWith(prefix + ".") || path.startsWith(prefix + "[");
1326
- }
1327
-
1328
- function changeKind(change) {
1329
- return change.before === undefined ? "added" : change.after === undefined ? "removed" : "modified";
1330
- }
1331
-
1332
- function capitalize(value) {
1333
- return value.charAt(0).toUpperCase() + value.slice(1);
1334
- }
1335
-
1336
- // Parse "base[idx]rest" \u2014 returns the item index and trailing path, or null.
1337
- function matchItem(path, base) {
1338
- if (!path.startsWith(base + "[")) return null;
1339
- const close = path.indexOf("]", base.length + 1);
1340
- if (close === -1) return null;
1341
- const idx = path.slice(base.length + 1, close);
1342
- if (!/^\\d+$/.test(idx)) return null;
1343
- return { idx, rest: path.slice(close + 1) };
1344
- }
1345
-
1346
- // Group a node's flat change list into ordered sections: schema-driven first,
1347
- // then a generic fallback (array-prefixed groups, then a Configuration bucket).
1348
- function organizeChanges(node) {
1349
- const schemas = DATA.sectionSchemas[node.type] || [];
1350
- const sections = schemas.map((schema) => ({ schema, changes: [] }));
1351
- const fallback = new Map();
1352
- for (const change of node.changes || []) {
1353
- const sec = sections.find((s) => s.schema.paths.some((path) => owns(path, change.path)));
1354
- if (sec) { sec.changes.push(change); continue; }
1355
- const match = change.path.match(/^([^.[]+)(\\[\\d+\\])?/);
1356
- const isArray = !!(match && match[2] !== undefined);
1357
- const key = isArray ? match[1] : "Configuration";
1358
- if (!fallback.has(key)) fallback.set(key, []);
1359
- fallback.get(key).push(change);
1360
- }
1361
- const out = [];
1362
- for (const s of sections) {
1363
- if (s.changes.length) out.push({ name: s.schema.name, schema: s.schema, changes: s.changes });
1364
- }
1365
- for (const [key, changes] of fallback) {
1366
- out.push({
1367
- name: key === "Configuration" ? "Configuration" : humanizePath(key),
1368
- schema: { render: "lines", paths: [key] },
1369
- changes,
1370
- });
1371
- }
1372
- return out;
1373
- }
1374
-
1375
- function formatSection(section, node) {
1376
- const body = renderSectionBody(section, node);
1377
- return "<details class='detail-section' open><summary>" + escapeHtml(section.name)
1378
- + " <span class='section-count'>(" + section.changes.length + ")</span></summary>"
1379
- + "<div class='section-body'>" + body + "</div></details>";
1380
- }
1381
-
1382
- function renderSectionBody(section, node) {
1383
- const render = section.schema.render;
1384
- if (render === "grouped-table") return renderGroupedTable(section, node);
1385
- if (render === "table" && section.schema.columns) {
1386
- return renderTable(section.changes, section.schema.paths, section.schema.columns, { before: node.before, after: node.after, prefix: "" });
1387
- }
1388
- return renderLines(section.changes, {});
1389
- }
1390
-
1391
- // Labeled before \u2192 after lines, for scalar changes and the generic fallback.
1392
- // The plain option drops per-line badges \u2014 used under a wholly added/removed
1393
- // outcome, where the single group badge already states the change (Finding 3).
1394
- function renderLines(changes, options) {
1395
- const plain = options && options.plain;
1396
- return "<ul class='changes'>" + changes.map((change) => {
1397
- const kind = changeKind(change);
1398
- const badge = plain ? "" : "<span class='change-kind " + kind + "'>" + capitalize(kind) + "</span>";
1399
- return "<li><div class='change-line-head'>" + badge
1400
- + "<span class='change-line-label'>" + escapeHtml(humanizePath(change.path)) + "</span></div>"
1401
- + "<div class='change-line-value'>" + diffValue(change.before, change.after) + "</div></li>";
1402
- }).join("") + "</ul>";
1403
- }
1404
-
1405
- // Walk a path like "rules[0].conditions[2]" into an object.
1406
- function resolvePath(obj, path) {
1407
- if (obj == null || !path) return undefined;
1408
- let cur = obj;
1409
- const tokens = path.match(/[^.[\\]]+|\\[\\d+\\]/g) || [];
1410
- for (const token of tokens) {
1411
- if (cur == null || typeof cur !== "object") return undefined;
1412
- cur = token[0] === "[" ? cur[Number(token.slice(1, -1))] : cur[token];
1413
- }
1414
- return cur;
1415
- }
1416
-
1417
- // Collapse arr[i].* leaf changes (and whole-item changes) into one row per
1418
- // item index, then fill any unchanged columns from the item snapshot so a
1419
- // modified row still shows its sibling context (Resource/Operator/etc.).
1420
- function buildRows(changes, basePaths, columns, ctx) {
1421
- const rows = new Map();
1422
- let order = 0;
1423
- for (const change of changes) {
1424
- let base = null, idx = null, rest = "";
1425
- for (const p of basePaths) {
1426
- const m = matchItem(change.path, p);
1427
- if (m) { base = p; idx = m.idx; rest = m.rest; break; }
1428
- }
1429
- if (base === null) continue;
1430
- const itemKey = base + "[" + idx + "]";
1431
- if (!rows.has(itemKey)) rows.set(itemKey, { kind: "modified", order: order++, idx, base, cells: {} });
1432
- const row = rows.get(itemKey);
1433
- if (rest === "") {
1434
- row.kind = changeKind(change);
1435
- for (const col of columns) {
1436
- const before = extractField(change.before, col.path);
1437
- const after = extractField(change.after, col.path);
1438
- row.cells[col.path] = { before, after, changed: !valuesEqual(before, after) };
1439
- }
1440
- continue;
1441
- }
1442
- const sub = rest.replace(/^\\./, "");
1443
- const col = columns.find((c) => sub === c.path || sub.startsWith(c.path + ".") || sub.startsWith(c.path + "["));
1444
- if (col) row.cells[col.path] = { before: unwrapValue(change.before), after: unwrapValue(change.after), changed: true };
1445
- }
1446
- const result = [...rows.values()].sort((a, b) => a.order - b.order);
1447
- if (ctx) {
1448
- for (const row of result) {
1449
- const abs = ctx.prefix ? ctx.prefix + "." + row.base + "[" + row.idx + "]" : row.base + "[" + row.idx + "]";
1450
- const snapshot = row.kind === "removed" ? resolvePath(ctx.before, abs) : resolvePath(ctx.after, abs);
1451
- if (!snapshot || typeof snapshot !== "object") continue;
1452
- for (const col of columns) {
1453
- if (row.cells[col.path]) continue;
1454
- const value = extractField(snapshot, col.path);
1455
- if (value !== undefined) row.cells[col.path] = { before: value, after: value, changed: false };
1456
- }
1457
- }
1458
- }
1459
- return result;
1460
- }
1461
-
1462
- // options.uniformKind (a wholly added/removed group): drop the Change
1463
- // column and per-row badges, colouring every cell by that single kind.
1464
- function renderTable(changes, basePaths, columns, ctx, options) {
1465
- const uniform = options && options.uniformKind;
1466
- const rows = buildRows(changes, basePaths, columns, ctx);
1467
- if (!rows.length) return renderLines(changes, {});
1468
- const head = "<tr><th class='row-idx'>#</th>" + columns.map((c) => "<th>" + escapeHtml(c.label) + "</th>").join("")
1469
- + (uniform ? "" : "<th>Change</th>") + "</tr>";
1470
- const body = rows.map((row) => renderRow(row, columns, uniform)).join("");
1471
- return "<table class='change-table'><thead>" + head + "</thead><tbody>" + body + "</tbody></table>";
1472
- }
1473
-
1474
- function renderRow(row, columns, uniform) {
1475
- const kind = uniform || row.kind;
1476
- const cells = columns.map((col) => {
1477
- const cell = row.cells[col.path];
1478
- if (!cell) return "<td></td>";
1479
- if (kind === "added") return "<td>" + insVal(cell.after !== undefined ? cell.after : cell.before) + "</td>";
1480
- if (kind === "removed") return "<td>" + delVal(cell.before !== undefined ? cell.before : cell.after) + "</td>";
1481
- return "<td>" + (cell.changed ? diffValue(cell.before, cell.after) : faintVal(cell.after)) + "</td>";
1482
- }).join("");
1483
- const badge = uniform ? "" : "<td><span class='change-kind " + row.kind + "'>" + capitalize(row.kind) + "</span></td>";
1484
- return "<tr class='row-" + kind + "'><td class='row-idx'>" + (Number(row.idx) + 1) + "</td>" + cells + badge + "</tr>";
1912
+ panelBody.innerHTML = renderNodePanelBody(node, DATA.sectionSchemas[node.type] || []);
1485
1913
  }
1486
1914
 
1487
- // Decisions et al.: group by the outer array (one group per outcome/rule).
1488
- // A wholly added/removed outcome shows ONE group badge + plain context; a
1489
- // modified outcome shows its changed conditions in a per-row-badged table.
1490
- function renderGroupedTable(section, node) {
1491
- const schema = section.schema;
1492
- const groups = new Map();
1493
- let order = 0;
1494
- for (const change of section.changes) {
1495
- let matched = null;
1496
- for (const p of schema.paths) {
1497
- const m = matchItem(change.path, p);
1498
- if (m) { matched = { base: p, idx: m.idx, rest: m.rest }; break; }
1499
- }
1500
- if (!matched) continue;
1501
- const key = matched.idx;
1502
- if (!groups.has(key)) groups.set(key, { idx: matched.idx, base: matched.base, order: order++, label: null, kind: "modified", whole: false, inner: [], scalars: [] });
1503
- const g = groups.get(key);
1504
- if (matched.rest === "") {
1505
- g.kind = changeKind(change);
1506
- g.whole = true;
1507
- const obj = change.after !== undefined ? change.after : change.before;
1508
- if (obj && typeof obj === "object") {
1509
- if (schema.groupLabelPath && obj[schema.groupLabelPath]) g.label = obj[schema.groupLabelPath];
1510
- const inner = obj[schema.innerArray];
1511
- if (Array.isArray(inner)) {
1512
- inner.forEach((item, j) => g.inner.push({
1513
- path: schema.innerArray + "[" + j + "]",
1514
- before: g.kind === "removed" ? item : undefined,
1515
- after: g.kind === "removed" ? undefined : item,
1516
- }));
1517
- }
1518
- for (const k of Object.keys(obj)) {
1519
- if (k === schema.innerArray || k === schema.groupLabelPath) continue;
1520
- g.scalars.push({ path: k, before: g.kind === "removed" ? obj[k] : undefined, after: g.kind === "removed" ? undefined : obj[k] });
1521
- }
1522
- }
1523
- continue;
1524
- }
1525
- const sub = matched.rest.replace(/^\\./, "");
1526
- if (sub === schema.groupLabelPath) g.label = change.after !== undefined ? change.after : change.before;
1527
- if (sub === schema.innerArray || sub.startsWith(schema.innerArray + "[")) {
1528
- g.inner.push({ path: sub, before: change.before, after: change.after });
1529
- } else {
1530
- g.scalars.push({ path: sub, before: change.before, after: change.after });
1531
- }
1532
- }
1533
- return [...groups.values()].sort((a, b) => a.order - b.order).map((g) => {
1534
- const label = g.label != null ? String(g.label) : "Outcome " + (Number(g.idx) + 1);
1535
- const head = "<div class='group-head'><span class='change-kind " + g.kind + "'>" + capitalize(g.kind) + "</span>"
1536
- + "<span class='group-label'>" + escapeHtml(label) + "</span></div>";
1537
- const prefix = g.base + "[" + g.idx + "]";
1538
- const scalars = g.scalars.length ? renderLines(g.scalars, { plain: g.whole }) : "";
1539
- const inner = g.inner.length
1540
- ? renderTable(g.inner, [schema.innerArray], schema.innerColumns, { before: node.before, after: node.after, prefix }, g.whole ? { uniformKind: g.kind } : {})
1541
- : "";
1542
- return "<div class='outcome-group'>" + head + scalars + inner + "</div>";
1543
- }).join("");
1544
- }
1915
+ ${snapshotPanelClientScript()}
1545
1916
 
1546
1917
  function selectNode(id) {
1547
1918
  selectedNodeId = id;
@@ -1552,70 +1923,6 @@ function renderHtml(layout) {
1552
1923
  if (node) updatePanel(node);
1553
1924
  }
1554
1925
 
1555
- function humanizePath(path) {
1556
- const names = {
1557
- assignmentItems: "Assignment item",
1558
- conditions: "Condition",
1559
- rules: "Rule",
1560
- };
1561
- return path.split(".").map((segment) => {
1562
- const match = segment.match(/^([^[]+)(?:\\[(\\d+)\\])?$/);
1563
- if (!match) return segment;
1564
- const name = names[match[1]] || match[1].replace(/([a-z])([A-Z])/g, "$1 $2").replace(/^./, (char) => char.toUpperCase());
1565
- return match[2] === undefined ? name : name + " " + (Number(match[2]) + 1);
1566
- }).join(" \u203A ");
1567
- }
1568
-
1569
- // Collapse Salesforce typed-value wrappers to their scalar so reviewers see
1570
- // "true" / "Something Else" / "{!myVar}" instead of a JSON blob.
1571
- function unwrapValue(value) {
1572
- if (value === null || value === undefined || typeof value !== "object" || Array.isArray(value)) return value;
1573
- if ("elementReference" in value) return "{!" + value.elementReference + "}";
1574
- const keys = Object.keys(value);
1575
- if (keys.length === 1) {
1576
- const key = keys[0];
1577
- if (key === "stringValue" || key === "numberValue" || key === "dateValue" || key === "dateTimeValue") return value[key];
1578
- if (key === "booleanValue") return String(value[key]) === "true" ? "true" : "false";
1579
- }
1580
- return value;
1581
- }
1582
-
1583
- function extractField(obj, path) {
1584
- if (obj === undefined || obj === null) return undefined;
1585
- let cur = obj;
1586
- for (const segment of path.split(".")) {
1587
- if (cur === null || typeof cur !== "object") return undefined;
1588
- cur = cur[segment];
1589
- }
1590
- return unwrapValue(cur);
1591
- }
1592
-
1593
- function valuesEqual(a, b) {
1594
- if (a === b) return true;
1595
- if (a && b && typeof a === "object" && typeof b === "object") return JSON.stringify(a) === JSON.stringify(b);
1596
- return false;
1597
- }
1598
-
1599
- function renderScalar(value) {
1600
- if (value === undefined) return "<em>\u2014</em>";
1601
- if (value === null) return "<em>empty</em>";
1602
- if (typeof value === "object") return "<pre>" + escapeHtml(JSON.stringify(value, null, 2)) + "</pre>";
1603
- return escapeHtml(String(value));
1604
- }
1605
-
1606
- function insVal(value) { return "<span class='val ins'>" + renderScalar(value) + "</span>"; }
1607
- function delVal(value) { return "<span class='val del'>" + renderScalar(value) + "</span>"; }
1608
- function faintVal(value) { return "<span class='val faint'>" + renderScalar(value) + "</span>"; }
1609
-
1610
- // git-diff value grammar: removed (red strike) \u2192 added (green).
1611
- function diffValue(before, after) {
1612
- before = unwrapValue(before);
1613
- after = unwrapValue(after);
1614
- if (before === undefined) return insVal(after);
1615
- if (after === undefined) return delVal(before);
1616
- return delVal(before) + " <span class='arrow'>\u2192</span> " + insVal(after);
1617
- }
1618
-
1619
1926
  function pickInitialNode(visibleNodeIds) {
1620
1927
  return DATA.nodes.find((node) => visibleNodeIds.has(node.id) && node.status === "modified")
1621
1928
  || DATA.nodes.find((node) => visibleNodeIds.has(node.id) && node.status !== "unchanged")
@@ -1789,16 +2096,16 @@ function renderEdge(edge) {
1789
2096
  return "";
1790
2097
  }
1791
2098
  const d = points.map((point, index) => `${index === 0 ? "M" : "L"} ${point.x} ${point.y}`).join(" ");
1792
- return `<path class="edge ${edge.kind} ${edge.status}" d="${d}" data-edge-id="${escapeHtml(edge.id)}"></path>`;
2099
+ return `<path class="edge ${edge.kind} ${edge.status}" d="${d}" data-edge-id="${escapeHtml2(edge.id)}"></path>`;
1793
2100
  }
1794
2101
  function renderNode(node) {
1795
2102
  const lines = wrapLabel(node.label, 22);
1796
2103
  const lineHeight = 14;
1797
2104
  const textY = node.height / 2 - (lines.length - 1) * lineHeight / 2 + 5;
1798
- return `<g class="node ${node.status} type-${node.type}" data-node-id="${escapeHtml(node.id)}" transform="translate(${node.x},${node.y})">
2105
+ return `<g class="node ${node.status} type-${node.type}" data-node-id="${escapeHtml2(node.id)}" transform="translate(${node.x},${node.y})">
1799
2106
  <rect width="${node.width}" height="${node.height}"></rect>
1800
2107
  <text x="${node.width / 2}" y="${textY}" text-anchor="middle" class="node-label">
1801
- ${lines.map((line, index) => `<tspan x="${node.width / 2}" dy="${index === 0 ? 0 : lineHeight}">${escapeHtml(line)}</tspan>`).join("")}
2108
+ ${lines.map((line, index) => `<tspan x="${node.width / 2}" dy="${index === 0 ? 0 : lineHeight}">${escapeHtml2(line)}</tspan>`).join("")}
1802
2109
  </text>
1803
2110
  </g>`;
1804
2111
  }
@@ -1820,7 +2127,7 @@ function wrapLabel(label, maxChars) {
1820
2127
  }
1821
2128
  return lines.length ? lines.slice(0, 3) : [label];
1822
2129
  }
1823
- function escapeHtml(value) {
2130
+ function escapeHtml2(value) {
1824
2131
  return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
1825
2132
  }
1826
2133
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@syntax-syllogism/flow-delta",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
4
4
  "type": "module",
5
5
  "description": "Semantic visual diff for Salesforce Flows",
6
6
  "license": "MIT",