executable-stories-formatters 0.15.1 → 0.16.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.
package/dist/index.cjs CHANGED
@@ -187,6 +187,41 @@ function generateFeatureId(uri) {
187
187
  function generateScenarioId(featureId, scenarioName) {
188
188
  return `${featureId};${slugify(scenarioName)}`;
189
189
  }
190
+ function normalizeText(text2) {
191
+ return text2.toLowerCase().replace(/[^\p{L}\p{N}\s]/gu, " ").replace(/\s+/g, " ").trim();
192
+ }
193
+ function tokenize(text2) {
194
+ const normalized = normalizeText(text2);
195
+ return normalized.length === 0 ? [] : normalized.split(" ");
196
+ }
197
+ function behaviourFingerprint(input) {
198
+ const steps = input.steps.map((step) => `${step.keyword.toLowerCase()}:${normalizeText(step.text)}`).join("\n");
199
+ const covers = [...input.covers ?? []].map((path12) => path12.trim()).filter(Boolean).sort().join(",");
200
+ if (steps.length === 0 && covers.length === 0) return "";
201
+ return (0, import_node_crypto.createHash)("sha1").update(`${steps}\0${covers}`).digest("hex").slice(0, 16);
202
+ }
203
+ function jaccard(a, b) {
204
+ if (a.size === 0 && b.size === 0) return 1;
205
+ if (a.size === 0 || b.size === 0) return 0;
206
+ let intersection = 0;
207
+ for (const value of a) if (b.has(value)) intersection += 1;
208
+ return intersection / (a.size + b.size - intersection);
209
+ }
210
+ function stepTokens(steps) {
211
+ const tokens = /* @__PURE__ */ new Set();
212
+ for (const step of steps) {
213
+ for (const token of tokenize(step.text)) tokens.add(token);
214
+ }
215
+ return tokens;
216
+ }
217
+ function behaviourSimilarity(a, b) {
218
+ const aSteps = stepTokens(a.steps);
219
+ const bSteps = stepTokens(b.steps);
220
+ if (aSteps.size === 0 || bSteps.size === 0) return 0;
221
+ const stepScore = jaccard(aSteps, bSteps);
222
+ const titleScore = jaccard(new Set(tokenize(a.scenario)), new Set(tokenize(b.scenario)));
223
+ return stepScore * 0.7 + titleScore * 0.3;
224
+ }
190
225
 
191
226
  // src/converters/acl/steps.ts
192
227
  function deriveStepResults(steps, scenarioStatus, error) {
@@ -16808,6 +16843,10 @@ function titleFor(kind) {
16808
16843
  return "Added";
16809
16844
  case "removed":
16810
16845
  return "Removed";
16846
+ case "renamed":
16847
+ return "Renamed";
16848
+ case "moved":
16849
+ return "Moved";
16811
16850
  case "changed":
16812
16851
  return "Changed";
16813
16852
  default:
@@ -16856,6 +16895,8 @@ function createPrCommentSummary(diff, maxScenarios = 10) {
16856
16895
  addSection(lines, diff, "fixed", maxScenarios);
16857
16896
  addSection(lines, diff, "added", maxScenarios);
16858
16897
  addSection(lines, diff, "removed", maxScenarios);
16898
+ addSection(lines, diff, "renamed", maxScenarios);
16899
+ addSection(lines, diff, "moved", maxScenarios);
16859
16900
  addSection(lines, diff, "changed", maxScenarios);
16860
16901
  return lines.join("\n").trimEnd();
16861
16902
  }
@@ -16902,8 +16943,10 @@ function sortDiffs(scenarios) {
16902
16943
  fixed: 1,
16903
16944
  added: 2,
16904
16945
  removed: 3,
16905
- changed: 4,
16906
- unchanged: 5
16946
+ renamed: 4,
16947
+ moved: 5,
16948
+ changed: 6,
16949
+ unchanged: 7
16907
16950
  };
16908
16951
  return [...scenarios].sort((a, b) => {
16909
16952
  if (rank[a.kind] !== rank[b.kind]) {
@@ -16918,69 +16961,108 @@ function sortDiffs(scenarios) {
16918
16961
  return a.scenario.localeCompare(b.scenario);
16919
16962
  });
16920
16963
  }
16964
+ var SIMILARITY_THRESHOLD = 0.75;
16965
+ function identityInput(tc) {
16966
+ return {
16967
+ scenario: tc.story.scenario,
16968
+ sourceFile: tc.sourceFile,
16969
+ steps: tc.story.steps,
16970
+ covers: tc.story.covers
16971
+ };
16972
+ }
16973
+ function changedFieldsOf(flags) {
16974
+ return Object.entries(flags).filter(([, changed]) => changed).map(([field]) => field);
16975
+ }
16976
+ function allChangedFlags(errorMessage) {
16977
+ return {
16978
+ status: true,
16979
+ steps: true,
16980
+ docs: true,
16981
+ tags: true,
16982
+ tickets: true,
16983
+ source: true,
16984
+ duration: true,
16985
+ attachments: true,
16986
+ error: Boolean(errorMessage),
16987
+ titlePath: true
16988
+ };
16989
+ }
16990
+ function matchIdentities(removed, added) {
16991
+ const pairs = [];
16992
+ const remainingRemoved = new Set(removed);
16993
+ const remainingAdded = new Set(added);
16994
+ const removedByFp = /* @__PURE__ */ new Map();
16995
+ const addedByFp = /* @__PURE__ */ new Map();
16996
+ for (const tc of remainingRemoved) {
16997
+ const fp = behaviourFingerprint(identityInput(tc));
16998
+ if (fp === "") continue;
16999
+ (removedByFp.get(fp) ?? removedByFp.set(fp, []).get(fp)).push(tc);
17000
+ }
17001
+ for (const tc of remainingAdded) {
17002
+ const fp = behaviourFingerprint(identityInput(tc));
17003
+ if (fp === "") continue;
17004
+ (addedByFp.get(fp) ?? addedByFp.set(fp, []).get(fp)).push(tc);
17005
+ }
17006
+ for (const [fp, removedGroup] of removedByFp) {
17007
+ const addedGroup = addedByFp.get(fp);
17008
+ if (removedGroup.length === 1 && addedGroup && addedGroup.length === 1) {
17009
+ pairs.push({ before: removedGroup[0], after: addedGroup[0], confidence: 1, matchedBy: "fingerprint" });
17010
+ remainingRemoved.delete(removedGroup[0]);
17011
+ remainingAdded.delete(addedGroup[0]);
17012
+ }
17013
+ }
17014
+ for (const before of [...remainingRemoved]) {
17015
+ let best;
17016
+ let bestScore = 0;
17017
+ let tied = false;
17018
+ for (const after of remainingAdded) {
17019
+ const score = behaviourSimilarity(identityInput(before), identityInput(after));
17020
+ if (score > bestScore) {
17021
+ bestScore = score;
17022
+ best = after;
17023
+ tied = false;
17024
+ } else if (score === bestScore) {
17025
+ tied = true;
17026
+ }
17027
+ }
17028
+ if (best && !tied && bestScore >= SIMILARITY_THRESHOLD) {
17029
+ pairs.push({ before, after: best, confidence: Math.round(bestScore * 100) / 100, matchedBy: "similarity" });
17030
+ remainingRemoved.delete(before);
17031
+ remainingAdded.delete(best);
17032
+ }
17033
+ }
17034
+ return {
17035
+ pairs,
17036
+ unmatchedRemoved: [...remainingRemoved],
17037
+ unmatchedAdded: [...remainingAdded]
17038
+ };
17039
+ }
17040
+ function identityKind(before, after) {
17041
+ const titleChanged = before.story.scenario !== after.story.scenario;
17042
+ const fileChanged = before.sourceFile !== after.sourceFile;
17043
+ return fileChanged && !titleChanged ? "moved" : "renamed";
17044
+ }
16921
17045
  function diffRuns(baseline, current) {
16922
17046
  const baselineById = new Map(baseline.testCases.map((tc) => [tc.id, tc]));
16923
17047
  const currentById = new Map(current.testCases.map((tc) => [tc.id, tc]));
16924
17048
  const ids = /* @__PURE__ */ new Set([...baselineById.keys(), ...currentById.keys()]);
16925
17049
  const scenarios = [];
17050
+ const removedCases = [];
17051
+ const addedCases = [];
16926
17052
  for (const id of ids) {
16927
17053
  const before = baselineById.get(id);
16928
17054
  const after = currentById.get(id);
16929
17055
  if (!before && after) {
16930
- const flags2 = {
16931
- status: true,
16932
- steps: true,
16933
- docs: true,
16934
- tags: true,
16935
- tickets: true,
16936
- source: true,
16937
- duration: true,
16938
- attachments: true,
16939
- error: Boolean(after.errorMessage),
16940
- titlePath: true
16941
- };
16942
- scenarios.push({
16943
- kind: "added",
16944
- id,
16945
- scenario: after.story.scenario,
16946
- sourceFile: after.sourceFile,
16947
- sourceLine: after.sourceLine,
16948
- current: toScenarioSnapshot(after),
16949
- flags: flags2,
16950
- changedFields: Object.entries(flags2).filter(([, changed]) => changed).map(([field]) => field)
16951
- });
17056
+ addedCases.push(after);
16952
17057
  continue;
16953
17058
  }
16954
17059
  if (before && !after) {
16955
- const flags2 = {
16956
- status: true,
16957
- steps: true,
16958
- docs: true,
16959
- tags: true,
16960
- tickets: true,
16961
- source: true,
16962
- duration: true,
16963
- attachments: true,
16964
- error: Boolean(before.errorMessage),
16965
- titlePath: true
16966
- };
16967
- scenarios.push({
16968
- kind: "removed",
16969
- id,
16970
- scenario: before.story.scenario,
16971
- sourceFile: before.sourceFile,
16972
- sourceLine: before.sourceLine,
16973
- baseline: toScenarioSnapshot(before),
16974
- flags: flags2,
16975
- changedFields: Object.entries(flags2).filter(([, changed]) => changed).map(([field]) => field)
16976
- });
16977
- continue;
16978
- }
16979
- if (!before || !after) {
17060
+ removedCases.push(before);
16980
17061
  continue;
16981
17062
  }
17063
+ if (!before || !after) continue;
16982
17064
  const flags = buildFlags(before, after);
16983
- const changedFields = Object.entries(flags).filter(([, changed]) => changed).map(([field]) => field);
17065
+ const changedFields = changedFieldsOf(flags);
16984
17066
  const kind = getPrimaryKind(before, after, changedFields.length > 0);
16985
17067
  scenarios.push({
16986
17068
  kind,
@@ -16995,12 +17077,59 @@ function diffRuns(baseline, current) {
16995
17077
  durationDeltaMs: after.durationMs - before.durationMs
16996
17078
  });
16997
17079
  }
17080
+ const { pairs, unmatchedRemoved, unmatchedAdded } = matchIdentities(removedCases, addedCases);
17081
+ for (const { before, after, confidence, matchedBy } of pairs) {
17082
+ const flags = buildFlags(before, after);
17083
+ scenarios.push({
17084
+ kind: identityKind(before, after),
17085
+ id: after.id,
17086
+ previousId: before.id,
17087
+ scenario: after.story.scenario,
17088
+ sourceFile: after.sourceFile,
17089
+ sourceLine: after.sourceLine,
17090
+ baseline: toScenarioSnapshot(before),
17091
+ current: toScenarioSnapshot(after),
17092
+ flags,
17093
+ changedFields: changedFieldsOf(flags),
17094
+ durationDeltaMs: after.durationMs - before.durationMs,
17095
+ matchConfidence: confidence,
17096
+ matchedBy
17097
+ });
17098
+ }
17099
+ for (const after of unmatchedAdded) {
17100
+ const flags = allChangedFlags(after.errorMessage);
17101
+ scenarios.push({
17102
+ kind: "added",
17103
+ id: after.id,
17104
+ scenario: after.story.scenario,
17105
+ sourceFile: after.sourceFile,
17106
+ sourceLine: after.sourceLine,
17107
+ current: toScenarioSnapshot(after),
17108
+ flags,
17109
+ changedFields: changedFieldsOf(flags)
17110
+ });
17111
+ }
17112
+ for (const before of unmatchedRemoved) {
17113
+ const flags = allChangedFlags(before.errorMessage);
17114
+ scenarios.push({
17115
+ kind: "removed",
17116
+ id: before.id,
17117
+ scenario: before.story.scenario,
17118
+ sourceFile: before.sourceFile,
17119
+ sourceLine: before.sourceLine,
17120
+ baseline: toScenarioSnapshot(before),
17121
+ flags,
17122
+ changedFields: changedFieldsOf(flags)
17123
+ });
17124
+ }
16998
17125
  const sorted = sortDiffs(scenarios);
16999
17126
  const summary = {
17000
17127
  totalBaseline: baseline.testCases.length,
17001
17128
  totalCurrent: current.testCases.length,
17002
17129
  added: sorted.filter((s) => s.kind === "added").length,
17003
17130
  removed: sorted.filter((s) => s.kind === "removed").length,
17131
+ renamed: sorted.filter((s) => s.kind === "renamed").length,
17132
+ moved: sorted.filter((s) => s.kind === "moved").length,
17004
17133
  changed: sorted.filter((s) => s.kind === "changed").length,
17005
17134
  regressed: sorted.filter((s) => s.kind === "regressed").length,
17006
17135
  fixed: sorted.filter((s) => s.kind === "fixed").length,
@@ -17028,6 +17157,10 @@ function statusLabel(kind) {
17028
17157
  return "Added";
17029
17158
  case "removed":
17030
17159
  return "Removed";
17160
+ case "renamed":
17161
+ return "Renamed";
17162
+ case "moved":
17163
+ return "Moved";
17031
17164
  case "changed":
17032
17165
  return "Changed";
17033
17166
  default:
@@ -17353,6 +17486,8 @@ window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', fun
17353
17486
  <div class="summary-card"><strong>${diff.summary.fixed}</strong><span>Fixed</span></div>
17354
17487
  <div class="summary-card"><strong>${diff.summary.added}</strong><span>Added</span></div>
17355
17488
  <div class="summary-card"><strong>${diff.summary.removed}</strong><span>Removed</span></div>
17489
+ <div class="summary-card"><strong>${diff.summary.renamed}</strong><span>Renamed</span></div>
17490
+ <div class="summary-card"><strong>${diff.summary.moved}</strong><span>Moved</span></div>
17356
17491
  <div class="summary-card"><strong>${diff.summary.changed}</strong><span>Changed</span></div>
17357
17492
  <div class="summary-card"><strong>${diff.summary.unchanged}</strong><span>Unchanged</span></div>
17358
17493
  </section>
@@ -17413,6 +17548,10 @@ function formatStatus(kind) {
17413
17548
  return "Added";
17414
17549
  case "removed":
17415
17550
  return "Removed";
17551
+ case "renamed":
17552
+ return "Renamed";
17553
+ case "moved":
17554
+ return "Moved";
17416
17555
  case "changed":
17417
17556
  return "Changed";
17418
17557
  default:
@@ -17565,13 +17704,13 @@ var RunDiffMarkdownFormatter = class {
17565
17704
  lines.push("No regressions or fixes detected. Remaining changes are neutral.");
17566
17705
  }
17567
17706
  lines.push("");
17568
- lines.push("| Added | Removed | Regressed | Fixed | Changed | Unchanged |");
17569
- lines.push("| ---: | ---: | ---: | ---: | ---: | ---: |");
17707
+ lines.push("| Added | Removed | Renamed | Moved | Regressed | Fixed | Changed | Unchanged |");
17708
+ lines.push("| ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: |");
17570
17709
  lines.push(
17571
- `| ${diff.summary.added} | ${diff.summary.removed} | ${diff.summary.regressed} | ${diff.summary.fixed} | ${diff.summary.changed} | ${diff.summary.unchanged} |`
17710
+ `| ${diff.summary.added} | ${diff.summary.removed} | ${diff.summary.renamed} | ${diff.summary.moved} | ${diff.summary.regressed} | ${diff.summary.fixed} | ${diff.summary.changed} | ${diff.summary.unchanged} |`
17572
17711
  );
17573
17712
  lines.push("");
17574
- for (const kind of ["regressed", "fixed", "added", "removed", "changed"]) {
17713
+ for (const kind of ["regressed", "fixed", "added", "removed", "renamed", "moved", "changed"]) {
17575
17714
  const scenarios = diff.scenarios.filter((scenario) => scenario.kind === kind);
17576
17715
  if (scenarios.length === 0) continue;
17577
17716
  lines.push(`## ${formatStatus(kind)} (${scenarios.length})`);
@@ -19003,7 +19142,10 @@ function diffStoryReports(baseline, current) {
19003
19142
  };
19004
19143
  });
19005
19144
  const summary = { added: 0, removed: 0, regressed: 0, fixed: 0, changed: 0, unchanged: 0 };
19006
- for (const s of scenarios) summary[s.kind] += 1;
19145
+ for (const s of scenarios) {
19146
+ if (s.kind === "renamed" || s.kind === "moved") continue;
19147
+ summary[s.kind] += 1;
19148
+ }
19007
19149
  return { schemaVersion: "1.0", summary, scenarios };
19008
19150
  }
19009
19151