executable-stories-formatters 0.15.0 → 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.
Files changed (42) hide show
  1. package/dist/cli.js +238 -64
  2. package/dist/cli.js.map +1 -1
  3. package/dist/index.cjs +197 -55
  4. package/dist/index.cjs.map +1 -1
  5. package/dist/index.d.cts +11 -1
  6. package/dist/index.d.ts +11 -1
  7. package/dist/index.js +197 -55
  8. package/dist/index.js.map +1 -1
  9. package/package.json +3 -2
  10. package/schemas/README.md +1 -1
  11. package/templates/astro-starlight/astro.config.mjs +57 -0
  12. package/templates/astro-starlight/gitignore +14 -0
  13. package/templates/astro-starlight/package.json +20 -0
  14. package/templates/astro-starlight/public/stories/assets/.gitkeep +0 -0
  15. package/templates/astro-starlight/public/stories/notes-index.json +4 -0
  16. package/templates/astro-starlight/public/stories/story-report.json +17 -0
  17. package/templates/astro-starlight/src/components/ApiOperations.astro +366 -0
  18. package/templates/astro-starlight/src/components/Checklist.astro +15 -0
  19. package/templates/astro-starlight/src/components/HealthDashboard.astro +171 -0
  20. package/templates/astro-starlight/src/components/PageTitle.astro +53 -0
  21. package/templates/astro-starlight/src/components/VerifiedBy.astro +281 -0
  22. package/templates/astro-starlight/src/components/VerifiedStep.astro +91 -0
  23. package/templates/astro-starlight/src/content/docs/examples/example-adr.mdx +45 -0
  24. package/templates/astro-starlight/src/content/docs/guides/behavior-portal.mdx +41 -0
  25. package/templates/astro-starlight/src/content/docs/guides/writing-docs.mdx +49 -0
  26. package/templates/astro-starlight/src/content/docs/index.mdx +49 -0
  27. package/templates/astro-starlight/src/content/docs/stories/.gitkeep +0 -0
  28. package/templates/astro-starlight/src/content.config.ts +18 -0
  29. package/templates/astro-starlight/src/lib/config.ts +50 -0
  30. package/templates/astro-starlight/src/lib/render-doc-entry.ts +154 -0
  31. package/templates/astro-starlight/src/lib/report-health.ts +61 -0
  32. package/templates/astro-starlight/src/lib/verification.ts +247 -0
  33. package/templates/astro-starlight/src/pages/explorer/explorer.css +729 -0
  34. package/templates/astro-starlight/src/pages/explorer/index.astro +404 -0
  35. package/templates/astro-starlight/src/styles/global.css +293 -0
  36. package/templates/astro-starlight/src/styles/themes/corporate.css +83 -0
  37. package/templates/astro-starlight/src/styles/themes/dashboard.css +76 -0
  38. package/templates/astro-starlight/src/styles/themes/default.css +86 -0
  39. package/templates/astro-starlight/src/styles/themes/minimal.css +87 -0
  40. package/templates/astro-starlight/src/styles/themes/playful.css +77 -0
  41. package/templates/astro-starlight/src/styles/themes/terminal.css +77 -0
  42. package/templates/astro-starlight/tsconfig.json +13 -0
package/dist/index.d.cts CHANGED
@@ -502,7 +502,7 @@ interface ResolvedFormatterOptions {
502
502
  allowMissingAssets: boolean;
503
503
  }
504
504
 
