@syntax-syllogism/flow-delta 0.2.1 → 0.4.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -628,6 +628,38 @@ function isPlainObject(value) {
628
628
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
629
629
  }
630
630
 
631
+ // src/model/flow-header.ts
632
+ import { Parser as Parser2 } from "xml2js";
633
+ var FLOW_HEADER_KEYS = [
634
+ "status",
635
+ "processType",
636
+ "runInMode",
637
+ "apiVersion",
638
+ "triggerOrder",
639
+ "description",
640
+ "interviewLabel",
641
+ "isTemplate"
642
+ ];
643
+ async function extractFlowHeader(xml) {
644
+ try {
645
+ const parsed = await new Parser2({ explicitArray: false }).parseStringPromise(xml);
646
+ const flow = parsed.Flow;
647
+ if (!flow) {
648
+ return {};
649
+ }
650
+ const header = {};
651
+ for (const key of FLOW_HEADER_KEYS) {
652
+ const value = flow[key];
653
+ if (value !== void 0 && (value === null || typeof value !== "object")) {
654
+ header[key] = String(value);
655
+ }
656
+ }
657
+ return header;
658
+ } catch {
659
+ return {};
660
+ }
661
+ }
662
+
631
663
  // src/diff/deep-diff.ts
632
664
  function deepDiff(before, after, path = "") {
633
665
  if (Object.is(before, after)) {
@@ -685,21 +717,32 @@ function diffModel(oldModel, newModel) {
685
717
  const edgeIds = [.../* @__PURE__ */ new Set([...oldEdges.keys(), ...newEdges.keys()])].sort();
686
718
  const nodes = nodeIds.map((id) => classifyNode(oldNodes.get(id), newNodes.get(id)));
687
719
  const edges = edgeIds.map((id) => classifyEdge(oldEdges.get(id), newEdges.get(id)));
720
+ const flowChanges = diffFlowHeaders(oldModel, newModel);
688
721
  const summary = {
689
722
  addedNodes: nodes.filter((node) => node.status === "added").length,
690
723
  removedNodes: nodes.filter((node) => node.status === "deleted").length,
691
724
  modifiedNodes: nodes.filter((node) => node.status === "modified").length,
692
725
  unchangedNodes: nodes.filter((node) => node.status === "unchanged").length,
693
726
  addedEdges: edges.filter((edge) => edge.status === "added").length,
694
- removedEdges: edges.filter((edge) => edge.status === "deleted").length
727
+ removedEdges: edges.filter((edge) => edge.status === "deleted").length,
728
+ changedFlowAttributes: flowChanges.length
695
729
  };
696
730
  return {
697
731
  flowName: selectFlowName(oldModel, newModel),
698
732
  summary,
733
+ ...flowChanges.length > 0 ? { flowChanges } : {},
699
734
  nodes,
700
735
  edges
701
736
  };
702
737
  }
738
+ function diffFlowHeaders(oldModel, newModel) {
739
+ if (!oldModel.header || !newModel.header) {
740
+ return [];
741
+ }
742
+ const changes = deepDiff(oldModel.header, newModel.header);
743
+ const order = new Map(FLOW_HEADER_KEYS.map((key, index) => [key, index]));
744
+ return changes.sort((left, right) => (order.get(left.path) ?? 999) - (order.get(right.path) ?? 999));
745
+ }
703
746
  function selectFlowName(oldModel, newModel) {
704
747
  if (newModel.nodes.length > 0 || newModel.edges.length > 0) {
705
748
  return newModel.flowName;
@@ -714,10 +757,10 @@ function classifyNode(oldNode, newNode) {
714
757
  throw new Error("Cannot classify absent node");
715
758
  }
716
759
  if (!oldNode) {
717
- return { id: newNode.id, type: newNode.type, label: newNode.label, status: "added" };
760
+ return { id: newNode.id, type: newNode.type, label: newNode.label, status: "added", after: newNode.properties };
718
761
  }
719
762
  if (!newNode) {
720
- return { id: oldNode.id, type: oldNode.type, label: oldNode.label, status: "deleted" };
763
+ return { id: oldNode.id, type: oldNode.type, label: oldNode.label, status: "deleted", before: oldNode.properties };
721
764
  }
722
765
  const changes = [
723
766
  ...deepDiff(oldNode.label, newNode.label, "label"),
@@ -880,7 +923,37 @@ var PARAM_COLUMNS = [
880
923
  { path: "value", label: "Value" },
881
924
  { path: "assignToReference", label: "Assign To" }
882
925
  ];
926
+ var SCREEN_SETTING_PATHS = [
927
+ "allowBack",
928
+ "allowFinish",
929
+ "allowPause",
930
+ "showFooter",
931
+ "showHeader"
932
+ ];
883
933
  var SECTION_SCHEMAS = {
934
+ screen: [
935
+ { name: "Screen Settings", paths: SCREEN_SETTING_PATHS, render: "lines" }
936
+ ],
937
+ start: [
938
+ {
939
+ name: "Trigger",
940
+ paths: ["object", "triggerType", "recordTriggerType", "schedule", "filterLogic"],
941
+ render: "lines"
942
+ },
943
+ { name: "Entry Conditions", paths: ["filters"], render: "table", columns: FILTER_COLUMNS },
944
+ {
945
+ name: "Scheduled Paths",
946
+ paths: ["scheduledPaths"],
947
+ render: "table",
948
+ columns: [
949
+ { path: "name", label: "Name" },
950
+ { path: "label", label: "Label" },
951
+ { path: "offsetNumber", label: "Offset" },
952
+ { path: "offsetUnit", label: "Unit" },
953
+ { path: "timeSource", label: "Time Source" }
954
+ ]
955
+ }
956
+ ],
884
957
  decision: [{
885
958
  name: "Outcomes",
886
959
  paths: ["rules"],
@@ -907,6 +980,7 @@ var SECTION_SCHEMAS = {
907
980
  recordUpdate: [{ name: "Field Mappings", paths: ["inputAssignments"], render: "table", columns: MAPPING_COLUMNS }],
908
981
  recordLookup: [{ name: "Filters", paths: ["filters"], render: "table", columns: FILTER_COLUMNS }],
909
982
  recordDelete: [{ name: "Filters", paths: ["filters"], render: "table", columns: FILTER_COLUMNS }],
983
+ subflow: [{ name: "Parameters", paths: ["inputAssignments", "outputAssignments"], render: "table", columns: PARAM_COLUMNS }],
910
984
  actionCall: [{ name: "Parameters", paths: ["inputParameters", "outputParameters"], render: "table", columns: PARAM_COLUMNS }],
911
985
  apexPluginCall: [{ name: "Parameters", paths: ["inputParameters", "outputParameters"], render: "table", columns: PARAM_COLUMNS }],
912
986
  wait: [{
@@ -919,12 +993,571 @@ var SECTION_SCHEMAS = {
919
993
  { path: "offsetUnit", label: "Unit" },
920
994
  { path: "resumeDateReference", label: "Resume Date" }
921
995
  ]
922
- }]
996
+ }],
997
+ customError: [{
998
+ name: "Error Messages",
999
+ paths: ["customErrorMessages"],
1000
+ render: "table",
1001
+ columns: [
1002
+ { path: "name", label: "Name" },
1003
+ { path: "errorMessage", label: "Message" }
1004
+ ]
1005
+ }],
1006
+ transform: [
1007
+ {
1008
+ name: "Value Mappings",
1009
+ paths: ["transformValues", "valueMappings"],
1010
+ render: "table",
1011
+ columns: [
1012
+ { path: "name", label: "Name" },
1013
+ { path: "sourceDataType", label: "Source Type" },
1014
+ { path: "targetDataType", label: "Target Type" },
1015
+ { path: "value", label: "Value" }
1016
+ ]
1017
+ },
1018
+ {
1019
+ name: "Data Type Mappings",
1020
+ paths: ["dataTypeMappings"],
1021
+ render: "table",
1022
+ columns: [
1023
+ { path: "name", label: "Name" },
1024
+ { path: "sourceDataType", label: "Source Type" },
1025
+ { path: "targetDataType", label: "Target Type" }
1026
+ ]
1027
+ }
1028
+ ],
1029
+ collectionProcessor: [
1030
+ { name: "Collection Settings", paths: ["collectionReference", "operation"], render: "lines" },
1031
+ { name: "Filters", paths: ["filters"], render: "table", columns: FILTER_COLUMNS },
1032
+ {
1033
+ name: "Sort Options",
1034
+ paths: ["sortOptions"],
1035
+ render: "table",
1036
+ columns: [
1037
+ { path: "field", label: "Field" },
1038
+ { path: "sortOrder", label: "Order" }
1039
+ ]
1040
+ }
1041
+ ],
1042
+ orchestratedStage: [
1043
+ { name: "Stage Settings", paths: ["stageLabel", "stageOrder"], render: "lines" }
1044
+ ]
923
1045
  };
924
1046
  function getSectionSchemas(type) {
925
1047
  return SECTION_SCHEMAS[type] ?? [];
926
1048
  }
927
1049
 
1050
+ // src/render/snapshot-panel.ts
1051
+ var LONG_TEXT_THRESHOLD = 160;
1052
+ var COLLECTION_KEY_NAMES = [
1053
+ "assignmentItems",
1054
+ "choiceReferences",
1055
+ "choices",
1056
+ "conditions",
1057
+ "customErrorMessages",
1058
+ "dataTypeMappings",
1059
+ "fields",
1060
+ "filters",
1061
+ "inputAssignments",
1062
+ "inputParameters",
1063
+ "outputParameters",
1064
+ "outputAssignments",
1065
+ "processMetadataValues",
1066
+ "rules",
1067
+ "scheduledPaths",
1068
+ "sortOptions",
1069
+ "stageSteps",
1070
+ "steps",
1071
+ "storeOutputParameters",
1072
+ "transformValues",
1073
+ "valueMappings",
1074
+ "waitEvents"
1075
+ ];
1076
+ var COLLECTION_KEYS = new Set(COLLECTION_KEY_NAMES);
1077
+ function renderNodePanelBody(node, schemas = []) {
1078
+ if (node.status === "unchanged") {
1079
+ return "<div class='empty'>No property changes.</div>";
1080
+ }
1081
+ const before = node.status === "added" ? void 0 : node.before;
1082
+ const after = node.status === "deleted" ? void 0 : node.after;
1083
+ const options = { showUnchanged: node.status !== "modified", nodeStatus: node.status };
1084
+ const sections = renderSections(before, after, schemas || [], options);
1085
+ if (!sections.length) {
1086
+ return "<div class='empty'>No configuration.</div>";
1087
+ }
1088
+ return sections.map((section, index) => {
1089
+ const open = node.status === "modified" || index === 0 ? " open" : "";
1090
+ 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>";
1091
+ }).join("");
1092
+ }
1093
+ function renderSections(before, after, schemas, options) {
1094
+ const sections = [];
1095
+ const owned = /* @__PURE__ */ new Set();
1096
+ for (const schema of schemas) {
1097
+ for (const path of schema.paths) owned.add(path);
1098
+ const rendered = renderSchemaSection(before, after, schema, options);
1099
+ if (rendered) sections.push(rendered);
1100
+ }
1101
+ const keys = unionKeys(before, after).filter((key) => !ownsAny(owned, key));
1102
+ const settingsRows = [];
1103
+ let settingsCount = 0;
1104
+ const nestedSections = [];
1105
+ for (const key of keys) {
1106
+ const childBefore = getField(before, key);
1107
+ const childAfter = getField(after, key);
1108
+ const childChanged = hasChange(childBefore, childAfter, key);
1109
+ const scalar = scalarPair(childBefore, childAfter);
1110
+ if (scalar) {
1111
+ if (options.showUnchanged || childChanged) {
1112
+ settingsRows.push(scalarRow(key, scalar.before, scalar.after, statusOf(childBefore, childAfter), options.nodeStatus));
1113
+ }
1114
+ if (childChanged) settingsCount += 1;
1115
+ continue;
1116
+ }
1117
+ const rendered = renderValue(getField(before, key), getField(after, key), {
1118
+ ...options,
1119
+ label: humanizePath(key),
1120
+ pathKey: key,
1121
+ depth: 0
1122
+ });
1123
+ if (rendered.html) {
1124
+ nestedSections.push({ name: humanizePath(key), html: rendered.html, count: rendered.count });
1125
+ }
1126
+ }
1127
+ if (settingsRows.length) {
1128
+ sections.push({
1129
+ name: "Settings",
1130
+ html: "<table class='snapshot-kv'><tbody>" + settingsRows.join("") + "</tbody></table>",
1131
+ count: settingsCount
1132
+ });
1133
+ }
1134
+ sections.push(...nestedSections);
1135
+ return sections;
1136
+ }
1137
+ function renderSchemaSection(before, after, schema, options) {
1138
+ let rendered;
1139
+ if (schema.render === "table" && schema.columns) {
1140
+ rendered = renderTable(before, after, schema.paths, schema.columns, options);
1141
+ } else if (schema.render === "grouped-table" && schema.innerArray && schema.innerColumns) {
1142
+ rendered = renderGroupedTable(before, after, schema, options);
1143
+ } else {
1144
+ const sectionBefore = pickSectionObject(before, schema.paths);
1145
+ const sectionAfter = pickSectionObject(after, schema.paths);
1146
+ rendered = renderValue(sectionBefore, sectionAfter, {
1147
+ ...options,
1148
+ label: schema.name,
1149
+ pathKey: schema.paths[0] || schema.name,
1150
+ depth: 0
1151
+ });
1152
+ }
1153
+ if (!rendered.html) return null;
1154
+ return { name: schema.name, html: rendered.html, count: rendered.count };
1155
+ }
1156
+ function renderValue(before, after, ctx) {
1157
+ const changed = hasChange(before, after, ctx.pathKey);
1158
+ if (!ctx.showUnchanged && !changed) return { html: "", changed: false, count: 0 };
1159
+ const scalar = scalarPair(before, after);
1160
+ if (scalar) {
1161
+ return {
1162
+ html: renderLine(ctx.label, scalar.before, scalar.after, statusOf(before, after), ctx.nodeStatus),
1163
+ changed,
1164
+ count: changed ? 1 : 0
1165
+ };
1166
+ }
1167
+ if (isCollection(before, after, ctx.pathKey)) {
1168
+ const rendered2 = renderArray(before, after, ctx);
1169
+ return { html: rendered2.html, changed, count: rendered2.count || (changed ? 1 : 0) };
1170
+ }
1171
+ const rendered = renderObject(before, after, ctx);
1172
+ return { html: rendered.html, changed, count: rendered.count || (changed ? 1 : 0) };
1173
+ }
1174
+ function renderObject(before, after, ctx) {
1175
+ const beforeObj = isObj(before) ? before : {};
1176
+ const afterObj = isObj(after) ? after : {};
1177
+ const keys = unionKeys(beforeObj, afterObj);
1178
+ const scalarRows = [];
1179
+ const nested = [];
1180
+ let count = 0;
1181
+ for (const key of keys) {
1182
+ const childBefore = getField(beforeObj, key);
1183
+ const childAfter = getField(afterObj, key);
1184
+ const childChanged = hasChange(childBefore, childAfter, key);
1185
+ const scalar = scalarPair(childBefore, childAfter);
1186
+ if (scalar) {
1187
+ if (ctx.showUnchanged || childChanged || ctx.depth > 0) {
1188
+ scalarRows.push(scalarRow(key, scalar.before, scalar.after, statusOf(childBefore, childAfter), ctx.nodeStatus));
1189
+ }
1190
+ if (childChanged) count += 1;
1191
+ continue;
1192
+ }
1193
+ const rendered = renderValue(childBefore, childAfter, {
1194
+ ...ctx,
1195
+ label: humanizePath(key),
1196
+ pathKey: key,
1197
+ depth: ctx.depth + 1
1198
+ });
1199
+ if (rendered.html) {
1200
+ nested.push("<div class='snapshot-nested'><div class='snapshot-nested-title'>" + escapeHtml(humanizePath(key)) + "</div>" + rendered.html + "</div>");
1201
+ count += rendered.count;
1202
+ }
1203
+ }
1204
+ const table = scalarRows.length ? "<table class='snapshot-kv'><tbody>" + scalarRows.join("") + "</tbody></table>" : "";
1205
+ return { html: table + nested.join(""), changed: count > 0, count };
1206
+ }
1207
+ function renderArray(before, after, ctx) {
1208
+ const beforeItems = arr(before);
1209
+ const afterItems = arr(after);
1210
+ if (looksLikeNameValueArray(beforeItems, afterItems)) {
1211
+ return renderTableFromItems(before, after, [""], [
1212
+ { path: "name", label: "Name" },
1213
+ { path: "value", label: "Value" }
1214
+ ], ctx);
1215
+ }
1216
+ const max = Math.max(beforeItems.length, afterItems.length);
1217
+ const cards = [];
1218
+ let count = 0;
1219
+ for (let i = 0; i < max; i += 1) {
1220
+ const childBefore = beforeItems[i];
1221
+ const childAfter = afterItems[i];
1222
+ const childChanged = hasChange(childBefore, childAfter, "");
1223
+ if (!ctx.showUnchanged && !childChanged) continue;
1224
+ const title = itemTitle(childBefore, childAfter, i);
1225
+ const rendered = renderValue(childBefore, childAfter, {
1226
+ ...ctx,
1227
+ label: title,
1228
+ pathKey: "",
1229
+ depth: ctx.depth + 1
1230
+ });
1231
+ if (!rendered.html) continue;
1232
+ count += rendered.count || (childChanged ? 1 : 0);
1233
+ cards.push("<div class='snapshot-card " + statusOf(childBefore, childAfter) + "'><div class='snapshot-card-title'>" + escapeHtml(title) + "</div>" + rendered.html + "</div>");
1234
+ }
1235
+ return { html: cards.join(""), changed: count > 0, count };
1236
+ }
1237
+ function renderTable(before, after, paths, columns, options) {
1238
+ return renderTableFromItems(before, after, paths, columns, { ...options, label: "", pathKey: paths[0] || "", depth: 0 });
1239
+ }
1240
+ function renderTableFromItems(before, after, paths, columns, options) {
1241
+ const rows = [];
1242
+ for (const path of paths) {
1243
+ const beforeItems = arr(resolvePath(before, path));
1244
+ const afterItems = arr(resolvePath(after, path));
1245
+ const max = Math.max(beforeItems.length, afterItems.length);
1246
+ for (let i = 0; i < max; i += 1) {
1247
+ const itemBefore = beforeItems[i];
1248
+ const itemAfter = afterItems[i];
1249
+ if (!options.showUnchanged && !hasChange(itemBefore, itemAfter, path)) continue;
1250
+ const cells = {};
1251
+ for (const col of columns) {
1252
+ const cellBefore = resolvePath(itemBefore, col.path);
1253
+ const cellAfter = resolvePath(itemAfter, col.path);
1254
+ cells[col.path] = {
1255
+ before: unwrapValue(cellBefore),
1256
+ after: unwrapValue(cellAfter),
1257
+ changed: hasChange(cellBefore, cellAfter, col.path)
1258
+ };
1259
+ }
1260
+ rows.push({ idx: i, kind: statusOf(itemBefore, itemAfter), cells });
1261
+ }
1262
+ }
1263
+ if (!rows.length) return { html: "", changed: false, count: 0 };
1264
+ const showChange = options.nodeStatus === "modified" && !options.suppressChangeColumn;
1265
+ const head = "<tr><th class='row-idx'>#</th>" + columns.map((col) => "<th>" + escapeHtml(col.label) + "</th>").join("") + (showChange ? "<th>Change</th>" : "") + "</tr>";
1266
+ const body = rows.map((row) => renderRow(row, columns, showChange, options.nodeStatus)).join("");
1267
+ return {
1268
+ html: "<table class='change-table'><thead>" + head + "</thead><tbody>" + body + "</tbody></table>",
1269
+ changed: rows.some((row) => row.kind !== "unchanged" || Object.values(row.cells).some((cell) => cell.changed)),
1270
+ count: rows.filter((row) => row.kind !== "unchanged" || Object.values(row.cells).some((cell) => cell.changed)).length
1271
+ };
1272
+ }
1273
+ function renderRow(row, columns, showChange, nodeStatus) {
1274
+ const cells = columns.map((col) => {
1275
+ const cell = row.cells[col.path];
1276
+ return "<td>" + renderValueCell(cell.before, cell.after, cell.changed ? row.kind : "unchanged", nodeStatus) + "</td>";
1277
+ }).join("");
1278
+ const badge = showChange ? "<td><span class='change-kind " + row.kind + "'>" + capitalize(row.kind) + "</span></td>" : "";
1279
+ return "<tr class='row-" + row.kind + "'><td class='row-idx'>" + (row.idx + 1) + "</td>" + cells + badge + "</tr>";
1280
+ }
1281
+ function renderGroupedTable(before, after, schema, options) {
1282
+ const groups = [];
1283
+ let count = 0;
1284
+ for (const path of schema.paths) {
1285
+ const beforeGroups = arr(resolvePath(before, path));
1286
+ const afterGroups = arr(resolvePath(after, path));
1287
+ const max = Math.max(beforeGroups.length, afterGroups.length);
1288
+ for (let i = 0; i < max; i += 1) {
1289
+ const groupBefore = beforeGroups[i];
1290
+ const groupAfter = afterGroups[i];
1291
+ if (!options.showUnchanged && !hasChange(groupBefore, groupAfter, path)) continue;
1292
+ const kind = statusOf(groupBefore, groupAfter);
1293
+ const present = groupAfter !== void 0 ? groupAfter : groupBefore;
1294
+ const labelValue = schema.groupLabelPath && present ? resolvePath(present, schema.groupLabelPath) : void 0;
1295
+ const label = labelValue != null ? stripHtml(String(labelValue)) : "Outcome " + (i + 1);
1296
+ const scalarRows = renderGroupScalars(groupBefore, groupAfter, schema, options);
1297
+ const inner = renderTableFromItems(groupBefore, groupAfter, [schema.innerArray || ""], schema.innerColumns || [], {
1298
+ ...options,
1299
+ suppressChangeColumn: kind !== "modified",
1300
+ label,
1301
+ pathKey: schema.innerArray || "",
1302
+ depth: 1
1303
+ }).html;
1304
+ const head = "<div class='group-head'><span class='change-kind " + kind + "'>" + capitalize(kind) + "</span><span class='group-label'>" + escapeHtml(label) + "</span></div>";
1305
+ groups.push("<div class='outcome-group'>" + head + scalarRows + inner + "</div>");
1306
+ count += 1;
1307
+ }
1308
+ }
1309
+ return { html: groups.join(""), changed: count > 0, count };
1310
+ }
1311
+ function renderGroupScalars(before, after, schema, options) {
1312
+ const beforeObj = isObj(before) ? before : {};
1313
+ const afterObj = isObj(after) ? after : {};
1314
+ const skip = new Set([schema.innerArray, schema.groupLabelPath].filter(Boolean));
1315
+ const rows = unionKeys(beforeObj, afterObj).filter((key) => !skip.has(key)).map((key) => {
1316
+ const cellBefore = getField(beforeObj, key);
1317
+ const cellAfter = getField(afterObj, key);
1318
+ const scalar = scalarPair(cellBefore, cellAfter);
1319
+ if (!scalar) return "";
1320
+ if (!options.showUnchanged && !hasChange(cellBefore, cellAfter, key)) return "";
1321
+ return scalarRow(key, scalar.before, scalar.after, statusOf(cellBefore, cellAfter), options.nodeStatus);
1322
+ }).filter(Boolean).join("");
1323
+ return rows ? "<table class='snapshot-kv'><tbody>" + rows + "</tbody></table>" : "";
1324
+ }
1325
+ function scalarRow(key, before, after, kind, nodeStatus) {
1326
+ return "<tr><th>" + escapeHtml(humanizePath(key)) + "</th><td>" + renderValueCell(before, after, kind, nodeStatus) + "</td></tr>";
1327
+ }
1328
+ function renderLine(label, before, after, kind, nodeStatus) {
1329
+ const badge = nodeStatus === "modified" ? "<span class='change-kind " + kind + "'>" + capitalize(kind) + "</span>" : "";
1330
+ 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>";
1331
+ }
1332
+ function renderValueCell(before, after, kind, nodeStatus) {
1333
+ if (nodeStatus === "added") return oneSidedVal(after !== void 0 ? after : before);
1334
+ if (nodeStatus === "deleted") return oneSidedVal(before !== void 0 ? before : after);
1335
+ if (kind === "added") return insVal(after !== void 0 ? after : before);
1336
+ if (kind === "removed") return delVal(before !== void 0 ? before : after);
1337
+ if (kind === "modified") return diffValue(before, after);
1338
+ return faintVal(after !== void 0 ? after : before);
1339
+ }
1340
+ function statusOf(before, after) {
1341
+ if (before === void 0) return "added";
1342
+ if (after === void 0) return "removed";
1343
+ return valuesEqual(before, after) ? "unchanged" : "modified";
1344
+ }
1345
+ function hasChange(before, after, pathKey = "") {
1346
+ if (before === void 0 || after === void 0) return before !== after;
1347
+ const scalar = scalarPair(before, after);
1348
+ if (scalar) return !valuesEqual(scalar.before, scalar.after);
1349
+ if (isCollection(before, after, pathKey)) {
1350
+ const beforeItems = arr(before);
1351
+ const afterItems = arr(after);
1352
+ const max = Math.max(beforeItems.length, afterItems.length);
1353
+ for (let i = 0; i < max; i += 1) {
1354
+ if (hasChange(beforeItems[i], afterItems[i], "")) return true;
1355
+ }
1356
+ return false;
1357
+ }
1358
+ const beforeObj = isObj(before) ? before : {};
1359
+ const afterObj = isObj(after) ? after : {};
1360
+ for (const key of unionKeys(beforeObj, afterObj)) {
1361
+ if (hasChange(beforeObj[key], afterObj[key], key)) return true;
1362
+ }
1363
+ return false;
1364
+ }
1365
+ function scalarPair(before, after) {
1366
+ const beforeUnwrapped = unwrapValue(before);
1367
+ const afterUnwrapped = unwrapValue(after);
1368
+ const beforeScalar = beforeUnwrapped == null || typeof beforeUnwrapped !== "object";
1369
+ const afterScalar = afterUnwrapped == null || typeof afterUnwrapped !== "object";
1370
+ return beforeScalar && afterScalar ? { before: beforeUnwrapped, after: afterUnwrapped } : null;
1371
+ }
1372
+ function arr(value) {
1373
+ if (value === void 0 || value === null) return [];
1374
+ return Array.isArray(value) ? value : [value];
1375
+ }
1376
+ function isObj(value) {
1377
+ return !!value && typeof value === "object" && !Array.isArray(value);
1378
+ }
1379
+ function isCollection(before, after, pathKey) {
1380
+ return Array.isArray(before) || Array.isArray(after) || isCollectionKey(pathKey);
1381
+ }
1382
+ function isCollectionKey(key) {
1383
+ return COLLECTION_KEYS.has(key);
1384
+ }
1385
+ function unionKeys(before, after) {
1386
+ return [.../* @__PURE__ */ new Set([...Object.keys(before || {}), ...Object.keys(after || {})])].sort();
1387
+ }
1388
+ function ownsAny(paths, key) {
1389
+ for (const path of paths) {
1390
+ if (key === path || key.startsWith(path + ".") || key.startsWith(path + "[")) return true;
1391
+ }
1392
+ return false;
1393
+ }
1394
+ function pickSectionObject(obj, paths) {
1395
+ if (!obj) return void 0;
1396
+ const out = {};
1397
+ for (const path of paths) {
1398
+ const value = resolvePath(obj, path);
1399
+ if (value !== void 0) out[path] = value;
1400
+ }
1401
+ return Object.keys(out).length ? out : void 0;
1402
+ }
1403
+ function getField(obj, key) {
1404
+ return obj ? obj[key] : void 0;
1405
+ }
1406
+ function resolvePath(obj, path) {
1407
+ if (obj == null || !path) return obj;
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 void 0;
1412
+ cur = token[0] === "[" ? cur[Number(token.slice(1, -1))] : cur[token];
1413
+ }
1414
+ return cur;
1415
+ }
1416
+ function looksLikeNameValueArray(beforeItems, afterItems) {
1417
+ const items = [...beforeItems, ...afterItems].filter(isObj);
1418
+ return items.length > 0 && items.every((item) => "name" in item && "value" in item);
1419
+ }
1420
+ function itemTitle(before, after, index) {
1421
+ const item = isObj(after) ? after : before;
1422
+ if (item) {
1423
+ for (const key of ["fieldText", "label", "name", "fieldType", "field"]) {
1424
+ const value = unwrapValue(item[key]);
1425
+ if (value !== void 0 && value !== null && typeof value !== "object") {
1426
+ return stripHtml(String(value));
1427
+ }
1428
+ }
1429
+ }
1430
+ return "Item " + (index + 1);
1431
+ }
1432
+ function stripHtml(value) {
1433
+ return value.replace(/<[^>]*>/g, " ").replace(/\s+/g, " ").trim();
1434
+ }
1435
+ function capitalize(value) {
1436
+ return value.charAt(0).toUpperCase() + value.slice(1);
1437
+ }
1438
+ function humanizePath(path) {
1439
+ const names = {
1440
+ assignmentItems: "Assignment item",
1441
+ choiceReferences: "Choice reference",
1442
+ choices: "Choice",
1443
+ conditions: "Condition",
1444
+ customErrorMessages: "Custom error message",
1445
+ dataTypeMappings: "Data type mapping",
1446
+ fields: "Field",
1447
+ filters: "Filter",
1448
+ inputAssignments: "Input assignment",
1449
+ inputParameters: "Input parameter",
1450
+ outputParameters: "Output parameter",
1451
+ processMetadataValues: "Process metadata value",
1452
+ rules: "Rule",
1453
+ scheduledPaths: "Scheduled path",
1454
+ storeOutputParameters: "Store output parameter",
1455
+ transformValues: "Transform value",
1456
+ valueMappings: "Value mapping",
1457
+ waitEvents: "Wait event"
1458
+ };
1459
+ return path.split(".").map((segment) => {
1460
+ const match = segment.match(/^([^[]+)(?:\[(\d+)\])?$/);
1461
+ if (!match) return segment;
1462
+ const name = names[match[1]] || match[1].replace(/([a-z])([A-Z])/g, "$1 $2").replace(/^./, (char) => char.toUpperCase());
1463
+ return match[2] === void 0 ? name : name + " " + (Number(match[2]) + 1);
1464
+ }).join(" > ");
1465
+ }
1466
+ function unwrapValue(value) {
1467
+ if (value === null || value === void 0 || typeof value !== "object" || Array.isArray(value)) return value;
1468
+ if ("elementReference" in value) return "{!" + value.elementReference + "}";
1469
+ const keys = Object.keys(value);
1470
+ if (keys.length === 1) {
1471
+ const key = keys[0];
1472
+ const obj = value;
1473
+ if (key === "stringValue" || key === "numberValue" || key === "dateValue" || key === "dateTimeValue") return obj[key];
1474
+ if (key === "booleanValue") return String(obj[key]) === "true" ? "true" : "false";
1475
+ }
1476
+ return value;
1477
+ }
1478
+ function valuesEqual(a, b) {
1479
+ if (a === b) return true;
1480
+ if (a && b && typeof a === "object" && typeof b === "object") return JSON.stringify(a) === JSON.stringify(b);
1481
+ return false;
1482
+ }
1483
+ function renderScalar(value) {
1484
+ if (value === void 0) return "<em>-</em>";
1485
+ if (value === null) return "<em>empty</em>";
1486
+ if (typeof value === "object") return "<pre>" + escapeHtml(JSON.stringify(value, null, 2)) + "</pre>";
1487
+ const text = String(value);
1488
+ if (text.length > LONG_TEXT_THRESHOLD || text.includes("\n")) {
1489
+ return "<pre>" + escapeHtml(text) + "</pre>";
1490
+ }
1491
+ return escapeHtml(text);
1492
+ }
1493
+ function insVal(value) {
1494
+ return "<span class='val ins'>" + renderScalar(value) + "</span>";
1495
+ }
1496
+ function delVal(value) {
1497
+ return "<span class='val del'>" + renderScalar(value) + "</span>";
1498
+ }
1499
+ function faintVal(value) {
1500
+ return "<span class='val faint'>" + renderScalar(value) + "</span>";
1501
+ }
1502
+ function oneSidedVal(value) {
1503
+ return "<span class='val one-sided'>" + renderScalar(value) + "</span>";
1504
+ }
1505
+ function diffValue(before, after) {
1506
+ before = unwrapValue(before);
1507
+ after = unwrapValue(after);
1508
+ if (before === void 0) return insVal(after);
1509
+ if (after === void 0) return delVal(before);
1510
+ return delVal(before) + " <span class='arrow'>-></span> " + insVal(after);
1511
+ }
1512
+ function escapeHtml(value) {
1513
+ return String(value).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
1514
+ }
1515
+ function snapshotPanelClientScript() {
1516
+ const functions = [
1517
+ renderNodePanelBody,
1518
+ renderSections,
1519
+ renderSchemaSection,
1520
+ renderValue,
1521
+ renderObject,
1522
+ renderArray,
1523
+ renderTable,
1524
+ renderTableFromItems,
1525
+ renderRow,
1526
+ renderGroupedTable,
1527
+ renderGroupScalars,
1528
+ scalarRow,
1529
+ renderLine,
1530
+ renderValueCell,
1531
+ statusOf,
1532
+ hasChange,
1533
+ scalarPair,
1534
+ arr,
1535
+ isObj,
1536
+ isCollection,
1537
+ isCollectionKey,
1538
+ unionKeys,
1539
+ ownsAny,
1540
+ pickSectionObject,
1541
+ getField,
1542
+ resolvePath,
1543
+ looksLikeNameValueArray,
1544
+ itemTitle,
1545
+ stripHtml,
1546
+ capitalize,
1547
+ humanizePath,
1548
+ unwrapValue,
1549
+ valuesEqual,
1550
+ renderScalar,
1551
+ insVal,
1552
+ delVal,
1553
+ faintVal,
1554
+ oneSidedVal,
1555
+ diffValue,
1556
+ escapeHtml
1557
+ ].map((fn) => fn.toString()).join("\n\n");
1558
+ return "const LONG_TEXT_THRESHOLD = " + LONG_TEXT_THRESHOLD + ";\nconst COLLECTION_KEYS = new Set(" + JSON.stringify(COLLECTION_KEY_NAMES) + ");\n\n" + functions;
1559
+ }
1560
+
928
1561
  // src/render/render-html.ts
929
1562
  function renderHtml(layout) {
930
1563
  const data = {
@@ -942,12 +1575,13 @@ function renderHtml(layout) {
942
1575
  const edgeStat = s.addedEdges + s.removedEdges > 0 ? `<span class="sep">\xB7</span><span class="stat edges">+${s.addedEdges}/\u2212${s.removedEdges} edges</span>` : "";
943
1576
  const metaHtml = `${stat(s.addedNodes, "added", "added")}<span class="sep">\xB7</span>${stat(s.removedNodes, "deleted", "deleted")}<span class="sep">\xB7</span>${stat(s.modifiedNodes, "modified", "modified")}${edgeStat}`;
944
1577
  const viewBox = fitViewBox(layout.width, layout.height);
1578
+ const flowBannerHtml = renderFlowBanner(layout.diff.flowChanges);
945
1579
  return `<!doctype html>
946
1580
  <html lang="en">
947
1581
  <head>
948
1582
  <meta charset="utf-8" />
949
1583
  <meta name="viewport" content="width=device-width, initial-scale=1" />
950
- <title>${escapeHtml(layout.diff.flowName)}</title>
1584
+ <title>${escapeHtml2(layout.diff.flowName)}</title>
951
1585
  <style>
952
1586
  :root {
953
1587
  color-scheme: light;
@@ -1035,6 +1669,11 @@ function renderHtml(layout) {
1035
1669
  .panel.modified .badge { background: var(--modified-fill); color: var(--modified); }
1036
1670
  .panel .empty { color: var(--muted); font-size: 13px; }
1037
1671
  .detail-section { margin: 0 0 12px; border: 1px solid var(--border); border-radius: 8px; background: #fbfcfe; overflow: hidden; }
1672
+ .detail-section.snapshot-section { border-left-width: 3px; }
1673
+ .detail-section.snapshot-section.added { border-left-color: rgba(22, 163, 74, 0.5); }
1674
+ .detail-section.snapshot-section.deleted { border-left-color: rgba(220, 38, 38, 0.45); }
1675
+ .detail-section.snapshot-section.added summary { background: rgba(220, 252, 231, 0.35); }
1676
+ .detail-section.snapshot-section.deleted summary { background: rgba(254, 226, 226, 0.35); }
1038
1677
  .detail-section summary { cursor: pointer; padding: 11px 13px; font-weight: 650; }
1039
1678
  .detail-section[open] summary { border-bottom: 1px solid var(--border); }
1040
1679
  .section-count { color: var(--muted); font-size: 11px; font-weight: 500; }
@@ -1048,8 +1687,9 @@ function renderHtml(layout) {
1048
1687
  .val.ins { color: #14532d; background: var(--added-fill); }
1049
1688
  .val.del { color: #7f1d1d; background: var(--deleted-fill); text-decoration: line-through; text-decoration-color: rgba(127, 29, 29, 0.5); }
1050
1689
  .val.faint { color: var(--muted); background: transparent; padding-left: 0; }
1690
+ .val.one-sided { color: var(--text); background: transparent; padding-left: 0; }
1051
1691
  .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; }
1692
+ .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
1693
  .arrow { color: var(--faint); margin: 0 2px; }
1054
1694
  /* scalar / fallback change lines */
1055
1695
  .changes { list-style: none; padding: 0; margin: 0; }
@@ -1066,12 +1706,31 @@ function renderHtml(layout) {
1066
1706
  .change-table .row-idx { color: var(--faint); font-variant-numeric: tabular-nums; width: 1%; white-space: nowrap; }
1067
1707
  .change-table tr.row-added { background: rgba(22, 163, 74, 0.06); }
1068
1708
  .change-table tr.row-removed { background: rgba(220, 38, 38, 0.05); }
1709
+ .snapshot-kv { width: 100%; border-collapse: collapse; font-size: 12px; margin-bottom: 10px; }
1710
+ .snapshot-kv th, .snapshot-kv td { padding: 6px 7px; border-bottom: 1px solid var(--border); text-align: left; vertical-align: top; overflow-wrap: anywhere; }
1711
+ .snapshot-kv th { color: var(--muted); width: 34%; font-weight: 650; }
1712
+ .snapshot-kv tr:last-child th, .snapshot-kv tr:last-child td { border-bottom: 0; }
1713
+ .snapshot-nested { border-left: 2px solid var(--border); padding-left: 10px; margin: 10px 0; }
1714
+ .snapshot-nested-title { color: var(--muted); font-size: 11px; font-weight: 700; letter-spacing: 0.04em; text-transform: uppercase; margin-bottom: 7px; }
1715
+ .snapshot-card { border: 1px dashed var(--border); border-radius: 7px; padding: 9px 10px; margin: 8px 0; background: var(--surface); }
1716
+ .snapshot-card.added { border-left: 2px solid rgba(22, 163, 74, 0.5); }
1717
+ .snapshot-card.removed { border-left: 2px solid rgba(220, 38, 38, 0.45); }
1718
+ .snapshot-card-title { font-size: 12px; font-weight: 700; margin-bottom: 7px; overflow-wrap: anywhere; }
1069
1719
  /* outcome groups (decisions) */
1070
1720
  .outcome-group { margin-bottom: 12px; padding: 10px 11px; border: 1px solid var(--border); border-radius: 7px; background: var(--surface); }
1071
1721
  .outcome-group:last-child { margin-bottom: 0; }
1072
1722
  .group-head { display: flex; align-items: center; gap: 7px; margin-bottom: 8px; }
1073
1723
  .group-label { font-weight: 700; font-size: 12.5px; }
1074
1724
  .outcome-group .changes { margin-bottom: 8px; }
1725
+ .flow-banner { border-bottom: 1px solid var(--border); background: #fffaf0; padding: 12px 20px; }
1726
+ .flow-banner-callout { margin-bottom: 8px; font-weight: 750; }
1727
+ .flow-banner-callout.deactivated { color: var(--deleted); }
1728
+ .flow-banner-callout.activated { color: var(--added); }
1729
+ .flow-banner-callout.neutral { color: var(--modified); }
1730
+ .flow-banner-list { display: grid; gap: 7px; max-width: 960px; }
1731
+ .flow-change-row { display: grid; grid-template-columns: minmax(120px, 190px) minmax(0, 1fr); align-items: start; gap: 10px; }
1732
+ .flow-change-label { color: var(--muted); font-size: 12px; font-weight: 700; }
1733
+ .flow-change-value { min-width: 0; max-height: 180px; overflow: auto; }
1075
1734
  .edge { fill: none; }
1076
1735
  .edge.unchanged { stroke-width: 1.4; opacity: 0.55; }
1077
1736
  .edge.normal.unchanged { stroke: var(--edge); marker-end: url(#arrow-normal); }
@@ -1110,7 +1769,7 @@ function renderHtml(layout) {
1110
1769
  <body>
1111
1770
  <header>
1112
1771
  <div>
1113
- <h1>${escapeHtml(layout.diff.flowName)}</h1>
1772
+ <h1>${escapeHtml2(layout.diff.flowName)}</h1>
1114
1773
  <div class="meta">${metaHtml}</div>
1115
1774
  </div>
1116
1775
  <div class="header-actions">
@@ -1128,6 +1787,7 @@ function renderHtml(layout) {
1128
1787
  </div>
1129
1788
  </div>
1130
1789
  </header>
1790
+ ${flowBannerHtml}
1131
1791
  <main>
1132
1792
  <div class="canvas">
1133
1793
  <svg id="flow-svg" viewBox="${viewBox}" preserveAspectRatio="xMidYMid meet" role="img" aria-label="Flow diff">
@@ -1178,14 +1838,6 @@ function renderHtml(layout) {
1178
1838
  let dragStart = null;
1179
1839
  let selectedNodeId = null;
1180
1840
 
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
1841
  function isNodeVisible(node, mode) {
1190
1842
  if (mode === "after") return node.status !== "deleted";
1191
1843
  if (mode === "before") return node.status !== "added";
@@ -1311,237 +1963,10 @@ function renderHtml(layout) {
1311
1963
  panel.className = "panel " + node.status;
1312
1964
  panelTitle.textContent = node.label;
1313
1965
  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, {});
1966
+ panelBody.innerHTML = renderNodePanelBody(node, DATA.sectionSchemas[node.type] || []);
1389
1967
  }
1390
1968
 
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>";
1485
- }
1486
-
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
- }
1969
+ ${snapshotPanelClientScript()}
1545
1970
 
1546
1971
  function selectNode(id) {
1547
1972
  selectedNodeId = id;
@@ -1552,70 +1977,6 @@ function renderHtml(layout) {
1552
1977
  if (node) updatePanel(node);
1553
1978
  }
1554
1979
 
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
1980
  function pickInitialNode(visibleNodeIds) {
1620
1981
  return DATA.nodes.find((node) => visibleNodeIds.has(node.id) && node.status === "modified")
1621
1982
  || DATA.nodes.find((node) => visibleNodeIds.has(node.id) && node.status !== "unchanged")
@@ -1766,6 +2127,48 @@ function renderHtml(layout) {
1766
2127
  </body>
1767
2128
  </html>`;
1768
2129
  }
2130
+ function renderFlowBanner(changes) {
2131
+ if (!changes || changes.length === 0) {
2132
+ return "";
2133
+ }
2134
+ const statusChange = changes.find((change) => change.path === "status");
2135
+ const callout = statusChange ? renderStatusCallout(statusChange.before, statusChange.after) : "";
2136
+ return `<section class="flow-banner" aria-label="Flow-level changes">
2137
+ ${callout}
2138
+ <div class="flow-banner-list">
2139
+ ${changes.map((change) => `<div class="flow-change-row">
2140
+ <div class="flow-change-label">${escapeHtml2(humanizePath(change.path))}</div>
2141
+ <div class="flow-change-value">${renderValueDelta(change.before, change.after)}</div>
2142
+ </div>`).join("")}
2143
+ </div>
2144
+ </section>`;
2145
+ }
2146
+ function renderStatusCallout(before, after) {
2147
+ const beforeText = String(before);
2148
+ const afterText = String(after);
2149
+ if (beforeText === "Active" && afterText !== "Active") {
2150
+ return `<div class="flow-banner-callout deactivated">Deactivated (${escapeHtml2(beforeText)} -> ${escapeHtml2(afterText)})</div>`;
2151
+ }
2152
+ if (beforeText !== "Active" && afterText === "Active") {
2153
+ return `<div class="flow-banner-callout activated">Activated (${escapeHtml2(beforeText)} -> ${escapeHtml2(afterText)})</div>`;
2154
+ }
2155
+ return `<div class="flow-banner-callout neutral">Status: ${escapeHtml2(beforeText)} -> ${escapeHtml2(afterText)}</div>`;
2156
+ }
2157
+ function renderValueDelta(before, after) {
2158
+ if (before === void 0) {
2159
+ return `<span class="val ins">${escapeHtml2(formatValue(after))}</span>`;
2160
+ }
2161
+ if (after === void 0) {
2162
+ return `<span class="val del">${escapeHtml2(formatValue(before))}</span>`;
2163
+ }
2164
+ return `<span class="val del">${escapeHtml2(formatValue(before))}</span><span class="arrow">-></span><span class="val ins">${escapeHtml2(formatValue(after))}</span>`;
2165
+ }
2166
+ function formatValue(value) {
2167
+ if (value === void 0) {
2168
+ return "(missing)";
2169
+ }
2170
+ return String(value);
2171
+ }
1769
2172
  function fitViewBox(contentWidth, contentHeight) {
1770
2173
  const minWidth = 720;
1771
2174
  const minHeight = 540;
@@ -1789,16 +2192,16 @@ function renderEdge(edge) {
1789
2192
  return "";
1790
2193
  }
1791
2194
  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>`;
2195
+ return `<path class="edge ${edge.kind} ${edge.status}" d="${d}" data-edge-id="${escapeHtml2(edge.id)}"></path>`;
1793
2196
  }
1794
2197
  function renderNode(node) {
1795
2198
  const lines = wrapLabel(node.label, 22);
1796
2199
  const lineHeight = 14;
1797
2200
  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})">
2201
+ return `<g class="node ${node.status} type-${node.type}" data-node-id="${escapeHtml2(node.id)}" transform="translate(${node.x},${node.y})">
1799
2202
  <rect width="${node.width}" height="${node.height}"></rect>
1800
2203
  <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("")}
2204
+ ${lines.map((line, index) => `<tspan x="${node.width / 2}" dy="${index === 0 ? 0 : lineHeight}">${escapeHtml2(line)}</tspan>`).join("")}
1802
2205
  </text>
1803
2206
  </g>`;
1804
2207
  }
@@ -1820,7 +2223,7 @@ function wrapLabel(label, maxChars) {
1820
2223
  }
1821
2224
  return lines.length ? lines.slice(0, 3) : [label];
1822
2225
  }
1823
- function escapeHtml(value) {
2226
+ function escapeHtml2(value) {
1824
2227
  return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
1825
2228
  }
1826
2229
 
@@ -1929,8 +2332,10 @@ async function main(argv = process.argv.slice(2)) {
1929
2332
  }
1930
2333
  }
1931
2334
  async function runFileMode(oldPath, newPath, outDir, writeJson) {
1932
- const oldModel = buildModel(await parseXml(readFlowFromFile(oldPath)));
1933
- const newModel = buildModel(await parseXml(readFlowFromFile(newPath)));
2335
+ const oldXml = readFlowFromFile(oldPath);
2336
+ const newXml = readFlowFromFile(newPath);
2337
+ const oldModel = await buildModelWithHeader(oldXml);
2338
+ const newModel = await buildModelWithHeader(newXml);
1934
2339
  await writeArtifacts(oldModel, newModel, outDir, writeJson);
1935
2340
  }
1936
2341
  async function runGitMode(repo, from, to, pattern, outDir, writeJson, changedOnly) {
@@ -1940,8 +2345,8 @@ async function runGitMode(repo, from, to, pattern, outDir, writeJson, changedOnl
1940
2345
  try {
1941
2346
  const oldXml = readFlowFromGit(repo, from, filePath);
1942
2347
  const newXml = readFlowFromGit(repo, to, filePath);
1943
- const oldModel = oldXml ? buildModel(await parseXml(oldXml)) : buildEmptyModel();
1944
- const newModel = newXml ? buildModel(await parseXml(newXml)) : buildEmptyModel();
2348
+ const oldModel = oldXml ? await buildModelWithHeader(oldXml) : buildEmptyModel();
2349
+ const newModel = newXml ? await buildModelWithHeader(newXml) : buildEmptyModel();
1945
2350
  await writeArtifacts(oldModel, newModel, outDir, writeJson, filePath);
1946
2351
  } catch (error) {
1947
2352
  hadFailure = true;
@@ -1956,6 +2361,11 @@ async function parseXml(xml) {
1956
2361
  const parser = new FlowParser(xml);
1957
2362
  return parser.generateFlowDefinition();
1958
2363
  }
2364
+ async function buildModelWithHeader(xml) {
2365
+ const model = buildModel(await parseXml(xml));
2366
+ model.header = await extractFlowHeader(xml);
2367
+ return model;
2368
+ }
1959
2369
  function buildEmptyModel() {
1960
2370
  return {
1961
2371
  flowName: "(unknown)",
@@ -1974,9 +2384,15 @@ async function writeArtifacts(oldModel, newModel, outDir, writeJson, sourcePath)
1974
2384
  writeFileSync(join(outDir, `${fileStem}.diff.json`), JSON.stringify(diff, null, 2), "utf8");
1975
2385
  }
1976
2386
  console.log(
1977
- `${diff.flowName}: nodes ${diff.summary.addedNodes} added, ${diff.summary.removedNodes} deleted, ${diff.summary.modifiedNodes} modified; edges ${diff.summary.addedEdges} added, ${diff.summary.removedEdges} deleted`
2387
+ `${diff.flowName}: nodes ${diff.summary.addedNodes} added, ${diff.summary.removedNodes} deleted, ${diff.summary.modifiedNodes} modified; edges ${diff.summary.addedEdges} added, ${diff.summary.removedEdges} deleted${formatFlowAttributeSummary(diff.flowChanges)}`
1978
2388
  );
1979
2389
  }
2390
+ function formatFlowAttributeSummary(flowChanges) {
2391
+ if (!flowChanges || flowChanges.length === 0) {
2392
+ return "";
2393
+ }
2394
+ return `; flow attributes: ${flowChanges.length} changed (${flowChanges.map((change) => change.path).join(", ")})`;
2395
+ }
1980
2396
  function discoverGitFiles(repo, from, to, pattern, changedOnly) {
1981
2397
  const matcher = createPathMatcher(pattern);
1982
2398
  if (!changedOnly) {