505
- type ScenarioChangeKind = "added" | "removed" | "regressed" | "fixed" | "changed" | "unchanged";
505
+ type ScenarioChangeKind = "added" | "removed" | "renamed" | "moved" | "regressed" | "fixed" | "changed" | "unchanged";
506
506
  interface ScenarioChangeFlags {
507
507
  status: boolean;
508
508
  steps: boolean;
@@ -541,12 +541,22 @@ interface ScenarioDiff {
541
541
  flags: ScenarioChangeFlags;
542
542
  changedFields: string[];
543
543
  durationDeltaMs?: number;
544
+ /** For `renamed`/`moved`: the baseline test-case id this behaviour was matched from. */
545
+ previousId?: string;
546
+ /** For `renamed`/`moved`: match confidence in 0..1 (1 = exact content fingerprint). */
547
+ matchConfidence?: number;
548
+ /** For `renamed`/`moved`: how the baseline/current pair was re-identified. */
549
+ matchedBy?: "fingerprint" | "similarity";
544
550
  }
545
551
  interface RunDiffSummary {
546
552
  totalBaseline: number;
547
553
  totalCurrent: number;
548
554
  added: number;
549
555
  removed: number;
556
+ /** Behaviours re-identified across a title change (content preserved). */
557
+ renamed: number;
558
+ /** Behaviours re-identified across a file move (content preserved). */
559
+ moved: number;
550
560
  changed: number;
551
561
  regressed: number;
552
562
  fixed: number;
package/dist/index.d.ts CHANGED
@@ -502,7 +502,7 @@ interface ResolvedFormatterOptions {
502
502
  allowMissingAssets: boolean;
503
503
  }
504
504
 
505
- type ScenarioChangeKind = "added" | "removed" | "regressed" | "fixed" | "changed" | "unchanged";
505
+ type ScenarioChangeKind = "added" | "removed" | "renamed" | "moved" | "regressed" | "fixed" | "changed" | "unchanged";
506
506
  interface ScenarioChangeFlags {
507
507
  status: boolean;
508
508
  steps: boolean;
@@ -541,12 +541,22 @@ interface ScenarioDiff {
541
541
  flags: ScenarioChangeFlags;
542
542
  changedFields: string[];
543
543
  durationDeltaMs?: number;
544
+ /** For `renamed`/`moved`: the baseline test-case id this behaviour was matched from. */
545
+ previousId?: string;
546
+ /** For `renamed`/`moved`: match confidence in 0..1 (1 = exact content fingerprint). */
547
+ matchConfidence?: number;
548
+ /** For `renamed`/`moved`: how the baseline/current pair was re-identified. */
549
+ matchedBy?: "fingerprint" | "similarity";
544
550
  }
545
551
  interface RunDiffSummary {
546
552
  totalBaseline: number;
547
553
  totalCurrent: number;
548
554
  added: number;
549
555
  removed: number;
556
+ /** Behaviours re-identified across a title change (content preserved). */
557
+ renamed: number;
558
+ /** Behaviours re-identified across a file move (content preserved). */
559
+ moved: number;
550
560
  changed: number;
551
561
  regressed: number;
552
562
  fixed: number;
package/dist/index.js CHANGED
@@ -42,6 +42,41 @@ function generateFeatureId(uri) {
42
42
  function generateScenarioId(featureId, scenarioName) {
43
43
  return `${featureId};${slugify(scenarioName)}`;
44
44
  }
45
+ function normalizeText(text2) {
46
+ return text2.toLowerCase().replace(/[^\p{L}\p{N}\s]/gu, " ").replace(/\s+/g, " ").trim();
47
+ }
48
+ function tokenize(text2) {
49
+ const normalized = normalizeText(text2);
50
+ return normalized.length === 0 ? [] : normalized.split(" ");
51
+ }
52
+ function behaviourFingerprint(input) {
53
+ const steps = input.steps.map((step) => `${step.keyword.toLowerCase()}:${normalizeText(step.text)}`).join("\n");
54
+ const covers = [...input.covers ?? []].map((path12) => path12.trim()).filter(Boolean).sort().join(",");
55
+ if (steps.length === 0 && covers.length === 0) return "";
56
+ return createHash("sha1").update(`${steps}\0${covers}`).digest("hex").slice(0, 16);
57
+ }
58
+ function jaccard(a, b) {
59
+ if (a.size === 0 && b.size === 0) return 1;
60
+ if (a.size === 0 || b.size === 0) return 0;
61
+ let intersection = 0;
62
+ for (const value of a) if (b.has(value)) intersection += 1;
63
+ return intersection / (a.size + b.size - intersection);
64
+ }
65
+ function stepTokens(steps) {
66
+ const tokens = /* @__PURE__ */ new Set();
67
+ for (const step of steps) {
68
+ for (const token of tokenize(step.text)) tokens.add(token);
69
+ }
70
+ return tokens;
71
+ }
72
+ function behaviourSimilarity(a, b) {
73
+ const aSteps = stepTokens(a.steps);
74
+ const bSteps = stepTokens(b.steps);
75
+ if (aSteps.size === 0 || bSteps.size === 0) return 0;
76
+ const stepScore = jaccard(aSteps, bSteps);
77
+ const titleScore = jaccard(new Set(tokenize(a.scenario)), new Set(tokenize(b.scenario)));
78
+ return stepScore * 0.7 + titleScore * 0.3;
79
+ }
45
80
 
46
81
  // src/converters/acl/steps.ts
47
82
  function deriveStepResults(steps, scenarioStatus, error) {
@@ -16663,6 +16698,10 @@ function titleFor(kind) {
16663
16698
  return "Added";
16664
16699
  case "removed":
16665
16700
  return "Removed";
16701
+ case "renamed":
16702
+ return "Renamed";
16703
+ case "moved":
16704
+ return "Moved";
16666
16705
  case "changed":
16667
16706
  return "Changed";
16668
16707
  default:
@@ -16711,6 +16750,8 @@ function createPrCommentSummary(diff, maxScenarios = 10) {
16711
16750
  addSection(lines, diff, "fixed", maxScenarios);
16712
16751
  addSection(lines, diff, "added", maxScenarios);
16713
16752
  addSection(lines, diff, "removed", maxScenarios);
16753
+ addSection(lines, diff, "renamed", maxScenarios);
16754
+ addSection(lines, diff, "moved", maxScenarios);
16714
16755
  addSection(lines, diff, "changed", maxScenarios);
16715
16756
  return lines.join("\n").trimEnd();
16716
16757
  }
@@ -16757,8 +16798,10 @@ function sortDiffs(scenarios) {
16757
16798
  fixed: 1,
16758
16799
  added: 2,
16759
16800
  removed: 3,
16760
- changed: 4,
16761
- unchanged: 5
16801
+ renamed: 4,
16802
+ moved: 5,
16803
+ changed: 6,
16804
+ unchanged: 7
16762
16805
  };
16763
16806
  return [...scenarios].sort((a, b) => {
16764
16807
  if (rank[a.kind] !== rank[b.kind]) {
@@ -16773,69 +16816,108 @@ function sortDiffs(scenarios) {
16773
16816
  return a.scenario.localeCompare(b.scenario);
16774
16817
  });
16775
16818
  }
16819
+ var SIMILARITY_THRESHOLD = 0.75;
16820
+ function identityInput(tc) {
16821
+ return {
16822
+ scenario: tc.story.scenario,
16823
+ sourceFile: tc.sourceFile,
16824
+ steps: tc.story.steps,
16825
+ covers: tc.story.covers
16826
+ };
16827
+ }
16828
+ function changedFieldsOf(flags) {
16829
+ return Object.entries(flags).filter(([, changed]) => changed).map(([field]) => field);
16830
+ }
16831
+ function allChangedFlags(errorMessage) {
16832
+ return {
16833
+ status: true,
16834
+ steps: true,
16835
+ docs: true,
16836
+ tags: true,
16837
+ tickets: true,
16838
+ source: true,
16839
+ duration: true,
16840
+ attachments: true,
16841
+ error: Boolean(errorMessage),
16842
+ titlePath: true
16843
+ };
16844
+ }
16845
+ function matchIdentities(removed, added) {
16846
+ const pairs = [];
16847
+ const remainingRemoved = new Set(removed);
16848
+ const remainingAdded = new Set(added);
16849
+ const removedByFp = /* @__PURE__ */ new Map();
16850
+ const addedByFp = /* @__PURE__ */ new Map();
16851
+ for (const tc of remainingRemoved) {
16852
+ const fp = behaviourFingerprint(identityInput(tc));
16853
+ if (fp === "") continue;
16854
+ (removedByFp.get(fp) ?? removedByFp.set(fp, []).get(fp)).push(tc);
16855
+ }
16856
+ for (const tc of remainingAdded) {
16857
+ const fp = behaviourFingerprint(identityInput(tc));
16858
+ if (fp === "") continue;
16859
+ (addedByFp.get(fp) ?? addedByFp.set(fp, []).get(fp)).push(tc);
16860
+ }
16861
+ for (const [fp, removedGroup] of removedByFp) {
16862
+ const addedGroup = addedByFp.get(fp);
16863
+ if (removedGroup.length === 1 && addedGroup && addedGroup.length === 1) {
16864
+ pairs.push({ before: removedGroup[0], after: addedGroup[0], confidence: 1, matchedBy: "fingerprint" });
16865
+ remainingRemoved.delete(removedGroup[0]);
16866
+ remainingAdded.delete(addedGroup[0]);
16867
+ }
16868
+ }
16869
+ for (const before of [...remainingRemoved]) {
16870
+ let best;
16871
+ let bestScore = 0;
16872
+ let tied = false;
16873
+ for (const after of remainingAdded) {
16874
+ const score = behaviourSimilarity(identityInput(before), identityInput(after));
16875
+ if (score > bestScore) {
16876
+ bestScore = score;
16877
+ best = after;
16878
+ tied = false;
16879
+ } else if (score === bestScore) {
16880
+ tied = true;
16881
+ }
16882
+ }
16883
+ if (best && !tied && bestScore >= SIMILARITY_THRESHOLD) {
16884
+ pairs.push({ before, after: best, confidence: Math.round(bestScore * 100) / 100, matchedBy: "similarity" });
16885
+ remainingRemoved.delete(before);
16886
+ remainingAdded.delete(best);
16887
+ }
16888
+ }
16889
+ return {
16890
+ pairs,
16891
+ unmatchedRemoved: [...remainingRemoved],
16892
+ unmatchedAdded: [...remainingAdded]
16893
+ };
16894
+ }
16895
+ function identityKind(before, after) {
16896
+ const titleChanged = before.story.scenario !== after.story.scenario;
16897
+ const fileChanged = before.sourceFile !== after.sourceFile;
16898
+ return fileChanged && !titleChanged ? "moved" : "renamed";
16899
+ }
16776
16900
  function diffRuns(baseline, current) {
16777
16901
  const baselineById = new Map(baseline.testCases.map((tc) => [tc.id, tc]));
16778
16902
  const currentById = new Map(current.testCases.map((tc) => [tc.id, tc]));
16779
16903
  const ids = /* @__PURE__ */ new Set([...baselineById.keys(), ...currentById.keys()]);
16780
16904
  const scenarios = [];
16905
+ const removedCases = [];
16906
+ const addedCases = [];
16781
16907
  for (const id of ids) {
16782
16908
  const before = baselineById.get(id);
16783
16909
  const after = currentById.get(id);
16784
16910
  if (!before && after) {
16785
- const flags2 = {
16786
- status: true,
16787
- steps: true,
16788
- docs: true,
16789
- tags: true,
16790
- tickets: true,
16791
- source: true,
16792
- duration: true,
16793
- attachments: true,
16794
- error: Boolean(after.errorMessage),
16795
- titlePath: true
16796
- };
16797
- scenarios.push({
16798
- kind: "added",
16799
- id,
16800
- scenario: after.story.scenario,
16801
- sourceFile: after.sourceFile,
16802
- sourceLine: after.sourceLine,
16803
- current: toScenarioSnapshot(after),
16804
- flags: flags2,
16805
- changedFields: Object.entries(flags2).filter(([, changed]) => changed).map(([field]) => field)
16806
- });
16911
+ addedCases.push(after);
16807
16912
  continue;
16808
16913
  }
16809
16914
  if (before && !after) {
16810
- const flags2 = {
16811
- status: true,
16812
- steps: true,
16813
- docs: true,
16814
- tags: true,
16815
- tickets: true,
16816
- source: true,
16817
- duration: true,
16818
- attachments: true,
16819
- error: Boolean(before.errorMessage),
16820
- titlePath: true
16821
- };
16822
- scenarios.push({
16823
- kind: "removed",
16824
- id,
16825
- scenario: before.story.scenario,
16826
- sourceFile: before.sourceFile,
16827
- sourceLine: before.sourceLine,
16828
- baseline: toScenarioSnapshot(before),
16829
- flags: flags2,
16830
- changedFields: Object.entries(flags2).filter(([, changed]) => changed).map(([field]) => field)
16831
- });
16832
- continue;
16833
- }
16834
- if (!before || !after) {
16915
+ removedCases.push(before);
16835
16916
  continue;
16836
16917
  }
16918
+ if (!before || !after) continue;
16837
16919
  const flags = buildFlags(before, after);
16838
- const changedFields = Object.entries(flags).filter(([, changed]) => changed).map(([field]) => field);
16920
+ const changedFields = changedFieldsOf(flags);
16839
16921
  const kind = getPrimaryKind(before, after, changedFields.length > 0);
16840
16922
  scenarios.push({
16841
16923
  kind,
@@ -16850,12 +16932,59 @@ function diffRuns(baseline, current) {
16850
16932
  durationDeltaMs: after.durationMs - before.durationMs
16851
16933
  });
16852
16934
  }
16935
+ const { pairs, unmatchedRemoved, unmatchedAdded } = matchIdentities(removedCases, addedCases);
16936
+ for (const { before, after, confidence, matchedBy } of pairs) {
16937
+ const flags = buildFlags(before, after);
16938
+ scenarios.push({
16939
+ kind: identityKind(before, after),
16940
+ id: after.id,
16941
+ previousId: before.id,
16942
+ scenario: after.story.scenario,
16943
+ sourceFile: after.sourceFile,
16944
+ sourceLine: after.sourceLine,
16945
+ baseline: toScenarioSnapshot(before),
16946
+ current: toScenarioSnapshot(after),
16947
+ flags,
16948
+ changedFields: changedFieldsOf(flags),
16949
+ durationDeltaMs: after.durationMs - before.durationMs,
16950
+ matchConfidence: confidence,
16951
+ matchedBy
16952
+ });
16953
+ }
16954
+ for (const after of unmatchedAdded) {
16955
+ const flags = allChangedFlags(after.errorMessage);
16956
+ scenarios.push({
16957
+ kind: "added",
16958
+ id: after.id,
16959
+ scenario: after.story.scenario,
16960
+ sourceFile: after.sourceFile,
16961
+ sourceLine: after.sourceLine,
16962
+ current: toScenarioSnapshot(after),
16963
+ flags,
16964
+ changedFields: changedFieldsOf(flags)
16965
+ });
16966
+ }
16967
+ for (const before of unmatchedRemoved) {
16968
+ const flags = allChangedFlags(before.errorMessage);
16969
+ scenarios.push({
16970
+ kind: "removed",
16971
+ id: before.id,
16972
+ scenario: before.story.scenario,
16973
+ sourceFile: before.sourceFile,
16974
+ sourceLine: before.sourceLine,
16975
+ baseline: toScenarioSnapshot(before),
16976
+ flags,
16977
+ changedFields: changedFieldsOf(flags)
16978
+ });
16979
+ }
16853
16980
  const sorted = sortDiffs(scenarios);
16854
16981
  const summary = {
16855
16982
  totalBaseline: baseline.testCases.length,
16856
16983
  totalCurrent: current.testCases.length,
16857
16984
  added: sorted.filter((s) => s.kind === "added").length,
16858
16985
  removed: sorted.filter((s) => s.kind === "removed").length,
16986
+ renamed: sorted.filter((s) => s.kind === "renamed").length,
16987
+ moved: sorted.filter((s) => s.kind === "moved").length,
16859
16988
  changed: sorted.filter((s) => s.kind === "changed").length,
16860
16989
  regressed: sorted.filter((s) => s.kind === "regressed").length,
16861
16990
  fixed: sorted.filter((s) => s.kind === "fixed").length,
@@ -16883,6 +17012,10 @@ function statusLabel(kind) {
16883
17012
  return "Added";
16884
17013
  case "removed":
16885
17014
  return "Removed";
17015
+ case "renamed":
17016
+ return "Renamed";
17017
+ case "moved":
17018
+ return "Moved";
16886
17019
  case "changed":
16887
17020
  return "Changed";
16888
17021
  default:
@@ -17208,6 +17341,8 @@ window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', fun
17208
17341
  <div class="summary-card"><strong>${diff.summary.fixed}</strong><span>Fixed</span></div>
17209
17342
  <div class="summary-card"><strong>${diff.summary.added}</strong><span>Added</span></div>
17210
17343
  <div class="summary-card"><strong>${diff.summary.removed}</strong><span>Removed</span></div>
17344
+ <div class="summary-card"><strong>${diff.summary.renamed}</strong><span>Renamed</span></div>
17345
+ <div class="summary-card"><strong>${diff.summary.moved}</strong><span>Moved</span></div>
17211
17346
  <div class="summary-card"><strong>${diff.summary.changed}</strong><span>Changed</span></div>
17212
17347
  <div class="summary-card"><strong>${diff.summary.unchanged}</strong><span>Unchanged</span></div>
17213
17348
  </section>
@@ -17268,6 +17403,10 @@ function formatStatus(kind) {
17268
17403
  return "Added";
17269
17404
  case "removed":
17270
17405
  return "Removed";
17406
+ case "renamed":
17407
+ return "Renamed";
17408
+ case "moved":
17409
+ return "Moved";
17271
17410
  case "changed":
17272
17411
  return "Changed";
17273
17412
  default:
@@ -17420,13 +17559,13 @@ var RunDiffMarkdownFormatter = class {
17420
17559
  lines.push("No regressions or fixes detected. Remaining changes are neutral.");
17421
17560
  }
17422
17561
  lines.push("");
17423
- lines.push("| Added | Removed | Regressed | Fixed | Changed | Unchanged |");
17424
- lines.push("| ---: | ---: | ---: | ---: | ---: | ---: |");
17562
+ lines.push("| Added | Removed | Renamed | Moved | Regressed | Fixed | Changed | Unchanged |");
17563
+ lines.push("| ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: |");
17425
17564
  lines.push(
17426
- `| ${diff.summary.added} | ${diff.summary.removed} | ${diff.summary.regressed} | ${diff.summary.fixed} | ${diff.summary.changed} | ${diff.summary.unchanged} |`
17565
+ `| ${diff.summary.added} | ${diff.summary.removed} | ${diff.summary.renamed} | ${diff.summary.moved} | ${diff.summary.regressed} | ${diff.summary.fixed} | ${diff.summary.changed} | ${diff.summary.unchanged} |`
17427
17566
  );
17428
17567
  lines.push("");
17429
- for (const kind of ["regressed", "fixed", "added", "removed", "changed"]) {
17568
+ for (const kind of ["regressed", "fixed", "added", "removed", "renamed", "moved", "changed"]) {
17430
17569
  const scenarios = diff.scenarios.filter((scenario) => scenario.kind === kind);
17431
17570
  if (scenarios.length === 0) continue;
17432
17571
  lines.push(`## ${formatStatus(kind)} (${scenarios.length})`);
@@ -18858,7 +18997,10 @@ function diffStoryReports(baseline, current) {
18858
18997
  };
18859
18998
  });
18860
18999
  const summary = { added: 0, removed: 0, regressed: 0, fixed: 0, changed: 0, unchanged: 0 };
18861
- for (const s of scenarios) summary[s.kind] += 1;
19000
+ for (const s of scenarios) {
19001
+ if (s.kind === "renamed" || s.kind === "moved") continue;
19002
+ summary[s.kind] += 1;
19003
+ }
18862
19004
  return { schemaVersion: "1.0", summary, scenarios };
18863
19005
  }
18864
19006