@swedevtools/livedoc-vitest 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/LICENSE +20 -20
  3. package/README.md +160 -95
  4. package/dist/{RuleContext-BZhuy-zS.d.cts → RuleContext-DQ8o_n1D.d.ts} +62 -62
  5. package/dist/globals.d.ts +99 -99
  6. package/dist/{index-Blmp569T.d.cts → index-sbV15ohX.d.ts} +21 -2
  7. package/dist/index.d.ts +7 -5
  8. package/dist/index.js +1062 -159
  9. package/dist/reporter/index.d.ts +2 -2
  10. package/dist/reporter/index.js +629 -84
  11. package/package.json +14 -12
  12. package/tools/livedoc-setup.mjs +172 -164
  13. package/tools/skills/SKILL.md +339 -244
  14. package/tools/skills/VALIDATION.md +37 -29
  15. package/tools/skills/examples/routing.md +75 -60
  16. package/tools/skills/resources/anti-patterns.md +19 -0
  17. package/tools/skills/resources/bdd-features.md +231 -231
  18. package/tools/skills/resources/partial-testing.md +77 -0
  19. package/tools/skills/resources/playwright.md +148 -148
  20. package/tools/skills/resources/reporter-config.md +213 -163
  21. package/tools/skills/resources/specifications.md +159 -159
  22. package/tools/skills/resources/test-strategy.md +103 -0
  23. package/tools/skills/resources/web-testing.md +62 -0
  24. package/dist/RuleContext-BZhuy-zS.d.ts +0 -206
  25. package/dist/globals.cjs +0 -2
  26. package/dist/globals.d.cts +0 -104
  27. package/dist/index-CysiWbtk.d.ts +0 -687
  28. package/dist/index.cjs +0 -10024
  29. package/dist/index.d.cts +0 -291
  30. package/dist/playwright/index.cjs +0 -103
  31. package/dist/playwright/index.d.cts +0 -129
  32. package/dist/reporter/index.cjs +0 -8676
  33. package/dist/reporter/index.d.cts +0 -7
  34. package/dist/setup.cjs +0 -14
  35. package/dist/setup.d.cts +0 -2
package/dist/index.js CHANGED
@@ -1,9 +1,10 @@
1
1
  import chalk3, { Chalk } from 'chalk';
2
2
  import { afterAll, it, describe, beforeAll, TestRunner } from 'vitest';
3
+ import { AsyncLocalStorage } from 'async_hooks';
3
4
  import { fileURLToPath } from 'url';
4
- import * as path from 'path';
5
- import { dirname, resolve } from 'path';
6
- import { existsSync, unlinkSync, appendFileSync, mkdirSync, writeFileSync, promises } from 'fs';
5
+ import * as path2 from 'path';
6
+ import path2__default, { dirname, resolve } from 'path';
7
+ import { existsSync, unlinkSync, appendFileSync, mkdirSync, writeFileSync, promises, statSync, readFileSync } from 'fs';
7
8
  import CliTable3 from 'cli-table3';
8
9
  import * as diff from 'diff';
9
10
  import wordwrap from 'wordwrap';
@@ -1565,10 +1566,12 @@ var FilterOptions = class {
1565
1566
  var PublishOptions = class {
1566
1567
  /** Server URL, e.g., 'http://localhost:3100' */
1567
1568
  server = "http://localhost:3100";
1568
- /** Project name (defaults to 'default') */
1569
- project = "default";
1569
+ /** Project name (defaults to 'livedoc') */
1570
+ project = "livedoc";
1570
1571
  /** Environment name (defaults to 'local') */
1571
1572
  environment = "local";
1573
+ /** Whether the invocation reports the complete test inventory or a focused subset. */
1574
+ runType = "full";
1572
1575
  /** Whether publishing is enabled */
1573
1576
  enabled = false;
1574
1577
  };
@@ -1682,6 +1685,7 @@ var currentStep = null;
1682
1685
  var scenarioCount = 0;
1683
1686
  var scenarioId = 0;
1684
1687
  var currentSpecification = null;
1688
+ var declarationContextStorage = new AsyncLocalStorage();
1685
1689
  var specificationRegistry = [];
1686
1690
  var afterBackgroundFnMap = /* @__PURE__ */ new Map();
1687
1691
  var backgroundStepsMap = /* @__PURE__ */ new Map();
@@ -1690,10 +1694,76 @@ var backgroundStepsComplete = false;
1690
1694
  var scenarioStartHooks = [];
1691
1695
  var scenarioEndHooks = [];
1692
1696
  var isPendingContext = false;
1697
+ var currentFilterOmissionReason;
1698
+ var isIncludedContext = false;
1693
1699
  var isFilteredContext = false;
1694
1700
  var isDynamicExecution = false;
1695
1701
  var capturedThrownException = null;
1696
1702
  var resultsFileWritten = false;
1703
+ function createDefaultOptions() {
1704
+ const options = new LiveDocOptions();
1705
+ options.rules.singleGivenWhenThen = "enabled" /* enabled */;
1706
+ options.rules.backgroundMustOnlyIncludeGiven = "enabled" /* enabled */;
1707
+ options.rules.enforceTitle = "enabled" /* enabled */;
1708
+ options.rules.enforceUsingGivenOverBefore = "warning" /* warning */;
1709
+ options.rules.mustIncludeGiven = "warning" /* warning */;
1710
+ options.rules.mustIncludeWhen = "warning" /* warning */;
1711
+ options.rules.mustIncludeThen = "warning" /* warning */;
1712
+ return options;
1713
+ }
1714
+ function assertDeclarationParent(type, title, expectedParent, missingParentMessage, filename) {
1715
+ const declarationTitle = title.trim().split(/\r?\n/, 1)[0].trim();
1716
+ const currentDeclarationContext = declarationContextStorage.getStore();
1717
+ if (currentDeclarationContext?.type === expectedParent) {
1718
+ return;
1719
+ }
1720
+ if (currentDeclarationContext) {
1721
+ throw new ParserException(
1722
+ `Invalid nesting: ${type} "${declarationTitle}" cannot be declared within ${currentDeclarationContext.type} "${currentDeclarationContext.title}".`,
1723
+ declarationTitle,
1724
+ filename
1725
+ );
1726
+ }
1727
+ if (expectedParent !== null) {
1728
+ throw new ParserException(missingParentMessage, title, filename);
1729
+ }
1730
+ }
1731
+ function captureParserException(error) {
1732
+ if (!isDynamicExecution || !(error instanceof ParserException)) {
1733
+ return;
1734
+ }
1735
+ capturedThrownException = {
1736
+ type: "ParserException",
1737
+ message: error.description,
1738
+ data: {
1739
+ description: error.description,
1740
+ title: error.title,
1741
+ filename: error.filename
1742
+ }
1743
+ };
1744
+ }
1745
+ function withDeclarationContext(context, fn) {
1746
+ return declarationContextStorage.run(context, fn);
1747
+ }
1748
+ async function withDeclarationContextAsync(context, fn) {
1749
+ return await declarationContextStorage.run(context, fn);
1750
+ }
1751
+ function getStepDeclarationType(stepType) {
1752
+ switch (stepType) {
1753
+ case "given":
1754
+ return "Given";
1755
+ case "when":
1756
+ return "When";
1757
+ case "then":
1758
+ return "Then";
1759
+ case "and":
1760
+ return "And";
1761
+ case "but":
1762
+ return "But";
1763
+ default:
1764
+ throw new Error(`Unsupported LiveDoc step type: ${stepType}`);
1765
+ }
1766
+ }
1697
1767
  var __filename_esm = fileURLToPath(import.meta.url);
1698
1768
  dirname(__filename_esm);
1699
1769
  var dynamicResultsFile = process.env.LIVEDOC_DYNAMIC_RESULTS_FILE;
@@ -1704,11 +1774,13 @@ if (dynamicResultsFile) {
1704
1774
  return;
1705
1775
  }
1706
1776
  try {
1777
+ const outputFeatures = pruneFilteredFeatures(featureRegistry);
1778
+ const outputSpecifications = pruneFilteredSpecifications(specificationRegistry);
1707
1779
  const fs2 = __require("fs");
1708
1780
  const results = {
1709
- features: featureRegistry.map((f) => f.toJSON()),
1781
+ features: outputFeatures.map((f) => f.toJSON()),
1710
1782
  suites: suiteRegistry.map((s) => s.toJSON()),
1711
- specifications: specificationRegistry.map((s) => s.toJSON())
1783
+ specifications: outputSpecifications.map((s) => s.toJSON())
1712
1784
  };
1713
1785
  if (capturedThrownException) {
1714
1786
  results.thrownException = capturedThrownException;
@@ -1725,15 +1797,8 @@ if (dynamicResultsFile) {
1725
1797
  });
1726
1798
  }
1727
1799
  var livedoc = {
1728
- options: new LiveDocOptions()
1729
- };
1730
- livedoc.options.rules.singleGivenWhenThen = "enabled" /* enabled */;
1731
- livedoc.options.rules.backgroundMustOnlyIncludeGiven = "enabled" /* enabled */;
1732
- livedoc.options.rules.enforceTitle = "enabled" /* enabled */;
1733
- livedoc.options.rules.enforceUsingGivenOverBefore = "warning" /* warning */;
1734
- livedoc.options.rules.mustIncludeGiven = "warning" /* warning */;
1735
- livedoc.options.rules.mustIncludeWhen = "warning" /* warning */;
1736
- livedoc.options.rules.mustIncludeThen = "warning" /* warning */;
1800
+ options: createDefaultOptions()
1801
+ };
1737
1802
  var displayedViolations = {};
1738
1803
  function markedAsExcluded(tags) {
1739
1804
  if (tags.length === 0 || !livedoc.options.filters.exclude) {
@@ -1757,14 +1822,189 @@ function markedAsIncluded(tags) {
1757
1822
  }
1758
1823
  return false;
1759
1824
  }
1760
- function shouldMarkAsPending(tags) {
1761
- return markedAsExcluded(tags) && (!markedAsIncluded(tags) || !!livedoc.options.filters.showFilterConflicts);
1825
+ function hasIncludeFilter() {
1826
+ return (livedoc.options.filters.include?.length ?? 0) > 0;
1762
1827
  }
1763
- function shouldInclude(tags) {
1764
- if (tags.length === 0) {
1765
- return false;
1828
+ function decideFilter(tags, inheritedIncluded, inheritedFilteredOut, allowIncludeMiss) {
1829
+ const directlyIncluded = markedAsIncluded(tags);
1830
+ const directlyExcluded = markedAsExcluded(tags);
1831
+ const included = inheritedIncluded || directlyIncluded;
1832
+ const isConflict = directlyIncluded && directlyExcluded;
1833
+ if (inheritedFilteredOut) {
1834
+ return {
1835
+ filteredOut: true,
1836
+ included,
1837
+ pendingConflict: false,
1838
+ reason: "parent-filtered"
1839
+ };
1840
+ }
1841
+ if (isConflict && !!livedoc.options.filters.showFilterConflicts) {
1842
+ return {
1843
+ filteredOut: false,
1844
+ included: true,
1845
+ pendingConflict: true
1846
+ };
1847
+ }
1848
+ if (directlyExcluded) {
1849
+ return {
1850
+ filteredOut: true,
1851
+ included,
1852
+ pendingConflict: false,
1853
+ reason: "exclude"
1854
+ };
1855
+ }
1856
+ if (allowIncludeMiss && hasIncludeFilter() && !included) {
1857
+ return {
1858
+ filteredOut: true,
1859
+ included: false,
1860
+ pendingConflict: false,
1861
+ reason: "include-miss"
1862
+ };
1863
+ }
1864
+ return {
1865
+ filteredOut: false,
1866
+ included,
1867
+ pendingConflict: false
1868
+ };
1869
+ }
1870
+ function markFilteredOut(target, decision) {
1871
+ if (!decision.filteredOut) {
1872
+ return;
1873
+ }
1874
+ const mutable = target;
1875
+ mutable.filteredOut = true;
1876
+ mutable.filterOmissionReason = decision.reason ?? "parent-filtered";
1877
+ }
1878
+ function shouldSkipForFilterDecision(decision, explicitPending, inheritedPending = false) {
1879
+ return !!explicitPending || inheritedPending || decision.filteredOut || decision.pendingConflict;
1880
+ }
1881
+ function filterMeta(reason) {
1882
+ if (!reason) {
1883
+ return void 0;
1884
+ }
1885
+ return {
1886
+ filteredOut: true,
1887
+ reason
1888
+ };
1889
+ }
1890
+ function isFilteredOutModel(value) {
1891
+ return !!value?.filteredOut;
1892
+ }
1893
+ function pruneFilteredFeatures(features) {
1894
+ return features.map((feature3) => pruneFeatureForOutput(feature3)).filter((feature3) => feature3 !== null);
1895
+ }
1896
+ function pruneFeatureForOutput(feature3) {
1897
+ if (isFilteredOutModel(feature3)) {
1898
+ return null;
1899
+ }
1900
+ feature3.scenarios = feature3.scenarios.map((scenario3) => pruneScenarioForOutput(scenario3)).filter((scenario3) => scenario3 !== null);
1901
+ if (feature3.scenarios.length === 0) {
1902
+ return null;
1903
+ }
1904
+ recalculateFeatureStatistics(feature3);
1905
+ return feature3;
1906
+ }
1907
+ function pruneScenarioForOutput(scenario3) {
1908
+ if (isFilteredOutModel(scenario3)) {
1909
+ return null;
1910
+ }
1911
+ if (scenario3 instanceof ScenarioOutline) {
1912
+ scenario3.examples = scenario3.examples.filter((example) => !isFilteredOutModel(example));
1913
+ if (scenario3.examples.length === 0) {
1914
+ return null;
1915
+ }
1916
+ }
1917
+ return scenario3;
1918
+ }
1919
+ function pruneFilteredSpecifications(specifications) {
1920
+ return specifications.map((specification3) => pruneSpecificationForOutput(specification3)).filter((specification3) => specification3 !== null);
1921
+ }
1922
+ function pruneSpecificationForOutput(specification3) {
1923
+ if (isFilteredOutModel(specification3)) {
1924
+ return null;
1925
+ }
1926
+ specification3.rules = specification3.rules.map((rule3) => pruneRuleForOutput(rule3)).filter((rule3) => rule3 !== null);
1927
+ if (specification3.rules.length === 0) {
1928
+ return null;
1929
+ }
1930
+ recalculateSpecificationStatistics(specification3);
1931
+ return specification3;
1932
+ }
1933
+ function pruneRuleForOutput(rule3) {
1934
+ if (isFilteredOutModel(rule3)) {
1935
+ return null;
1936
+ }
1937
+ if (rule3 instanceof RuleOutline) {
1938
+ rule3.examples = rule3.examples.filter((example) => !isFilteredOutModel(example));
1939
+ if (rule3.examples.length === 0) {
1940
+ return null;
1941
+ }
1942
+ }
1943
+ return rule3;
1944
+ }
1945
+ function resetStatistics(suite) {
1946
+ suite.statistics = new Statistics(suite);
1947
+ }
1948
+ function recalculateFeatureStatistics(feature3) {
1949
+ resetStatistics(feature3);
1950
+ if (feature3.background && !isFilteredOutModel(feature3.background)) {
1951
+ resetStatistics(feature3.background);
1952
+ for (const step of feature3.background.steps) {
1953
+ if (!isFilteredOutModel(step)) {
1954
+ feature3.background.statistics.updateStats(step.status, step.duration);
1955
+ }
1956
+ }
1957
+ }
1958
+ for (const scenario3 of feature3.scenarios) {
1959
+ recalculateScenarioStatistics(scenario3);
1960
+ }
1961
+ }
1962
+ function recalculateScenarioStatistics(scenario3) {
1963
+ resetStatistics(scenario3);
1964
+ if (scenario3 instanceof ScenarioOutline) {
1965
+ for (const example of scenario3.examples) {
1966
+ resetStatistics(example);
1967
+ for (const step of example.steps) {
1968
+ if (!isFilteredOutModel(step)) {
1969
+ example.statistics.updateStats(step.status, step.duration);
1970
+ }
1971
+ }
1972
+ }
1973
+ return;
1974
+ }
1975
+ for (const step of scenario3.steps) {
1976
+ if (!isFilteredOutModel(step)) {
1977
+ scenario3.statistics.updateStats(step.status, step.duration);
1978
+ }
1979
+ }
1980
+ }
1981
+ function recalculateSpecificationStatistics(specification3) {
1982
+ resetStatistics(specification3);
1983
+ for (const rule3 of specification3.rules) {
1984
+ if (rule3 instanceof RuleOutline) {
1985
+ recalculateRuleOutlineStatus(rule3);
1986
+ for (const example of rule3.examples) {
1987
+ specification3.statistics.updateStats(example.status, example.executionTime);
1988
+ }
1989
+ } else {
1990
+ specification3.statistics.updateStats(rule3.status, rule3.executionTime);
1991
+ }
1992
+ }
1993
+ }
1994
+ function recalculateRuleOutlineStatus(ruleOutline3) {
1995
+ if (ruleOutline3.examples.length === 0) {
1996
+ ruleOutline3.status = "unknown" /* unknown */;
1997
+ return;
1998
+ }
1999
+ if (ruleOutline3.examples.some((example) => example.status === "fail" /* fail */)) {
2000
+ ruleOutline3.status = "fail" /* fail */;
2001
+ } else if (ruleOutline3.examples.every((example) => example.status === "pass" /* pass */)) {
2002
+ ruleOutline3.status = "pass" /* pass */;
2003
+ } else if (ruleOutline3.examples.every((example) => example.status === "pending" /* pending */)) {
2004
+ ruleOutline3.status = "pending" /* pending */;
2005
+ } else {
2006
+ ruleOutline3.status = "pass" /* pass */;
1766
2007
  }
1767
- return markedAsIncluded(tags) && (!markedAsExcluded(tags) || !!livedoc.options.filters.showFilterConflicts);
1768
2008
  }
1769
2009
  function displayRuleViolation(violation, filename) {
1770
2010
  if (displayedViolations[violation.errorId]) {
@@ -1800,6 +2040,7 @@ function displayWarnings(filename) {
1800
2040
  }
1801
2041
  function featureImpl(title, fn, opts = {}) {
1802
2042
  const filename = getFilenameFromStack(3);
2043
+ assertDeclarationParent("Feature", title, null, "", filename);
1803
2044
  if (fn.constructor.name === "AsyncFunction") {
1804
2045
  throw new ParserException(`The async keyword is not supported for Feature`, title, filename);
1805
2046
  }
@@ -1811,23 +2052,39 @@ function featureImpl(title, fn, opts = {}) {
1811
2052
  thisFeature.generateId(thisFeature);
1812
2053
  thisFeature.validateIdUniqueness(thisFeature.id, featureRegistry);
1813
2054
  featureRegistry.push(thisFeature);
1814
- const shouldSkip = opts.pending || shouldMarkAsPending(thisFeature.tags);
1815
- const shouldOnlyRun = opts.isOnly || shouldInclude(thisFeature.tags);
2055
+ const filterDecision = decideFilter(thisFeature.tags, false, false, false);
2056
+ markFilteredOut(thisFeature, filterDecision);
2057
+ const shouldSkip = shouldSkipForFilterDecision(filterDecision, opts.pending);
2058
+ const shouldOnlyRun = opts.isOnly;
1816
2059
  const describeFunc = shouldSkip ? describe.skip : shouldOnlyRun ? describe.only : describe;
1817
2060
  describeFunc(thisFeature.displayTitle, () => {
1818
2061
  currentFeature = thisFeature;
1819
2062
  const previousPendingContext = isPendingContext;
1820
2063
  const previousFilteredContext = isFilteredContext;
2064
+ const previousFilterOmissionReason = currentFilterOmissionReason;
2065
+ const previousIncludedContext = isIncludedContext;
2066
+ if (filterDecision.included) {
2067
+ isIncludedContext = true;
2068
+ }
1821
2069
  if (shouldSkip) {
1822
2070
  isPendingContext = true;
2071
+ if (filterDecision.filteredOut) {
2072
+ isFilteredContext = true;
2073
+ currentFilterOmissionReason = filterDecision.reason ?? "parent-filtered";
2074
+ }
1823
2075
  }
1824
2076
  const ctx = {
1825
2077
  get feature() {
1826
2078
  return thisFeature?.getFeatureContext();
1827
2079
  }
1828
2080
  };
1829
- fn(ctx);
2081
+ withDeclarationContext(
2082
+ { type: "Feature", title: thisFeature.title },
2083
+ () => fn(ctx)
2084
+ );
1830
2085
  isPendingContext = previousPendingContext;
2086
+ currentFilterOmissionReason = previousFilterOmissionReason;
2087
+ isIncludedContext = previousIncludedContext;
1831
2088
  isFilteredContext = previousFilteredContext;
1832
2089
  });
1833
2090
  }
@@ -1845,29 +2102,38 @@ var feature = Object.assign(
1845
2102
  }
1846
2103
  );
1847
2104
  function scenarioImpl(title, fn, opts = {}) {
2105
+ const filename = getFilenameFromStack(3);
2106
+ assertDeclarationParent("Scenario", title, "Feature", "Scenario must be within a feature.", filename);
1848
2107
  if (!currentFeature) {
1849
- throw new ParserException("Scenario must be within a feature.", title, "");
2108
+ throw new ParserException("Scenario must be within a feature.", title, filename);
1850
2109
  }
1851
- const filename = getFilenameFromStack(3);
1852
2110
  if (fn.constructor.name === "AsyncFunction") {
1853
2111
  throw new ParserException(`The async keyword is not supported for Scenario`, title, filename);
1854
2112
  }
1855
2113
  const scenarioModel = parser.addScenario(currentFeature, title);
1856
2114
  scenarioCount++;
1857
2115
  const thisScenarioId = scenarioCount;
1858
- const shouldSkip = opts.pending || isPendingContext || shouldMarkAsPending(scenarioModel.tags);
1859
- const shouldOnlyRun = opts.isOnly || shouldInclude(scenarioModel.tags);
1860
- const isInheritedFilter = isFilteredContext;
2116
+ const filterDecision = decideFilter(scenarioModel.tags, isIncludedContext, isFilteredContext, true);
2117
+ markFilteredOut(scenarioModel, filterDecision);
2118
+ const shouldSkip = shouldSkipForFilterDecision(filterDecision, opts.pending, isPendingContext);
2119
+ const shouldOnlyRun = opts.isOnly;
2120
+ const inheritedFilterOmissionReason = currentFilterOmissionReason;
1861
2121
  const describeFunc = shouldSkip ? describe.skip : shouldOnlyRun ? describe.only : describe;
1862
2122
  describeFunc(scenarioModel.displayTitle, () => {
1863
2123
  const previousScenario = currentScenario;
1864
2124
  currentScenario = scenarioModel;
1865
2125
  const previousPendingContext = isPendingContext;
2126
+ const previousFilterOmissionReason = currentFilterOmissionReason;
2127
+ const previousIncludedContext = isIncludedContext;
2128
+ if (filterDecision.included) {
2129
+ isIncludedContext = true;
2130
+ }
1866
2131
  const previousFilteredContext = isFilteredContext;
1867
2132
  if (shouldSkip) {
1868
2133
  isPendingContext = true;
1869
- if (isInheritedFilter) {
2134
+ if (filterDecision.filteredOut) {
1870
2135
  isFilteredContext = true;
2136
+ currentFilterOmissionReason = filterDecision.reason ?? inheritedFilterOmissionReason ?? "parent-filtered";
1871
2137
  }
1872
2138
  }
1873
2139
  beforeAll(async () => {
@@ -1913,8 +2179,13 @@ function scenarioImpl(title, fn, opts = {}) {
1913
2179
  return scenarioModel.getScenarioContext();
1914
2180
  }
1915
2181
  };
1916
- fn(ctx);
2182
+ withDeclarationContext(
2183
+ { type: "Scenario", title: scenarioModel.title },
2184
+ () => fn(ctx)
2185
+ );
1917
2186
  isPendingContext = previousPendingContext;
2187
+ currentFilterOmissionReason = previousFilterOmissionReason;
2188
+ isIncludedContext = previousIncludedContext;
1918
2189
  isFilteredContext = previousFilteredContext;
1919
2190
  currentScenario = previousScenario;
1920
2191
  });
@@ -1933,10 +2204,11 @@ var scenario = Object.assign(
1933
2204
  }
1934
2205
  );
1935
2206
  function backgroundImpl(title, fn, opts = {}) {
2207
+ const filename = getFilenameFromStack(3);
2208
+ assertDeclarationParent("Background", title, "Feature", "Background must be within a feature.", filename);
1936
2209
  if (!currentFeature) {
1937
- throw new ParserException("Background must be within a feature.", title, "");
2210
+ throw new ParserException("Background must be within a feature.", title, filename);
1938
2211
  }
1939
- const filename = getFilenameFromStack(3);
1940
2212
  if (fn.constructor.name === "AsyncFunction") {
1941
2213
  throw new ParserException(`The async keyword is not supported for Background`, title, filename);
1942
2214
  }
@@ -1963,7 +2235,10 @@ function backgroundImpl(title, fn, opts = {}) {
1963
2235
  }
1964
2236
  }
1965
2237
  };
1966
- fn(ctx);
2238
+ withDeclarationContext(
2239
+ { type: "Background", title: backgroundModel.title },
2240
+ () => fn(ctx)
2241
+ );
1967
2242
  currentBackground = previousBackground;
1968
2243
  });
1969
2244
  }
@@ -1987,33 +2262,49 @@ function onScenarioEnd(fn) {
1987
2262
  scenarioEndHooks.push(fn);
1988
2263
  }
1989
2264
  function scenarioOutlineImpl(title, fn, opts = {}) {
2265
+ const filename = getFilenameFromStack(3);
2266
+ assertDeclarationParent(
2267
+ "Scenario Outline",
2268
+ title,
2269
+ "Feature",
2270
+ "Scenario Outline must be within a feature.",
2271
+ filename
2272
+ );
1990
2273
  if (!currentFeature) {
1991
- throw new ParserException("Scenario Outline must be within a feature.", title, "");
2274
+ throw new ParserException("Scenario Outline must be within a feature.", title, filename);
1992
2275
  }
1993
- const filename = getFilenameFromStack(3);
1994
2276
  if (fn.constructor.name === "AsyncFunction") {
1995
2277
  throw new ParserException(`The async keyword is not supported for Scenario Outline`, title, filename);
1996
2278
  }
1997
2279
  const scenarioOutlineModel = parser.addScenarioOutline(currentFeature, title);
1998
- const shouldSkip = opts.pending || isPendingContext || shouldMarkAsPending(scenarioOutlineModel.tags);
1999
- const shouldOnlyRun = opts.isOnly || shouldInclude(scenarioOutlineModel.tags);
2000
- const isInheritedFilter = isFilteredContext;
2280
+ const filterDecision = decideFilter(scenarioOutlineModel.tags, isIncludedContext, isFilteredContext, true);
2281
+ markFilteredOut(scenarioOutlineModel, filterDecision);
2282
+ const shouldSkip = shouldSkipForFilterDecision(filterDecision, opts.pending, isPendingContext);
2283
+ const shouldOnlyRun = opts.isOnly;
2284
+ const inheritedFilterOmissionReason = currentFilterOmissionReason;
2001
2285
  const outlineDescribeFunc = shouldSkip ? describe.skip : shouldOnlyRun ? describe.only : describe;
2002
2286
  outlineDescribeFunc(`Scenario: ${scenarioOutlineModel.title}`, () => {
2003
2287
  for (const example of scenarioOutlineModel.examples) {
2004
2288
  scenarioCount++;
2005
2289
  const thisScenarioId = scenarioCount;
2290
+ markFilteredOut(example, filterDecision);
2006
2291
  const materializedScenarioTitle = materializePlaceholders(scenarioOutlineModel.title, example.exampleRaw ?? example.example ?? {});
2007
2292
  const exampleName = `Example ${example.sequence}: ${materializedScenarioTitle}`;
2008
2293
  describe(exampleName, () => {
2009
2294
  const previousScenario = currentScenario;
2010
2295
  currentScenario = example;
2011
2296
  const previousPendingContext = isPendingContext;
2297
+ const previousFilterOmissionReason = currentFilterOmissionReason;
2298
+ const previousIncludedContext = isIncludedContext;
2299
+ if (filterDecision.included) {
2300
+ isIncludedContext = true;
2301
+ }
2012
2302
  const previousFilteredContext = isFilteredContext;
2013
2303
  if (shouldSkip) {
2014
2304
  isPendingContext = true;
2015
- if (isInheritedFilter) {
2305
+ if (filterDecision.filteredOut) {
2016
2306
  isFilteredContext = true;
2307
+ currentFilterOmissionReason = filterDecision.reason ?? inheritedFilterOmissionReason ?? "parent-filtered";
2017
2308
  }
2018
2309
  }
2019
2310
  beforeAll(async () => {
@@ -2062,8 +2353,13 @@ function scenarioOutlineImpl(title, fn, opts = {}) {
2062
2353
  return example.getScenarioContext();
2063
2354
  }
2064
2355
  };
2065
- fn(ctx);
2356
+ withDeclarationContext(
2357
+ { type: "Scenario Outline", title: scenarioOutlineModel.title },
2358
+ () => fn(ctx)
2359
+ );
2066
2360
  isPendingContext = previousPendingContext;
2361
+ currentFilterOmissionReason = previousFilterOmissionReason;
2362
+ isIncludedContext = previousIncludedContext;
2067
2363
  isFilteredContext = previousFilteredContext;
2068
2364
  currentScenario = previousScenario;
2069
2365
  });
@@ -2085,19 +2381,31 @@ var scenarioOutline = Object.assign(
2085
2381
  );
2086
2382
  function specificationImpl(title, fn, opts = {}) {
2087
2383
  const filename = getFilenameFromStack(3);
2384
+ assertDeclarationParent("Specification", title, null, "", filename);
2088
2385
  const thisSpecification = parser.createSpecification(title, filename);
2089
2386
  currentSpecification = thisSpecification;
2090
2387
  thisSpecification.generateId(thisSpecification);
2091
2388
  thisSpecification.validateIdUniqueness(thisSpecification.id, specificationRegistry);
2092
2389
  specificationRegistry.push(thisSpecification);
2093
- const shouldSkip = opts.pending || shouldMarkAsPending(thisSpecification.tags);
2094
- const shouldOnlyRun = opts.isOnly || shouldInclude(thisSpecification.tags);
2390
+ const filterDecision = decideFilter(thisSpecification.tags, false, false, false);
2391
+ markFilteredOut(thisSpecification, filterDecision);
2392
+ const shouldSkip = shouldSkipForFilterDecision(filterDecision, opts.pending);
2393
+ const shouldOnlyRun = opts.isOnly;
2095
2394
  const describeFunc = shouldSkip ? describe.skip : shouldOnlyRun ? describe.only : describe;
2096
2395
  describeFunc(thisSpecification.displayTitle, () => {
2097
2396
  currentSpecification = thisSpecification;
2098
2397
  const previousPendingContext = isPendingContext;
2099
2398
  const previousFilteredContext = isFilteredContext;
2399
+ const previousFilterOmissionReason = currentFilterOmissionReason;
2400
+ const previousIncludedContext = isIncludedContext;
2401
+ if (filterDecision.included) {
2402
+ isIncludedContext = true;
2403
+ }
2100
2404
  if (shouldSkip) {
2405
+ if (filterDecision.filteredOut) {
2406
+ isFilteredContext = true;
2407
+ currentFilterOmissionReason = filterDecision.reason ?? "parent-filtered";
2408
+ }
2101
2409
  isPendingContext = true;
2102
2410
  }
2103
2411
  const ctx = {
@@ -2105,8 +2413,13 @@ function specificationImpl(title, fn, opts = {}) {
2105
2413
  return thisSpecification?.getSpecificationContext();
2106
2414
  }
2107
2415
  };
2108
- fn(ctx);
2416
+ withDeclarationContext(
2417
+ { type: "Specification", title: thisSpecification.title },
2418
+ () => fn(ctx)
2419
+ );
2109
2420
  isPendingContext = previousPendingContext;
2421
+ currentFilterOmissionReason = previousFilterOmissionReason;
2422
+ isIncludedContext = previousIncludedContext;
2110
2423
  isFilteredContext = previousFilteredContext;
2111
2424
  });
2112
2425
  }
@@ -2124,13 +2437,20 @@ var specification = Object.assign(
2124
2437
  }
2125
2438
  );
2126
2439
  function ruleImpl(title, fn, opts = {}) {
2440
+ const filename = getFilenameFromStack(3);
2441
+ assertDeclarationParent("Rule", title, "Specification", "Rule must be within a specification.", filename);
2127
2442
  if (!currentSpecification) {
2128
- throw new ParserException("Rule must be within a specification.", title, "");
2443
+ throw new ParserException("Rule must be within a specification.", title, filename);
2129
2444
  }
2130
2445
  const ruleModel = parser.addRule(currentSpecification, title);
2131
2446
  const specificationModel = currentSpecification;
2132
- const shouldSkip = opts.pending || isPendingContext || shouldMarkAsPending(ruleModel.tags);
2133
- const shouldOnlyRun = opts.isOnly || shouldInclude(ruleModel.tags);
2447
+ const filterDecision = decideFilter(ruleModel.tags, isIncludedContext, isFilteredContext, true);
2448
+ markFilteredOut(ruleModel, filterDecision);
2449
+ const shouldSkip = shouldSkipForFilterDecision(filterDecision, opts.pending, isPendingContext);
2450
+ const shouldOnlyRun = opts.isOnly;
2451
+ if (shouldSkip && !filterDecision.filteredOut) {
2452
+ ruleModel.status = "pending" /* pending */;
2453
+ }
2134
2454
  const ruleMeta = {
2135
2455
  livedoc: {
2136
2456
  kind: "rule",
@@ -2138,7 +2458,8 @@ function ruleImpl(title, fn, opts = {}) {
2138
2458
  title: ruleModel.title,
2139
2459
  description: ruleModel.description ?? "",
2140
2460
  tags: ruleModel.tags ?? []
2141
- }
2461
+ },
2462
+ ...filterDecision.filteredOut ? { filter: filterMeta(filterDecision.reason ?? currentFilterOmissionReason ?? "parent-filtered") } : {}
2142
2463
  }
2143
2464
  };
2144
2465
  const ruleHandler = async () => {
@@ -2152,10 +2473,14 @@ function ruleImpl(title, fn, opts = {}) {
2152
2473
  };
2153
2474
  const startTime = Date.now();
2154
2475
  try {
2155
- await fn(ctx);
2476
+ await withDeclarationContextAsync(
2477
+ { type: "Rule", title: ruleModel.title },
2478
+ () => fn(ctx)
2479
+ );
2156
2480
  ruleModel.status = "pass" /* pass */;
2157
2481
  ruleModel.executionTime = Date.now() - startTime;
2158
2482
  } catch (error) {
2483
+ captureParserException(error);
2159
2484
  ruleModel.status = "fail" /* fail */;
2160
2485
  ruleModel.executionTime = Date.now() - startTime;
2161
2486
  ruleModel.error = error;
@@ -2190,12 +2515,25 @@ var rule = Object.assign(
2190
2515
  }
2191
2516
  );
2192
2517
  function ruleOutlineImpl(title, fn, opts = {}) {
2518
+ const filename = getFilenameFromStack(3);
2519
+ assertDeclarationParent(
2520
+ "Rule Outline",
2521
+ title,
2522
+ "Specification",
2523
+ "Rule Outline must be within a specification.",
2524
+ filename
2525
+ );
2193
2526
  if (!currentSpecification) {
2194
- throw new ParserException("Rule Outline must be within a specification.", title, "");
2527
+ throw new ParserException("Rule Outline must be within a specification.", title, filename);
2195
2528
  }
2196
2529
  const ruleOutlineModel = parser.addRuleOutline(currentSpecification, title);
2197
- const shouldSkip = opts.pending || isPendingContext || shouldMarkAsPending(ruleOutlineModel.tags);
2198
- const shouldOnlyRun = opts.isOnly || shouldInclude(ruleOutlineModel.tags);
2530
+ const filterDecision = decideFilter(ruleOutlineModel.tags, isIncludedContext, isFilteredContext, true);
2531
+ markFilteredOut(ruleOutlineModel, filterDecision);
2532
+ const shouldSkip = shouldSkipForFilterDecision(filterDecision, opts.pending, isPendingContext);
2533
+ const shouldOnlyRun = opts.isOnly;
2534
+ if (shouldSkip && !filterDecision.filteredOut) {
2535
+ ruleOutlineModel.status = "pending" /* pending */;
2536
+ }
2199
2537
  const outlineDescribeFunc = shouldSkip ? describe.skip : shouldOnlyRun ? describe.only : describe;
2200
2538
  const specificationModel = currentSpecification;
2201
2539
  outlineDescribeFunc(`Rule Outline: ${ruleOutlineModel.title}`, () => {
@@ -2205,6 +2543,10 @@ function ruleOutlineImpl(title, fn, opts = {}) {
2205
2543
  example.title = materializedRuleTitle;
2206
2544
  example.displayTitle = materializedRuleTitle;
2207
2545
  const exampleName = `Example ${example.sequence}: ${materializedRuleTitle}`;
2546
+ markFilteredOut(example, filterDecision);
2547
+ if (shouldSkip && !filterDecision.filteredOut) {
2548
+ example.status = "pending" /* pending */;
2549
+ }
2208
2550
  const exampleMeta = {
2209
2551
  livedoc: {
2210
2552
  kind: "ruleExample",
@@ -2218,7 +2560,8 @@ function ruleOutlineImpl(title, fn, opts = {}) {
2218
2560
  values: example.example ?? {},
2219
2561
  valuesRaw: example.exampleRaw ?? {}
2220
2562
  }
2221
- }
2563
+ },
2564
+ ...filterDecision.filteredOut ? { filter: filterMeta(filterDecision.reason ?? currentFilterOmissionReason ?? "parent-filtered") } : {}
2222
2565
  }
2223
2566
  };
2224
2567
  const exampleHandler = async () => {
@@ -2235,10 +2578,14 @@ function ruleOutlineImpl(title, fn, opts = {}) {
2235
2578
  };
2236
2579
  const startTime = Date.now();
2237
2580
  try {
2238
- await fn(ctx);
2581
+ await withDeclarationContextAsync(
2582
+ { type: "Rule Outline", title: ruleOutlineModel.title },
2583
+ () => fn(ctx)
2584
+ );
2239
2585
  example.status = "pass" /* pass */;
2240
2586
  example.executionTime = Date.now() - startTime;
2241
2587
  } catch (error) {
2588
+ captureParserException(error);
2242
2589
  example.status = "fail" /* fail */;
2243
2590
  example.executionTime = Date.now() - startTime;
2244
2591
  example.error = error;
@@ -2254,6 +2601,7 @@ function ruleOutlineImpl(title, fn, opts = {}) {
2254
2601
  }
2255
2602
  currentSuite.task(exampleName, {
2256
2603
  meta: exampleMeta,
2604
+ skip: shouldSkip,
2257
2605
  handler: exampleHandler
2258
2606
  });
2259
2607
  }
@@ -2283,6 +2631,10 @@ function createStepFunction(stepType) {
2283
2631
  );
2284
2632
  }
2285
2633
  const stepDefinition = parser.createStep(stepType, title, passedParam);
2634
+ if (isFilteredContext) {
2635
+ stepDefinition.filteredOut = true;
2636
+ stepDefinition.filterOmissionReason = currentFilterOmissionReason ?? "parent-filtered";
2637
+ }
2286
2638
  if (currentScenario instanceof ScenarioExample || currentScenario instanceof ScenarioOutline) {
2287
2639
  parser.applyPassedParams(stepDefinition);
2288
2640
  }
@@ -2370,7 +2722,8 @@ function createStepFunction(stepType) {
2370
2722
  }
2371
2723
  } : {}
2372
2724
  }
2373
- } : {}
2725
+ } : {},
2726
+ ...isFilteredContext ? { filter: filterMeta(currentFilterOmissionReason ?? "parent-filtered") } : {}
2374
2727
  }
2375
2728
  };
2376
2729
  const stepHandler = async () => {
@@ -2396,10 +2749,10 @@ function createStepFunction(stepType) {
2396
2749
  return stepDefinition.getStepContext();
2397
2750
  }
2398
2751
  };
2399
- const result = fn(ctx);
2400
- if (result && typeof result.then === "function") {
2401
- await result;
2402
- }
2752
+ await withDeclarationContextAsync(
2753
+ { type: getStepDeclarationType(stepDefinition.type), title: stepDefinition.title },
2754
+ () => fn(ctx)
2755
+ );
2403
2756
  }
2404
2757
  } else if (capturedScenario) {
2405
2758
  const backgroundItExecuted = capturedFeature ? backgroundItExecutedMap.get(capturedFeature) ?? false : false;
@@ -2412,20 +2765,23 @@ function createStepFunction(stepType) {
2412
2765
  parser.applyPassedParams(stepDetail.stepDefinition);
2413
2766
  const bgStartTime = Date.now();
2414
2767
  try {
2415
- const result = stepDetail.func({
2416
- get feature() {
2417
- return capturedFeature?.getFeatureContext();
2768
+ await withDeclarationContextAsync(
2769
+ {
2770
+ type: getStepDeclarationType(stepDetail.stepDefinition.type),
2771
+ title: stepDetail.stepDefinition.title
2418
2772
  },
2419
- get background() {
2420
- return capturedFeature?.getBackgroundContext();
2421
- },
2422
- get step() {
2423
- return stepDetail.stepDefinition.getStepContext();
2424
- }
2425
- });
2426
- if (result && typeof result.then === "function") {
2427
- await result;
2428
- }
2773
+ () => stepDetail.func({
2774
+ get feature() {
2775
+ return capturedFeature?.getFeatureContext();
2776
+ },
2777
+ get background() {
2778
+ return capturedFeature?.getBackgroundContext();
2779
+ },
2780
+ get step() {
2781
+ return stepDetail.stepDefinition.getStepContext();
2782
+ }
2783
+ })
2784
+ );
2429
2785
  stepDetail.stepDefinition.setStatus("pass" /* pass */, Date.now() - bgStartTime);
2430
2786
  } catch (error) {
2431
2787
  stepDetail.stepDefinition.setStatus("fail" /* fail */, Date.now() - bgStartTime);
@@ -2459,10 +2815,10 @@ function createStepFunction(stepType) {
2459
2815
  return capturedFeature?.getBackgroundContext();
2460
2816
  }
2461
2817
  };
2462
- const result = fn(ctx);
2463
- if (result && typeof result.then === "function") {
2464
- await result;
2465
- }
2818
+ await withDeclarationContextAsync(
2819
+ { type: getStepDeclarationType(stepDefinition.type), title: stepDefinition.title },
2820
+ () => fn(ctx)
2821
+ );
2466
2822
  }
2467
2823
  }
2468
2824
  const duration = Date.now() - startTime;
@@ -2471,6 +2827,7 @@ function createStepFunction(stepType) {
2471
2827
  taskMeta.livedoc.step.attachments = stepDefinition.attachments;
2472
2828
  }
2473
2829
  } catch (error) {
2830
+ captureParserException(error);
2474
2831
  const duration = Date.now() - startTime;
2475
2832
  stepDefinition.setStatus("fail" /* fail */, duration);
2476
2833
  const exception = new Exception();
@@ -2499,7 +2856,8 @@ function createStepFunction(stepType) {
2499
2856
  }
2500
2857
  currentSuite.task(testName, {
2501
2858
  meta: taskMeta,
2502
- handler: stepHandler
2859
+ handler: stepHandler,
2860
+ skip: isFilteredContext || void 0
2503
2861
  });
2504
2862
  };
2505
2863
  }
@@ -2850,9 +3208,9 @@ var LiveDocReporter = class {
2850
3208
  if (!strs || strs.length === 0)
2851
3209
  return "";
2852
3210
  if (strs.length === 1)
2853
- return path.dirname(strs[0]);
3211
+ return path2.dirname(strs[0]);
2854
3212
  for (let i = 0; i < strs.length; i++) {
2855
- strs[i] = path.dirname(strs[i]);
3213
+ strs[i] = path2.dirname(strs[i]);
2856
3214
  }
2857
3215
  let shortestString = "";
2858
3216
  let shortestLength = Number.MAX_SAFE_INTEGER;
@@ -2890,8 +3248,8 @@ var LiveDocReporter = class {
2890
3248
  if (rootPath.length === 0 || stripPath === "") {
2891
3249
  return "";
2892
3250
  } else {
2893
- let dirPath = path.parse(stripPath).dir;
2894
- if (dirPath.startsWith(path.sep)) {
3251
+ let dirPath = path2.parse(stripPath).dir;
3252
+ if (dirPath.startsWith(path2.sep)) {
2895
3253
  return dirPath.substr(1);
2896
3254
  } else {
2897
3255
  return dirPath;
@@ -3111,6 +3469,9 @@ var LiveDocSpec = class _LiveDocSpec extends LiveDocReporter {
3111
3469
  }
3112
3470
  outputSpecificationDetails(spec) {
3113
3471
  this.writeLine(this.formatKeywordTitle("Specification", spec.title, this.colorTheme.keyword, this.colorTheme.featureTitle, 2));
3472
+ if (spec.tags.length > 0) {
3473
+ this.writeLine(this.applyBlockIndent(this.formatTags(spec.tags), 4));
3474
+ }
3114
3475
  if (spec.description) {
3115
3476
  this.writeLine(this.formatDescription(spec.description, 4, this.colorTheme.featureDescription));
3116
3477
  }
@@ -3120,6 +3481,9 @@ var LiveDocSpec = class _LiveDocSpec extends LiveDocReporter {
3120
3481
  const status = this.getStatusIndicator(rule3.status);
3121
3482
  const title = this.highlight(rule3.title, /<[^>]+>/g, this.colorTheme.valuePlaceholders);
3122
3483
  this.writeLine(` ${status} ${this.colorTheme.keyword("Rule Outline:")} ${this.colorTheme.scenarioTitle(title)}`);
3484
+ if (rule3.tags.length > 0) {
3485
+ this.writeLine(this.applyBlockIndent(this.formatTags(rule3.tags), 6));
3486
+ }
3123
3487
  if (rule3.description) {
3124
3488
  this.writeLine(this.formatDescription(rule3.description, 6, this.colorTheme.scenarioDescription));
3125
3489
  }
@@ -3148,6 +3512,9 @@ var LiveDocSpec = class _LiveDocSpec extends LiveDocReporter {
3148
3512
  } else {
3149
3513
  const status = this.getStatusIndicator(rule3.status);
3150
3514
  this.writeLine(` ${status} ${this.colorTheme.keyword("Rule:")} ${this.colorTheme.stepTitle(rule3.title)}`);
3515
+ if (rule3.tags.length > 0) {
3516
+ this.writeLine(this.applyBlockIndent(this.formatTags(rule3.tags), 6));
3517
+ }
3151
3518
  }
3152
3519
  }
3153
3520
  this.writeLine(" ");
@@ -4460,8 +4827,8 @@ function getErrorMap() {
4460
4827
 
4461
4828
  // ../../node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/helpers/parseUtil.js
4462
4829
  var makeIssue = (params) => {
4463
- const { data, path: path3, errorMaps, issueData } = params;
4464
- const fullPath = [...path3, ...issueData.path || []];
4830
+ const { data, path: path4, errorMaps, issueData } = params;
4831
+ const fullPath = [...path4, ...issueData.path || []];
4465
4832
  const fullIssue = {
4466
4833
  ...issueData,
4467
4834
  path: fullPath
@@ -4577,11 +4944,11 @@ var errorUtil;
4577
4944
 
4578
4945
  // ../../node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/types.js
4579
4946
  var ParseInputLazyPath = class {
4580
- constructor(parent, value, path3, key) {
4947
+ constructor(parent, value, path4, key) {
4581
4948
  this._cachedPath = [];
4582
4949
  this.parent = parent;
4583
4950
  this.data = value;
4584
- this._path = path3;
4951
+ this._path = path4;
4585
4952
  this._key = key;
4586
4953
  }
4587
4954
  get path() {
@@ -8033,9 +8400,9 @@ function simpleHash(str) {
8033
8400
  return Math.abs(hash).toString(36);
8034
8401
  }
8035
8402
  function generateStabilityId(params) {
8036
- const { project, path: path3, title, kind, parentId, keyword, index } = params;
8403
+ const { project, path: path4, title, kind, parentId, keyword, index } = params;
8037
8404
  if (!parentId) {
8038
- return simpleHash(`${project}:${path3 || ""}:${title}`);
8405
+ return simpleHash(`${project}:${path4 || ""}:${title}`);
8039
8406
  }
8040
8407
  if (kind === "Step") {
8041
8408
  return `${parentId}:${simpleHash(`${keyword || ""}:${title}:${index ?? 0}`)}`;
@@ -8101,6 +8468,53 @@ var V1StatisticsSchema = external_exports.object({
8101
8468
  pending: external_exports.number().int().nonnegative(),
8102
8469
  skipped: external_exports.number().int().nonnegative()
8103
8470
  });
8471
+ var V1CoverageMetricNameSchema = external_exports.enum(["lines", "branches", "functions", "statements"]);
8472
+ var V1CoverageMetricSchema = external_exports.object({
8473
+ covered: external_exports.number().nonnegative(),
8474
+ total: external_exports.number().nonnegative(),
8475
+ skipped: external_exports.number().nonnegative().optional(),
8476
+ pct: external_exports.number().min(0).max(100).nullable()
8477
+ });
8478
+ var V1CoverageSummarySchema = external_exports.object({
8479
+ lines: V1CoverageMetricSchema.optional(),
8480
+ branches: V1CoverageMetricSchema.optional(),
8481
+ functions: V1CoverageMetricSchema.optional(),
8482
+ statements: V1CoverageMetricSchema.optional()
8483
+ });
8484
+ var V1CoverageProvenanceSchema = external_exports.object({
8485
+ tool: external_exports.string().optional(),
8486
+ format: external_exports.string(),
8487
+ path: external_exports.string().optional(),
8488
+ detected: external_exports.enum(["auto", "configured"]),
8489
+ generatedAt: external_exports.string().optional()
8490
+ });
8491
+ var V1CoverageFileSchema = external_exports.object({
8492
+ path: external_exports.string(),
8493
+ module: external_exports.string().optional(),
8494
+ summary: V1CoverageSummarySchema,
8495
+ detailRef: external_exports.string().optional()
8496
+ });
8497
+ var V1CoverageDiagnosticSchema = external_exports.object({
8498
+ severity: external_exports.enum(["info", "warning", "error"]),
8499
+ code: external_exports.string(),
8500
+ message: external_exports.string(),
8501
+ path: external_exports.string().optional(),
8502
+ details: external_exports.array(external_exports.string()).optional()
8503
+ });
8504
+ var V1CoverageThresholdSchema = external_exports.object({
8505
+ metric: V1CoverageMetricNameSchema,
8506
+ minimum: external_exports.number().min(0).max(100),
8507
+ actual: external_exports.number().min(0).max(100).nullable(),
8508
+ status: external_exports.enum(["passed", "warning"])
8509
+ });
8510
+ var V1CoverageReportSchema = external_exports.object({
8511
+ status: external_exports.enum(["available", "not-collected", "partial", "invalid"]),
8512
+ summary: V1CoverageSummarySchema.optional(),
8513
+ files: external_exports.array(V1CoverageFileSchema).optional(),
8514
+ diagnostics: external_exports.array(V1CoverageDiagnosticSchema).optional(),
8515
+ provenance: V1CoverageProvenanceSchema.optional(),
8516
+ thresholds: external_exports.array(V1CoverageThresholdSchema).optional()
8517
+ });
8104
8518
  var V1BaseTestSchema = external_exports.object({
8105
8519
  id: external_exports.string(),
8106
8520
  kind: external_exports.string(),
@@ -8164,10 +8578,12 @@ var V1TestCaseSchema = external_exports.object({
8164
8578
  ruleViolations: external_exports.array(V1RuleViolationSchema).optional()
8165
8579
  });
8166
8580
  var V1FrameworkSchema = external_exports.string();
8581
+ var V1RunTypeSchema = external_exports.enum(["full", "partial"]);
8167
8582
  external_exports.object({
8168
8583
  protocolVersion: external_exports.literal("1.0"),
8169
8584
  runId: external_exports.string(),
8170
- sessionId: external_exports.string().optional(),
8585
+ runType: V1RunTypeSchema.optional(),
8586
+ baselineRunId: external_exports.string().optional(),
8171
8587
  project: external_exports.string(),
8172
8588
  environment: external_exports.string(),
8173
8589
  framework: V1FrameworkSchema,
@@ -8175,18 +8591,19 @@ external_exports.object({
8175
8591
  duration: external_exports.number().nonnegative(),
8176
8592
  status: V1StatusSchema,
8177
8593
  summary: V1StatisticsSchema,
8178
- documents: external_exports.array(V1TestCaseSchema)
8594
+ documents: external_exports.array(V1TestCaseSchema),
8595
+ coverage: V1CoverageReportSchema.optional()
8179
8596
  });
8180
8597
  external_exports.object({
8181
8598
  project: external_exports.string(),
8182
8599
  environment: external_exports.string(),
8183
8600
  framework: external_exports.string(),
8184
- timestamp: external_exports.string().optional()
8601
+ timestamp: external_exports.string().optional(),
8602
+ runType: V1RunTypeSchema.optional()
8185
8603
  });
8186
8604
  external_exports.object({
8187
8605
  protocolVersion: external_exports.literal("1.0"),
8188
8606
  runId: external_exports.string(),
8189
- sessionId: external_exports.string().optional(),
8190
8607
  websocketUrl: external_exports.string()
8191
8608
  });
8192
8609
  external_exports.object({
@@ -8197,7 +8614,8 @@ external_exports.object({
8197
8614
  complete: external_exports.object({
8198
8615
  status: V1StatusSchema,
8199
8616
  duration: external_exports.number().nonnegative(),
8200
- summary: V1StatisticsSchema.optional()
8617
+ summary: V1StatisticsSchema.optional(),
8618
+ coverage: V1CoverageReportSchema.optional()
8201
8619
  }).optional()
8202
8620
  });
8203
8621
  external_exports.object({
@@ -8226,31 +8644,17 @@ var V1UpsertOutlineExampleResultsRequestSchema = external_exports.object({
8226
8644
  external_exports.object({
8227
8645
  status: V1StatusSchema,
8228
8646
  duration: external_exports.number().nonnegative(),
8229
- summary: V1StatisticsSchema.optional()
8230
- });
8231
- var V1SessionRunInfoSchema = external_exports.object({
8232
- runId: external_exports.string(),
8233
- framework: external_exports.string(),
8234
- status: V1StatusSchema,
8235
- timestamp: external_exports.string(),
8236
- duration: external_exports.number().nonnegative(),
8237
- summary: V1StatisticsSchema,
8238
- documentCount: external_exports.number().int().nonnegative()
8647
+ summary: V1StatisticsSchema.optional(),
8648
+ coverage: V1CoverageReportSchema.optional()
8239
8649
  });
8240
8650
  external_exports.object({
8241
- sessionId: external_exports.string(),
8242
- project: external_exports.string(),
8243
- environment: external_exports.string(),
8244
- status: V1StatusSchema,
8245
- timestamp: external_exports.string(),
8246
- duration: external_exports.number().nonnegative(),
8247
- summary: V1StatisticsSchema,
8248
- runs: external_exports.array(V1SessionRunInfoSchema),
8249
- documents: external_exports.array(V1TestCaseSchema)
8651
+ coverage: V1CoverageReportSchema
8250
8652
  });
8251
8653
  var V1WsRunStartedSchema = external_exports.object({
8252
8654
  type: external_exports.literal("run:v1:started"),
8253
8655
  runId: external_exports.string(),
8656
+ runType: V1RunTypeSchema.optional(),
8657
+ baselineRunId: external_exports.string().optional(),
8254
8658
  project: external_exports.string(),
8255
8659
  environment: external_exports.string(),
8256
8660
  framework: external_exports.string(),
@@ -8284,15 +8688,13 @@ var V1WsRunCompletedSchema = external_exports.object({
8284
8688
  runId: external_exports.string(),
8285
8689
  status: V1StatusSchema,
8286
8690
  duration: external_exports.number().nonnegative(),
8287
- summary: V1StatisticsSchema
8691
+ summary: V1StatisticsSchema,
8692
+ coverage: V1CoverageReportSchema.optional()
8288
8693
  });
8289
- var V1WsSessionUpdatedSchema = external_exports.object({
8290
- type: external_exports.literal("session:v1:updated"),
8291
- sessionId: external_exports.string(),
8292
- project: external_exports.string(),
8293
- environment: external_exports.string(),
8294
- status: V1StatusSchema,
8295
- summary: V1StatisticsSchema
8694
+ var V1WsRunCoverageSchema = external_exports.object({
8695
+ type: external_exports.literal("run:v1:coverage"),
8696
+ runId: external_exports.string(),
8697
+ coverage: V1CoverageReportSchema
8296
8698
  });
8297
8699
  external_exports.union([
8298
8700
  V1WsRunStartedSchema,
@@ -8301,19 +8703,360 @@ external_exports.union([
8301
8703
  V1WsTestExecutionSchema,
8302
8704
  V1WsOutlineExampleResultsSchema,
8303
8705
  V1WsRunCompletedSchema,
8304
- V1WsSessionUpdatedSchema
8706
+ V1WsRunCoverageSchema
8305
8707
  ]);
8708
+ var metricNames = ["lines", "branches", "functions", "statements"];
8709
+ function collectCoverageReport(options = {}) {
8710
+ const envEnabled = parseBoolean(process.env.LIVEDOC_COVERAGE);
8711
+ const configuredPath = firstNonEmpty(options.artifactPath, process.env.LIVEDOC_COVERAGE_PATH);
8712
+ const thresholds = resolveThresholds(options.thresholds);
8713
+ const rootDir = path2__default.resolve(options.rootDir || process.cwd());
8714
+ const reportsDirectory = options.reportsDirectory ? resolvePath(rootDir, options.reportsDirectory) : void 0;
8715
+ const explicit = envEnabled === true || options.enabled === true || !!configuredPath;
8716
+ if (options.coverageMap !== void 0) {
8717
+ try {
8718
+ const report = parseIstanbulCoverageMap(options.coverageMap, rootDir);
8719
+ const applied = applyThresholds(report, thresholds);
8720
+ return {
8721
+ ...applied,
8722
+ provenance: {
8723
+ tool: "vitest",
8724
+ format: "istanbul-coverage-map",
8725
+ detected: "auto",
8726
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString()
8727
+ }
8728
+ };
8729
+ } catch (error) {
8730
+ return diagnosticReport(
8731
+ "parse-failed",
8732
+ "warning",
8733
+ `LiveDoc could not parse Vitest's in-memory coverage map: ${error instanceof Error ? error.message : String(error)}`
8734
+ );
8735
+ }
8736
+ }
8737
+ const candidates = findCandidates(rootDir, reportsDirectory, configuredPath);
8738
+ if (candidates.length === 0) {
8739
+ return explicit ? diagnosticReport("artifact-missing", "warning", "Coverage was enabled, but LiveDoc could not find a supported coverage artifact.") : void 0;
8740
+ }
8741
+ const stale = [];
8742
+ for (const candidate of candidates) {
8743
+ if (!existsSync(candidate.path)) continue;
8744
+ let fileStats;
8745
+ try {
8746
+ fileStats = statSync(candidate.path);
8747
+ } catch (error) {
8748
+ return diagnosticReport(
8749
+ "parse-failed",
8750
+ "warning",
8751
+ `LiveDoc could not read the coverage artifact metadata: ${error instanceof Error ? error.message : String(error)}`,
8752
+ candidate.path
8753
+ );
8754
+ }
8755
+ const generatedAt = fileStats.mtime.toISOString();
8756
+ if (isStale(fileStats, options.runStartedAt)) {
8757
+ stale.push({
8758
+ severity: "warning",
8759
+ code: "stale",
8760
+ message: "LiveDoc found a coverage artifact, but it appears older than the current test run.",
8761
+ path: candidate.path
8762
+ });
8763
+ if (candidate.detected === "configured") {
8764
+ return { status: "invalid", diagnostics: stale };
8765
+ }
8766
+ continue;
8767
+ }
8768
+ if (candidate.format === "unsupported") {
8769
+ return diagnosticReport(
8770
+ "unsupported-format",
8771
+ "warning",
8772
+ `LiveDoc does not support this coverage artifact format yet: ${path2__default.basename(candidate.path)}.`,
8773
+ candidate.path
8774
+ );
8775
+ }
8776
+ try {
8777
+ const report = candidate.format === "istanbul-json-summary" ? parseIstanbulSummary(candidate.path, rootDir) : parseLcov(candidate.path, rootDir);
8778
+ const applied = applyThresholds(report, thresholds);
8779
+ return {
8780
+ ...applied,
8781
+ provenance: {
8782
+ tool: candidate.format === "lcov" ? "lcov" : "istanbul",
8783
+ format: candidate.format,
8784
+ path: candidate.path,
8785
+ detected: candidate.detected,
8786
+ generatedAt
8787
+ }
8788
+ };
8789
+ } catch (error) {
8790
+ return diagnosticReport(
8791
+ "parse-failed",
8792
+ "warning",
8793
+ `LiveDoc could not parse the coverage artifact: ${error instanceof Error ? error.message : String(error)}`,
8794
+ candidate.path
8795
+ );
8796
+ }
8797
+ }
8798
+ if (stale.length > 0 && explicit) {
8799
+ return { status: "invalid", diagnostics: stale };
8800
+ }
8801
+ return void 0;
8802
+ }
8803
+ function parseIstanbulSummary(filePath, rootDir) {
8804
+ const data = JSON.parse(readFileSync(filePath, "utf8"));
8805
+ if (!isRecord(data)) {
8806
+ throw new Error("Istanbul summary root must be an object.");
8807
+ }
8808
+ const total = data.total;
8809
+ if (!isRecord(total)) {
8810
+ throw new Error("Istanbul summary is missing the total coverage block.");
8811
+ }
8812
+ const files = Object.entries(data).filter(([key]) => key !== "total").map(([file, value]) => ({
8813
+ path: normalizeCoveragePath(file, rootDir),
8814
+ summary: normalizeSummary(value)
8815
+ })).filter((file) => Object.keys(file.summary).length > 0);
8816
+ return {
8817
+ status: "available",
8818
+ summary: normalizeSummary(total),
8819
+ files
8820
+ };
8821
+ }
8822
+ function parseIstanbulCoverageMap(value, rootDir) {
8823
+ if (!isIstanbulCoverageMap(value)) {
8824
+ throw new Error("Coverage value does not expose the Istanbul CoverageMap API.");
8825
+ }
8826
+ const files = value.files().map((file) => {
8827
+ const fileCoverage = value.fileCoverageFor(file);
8828
+ const summary = fileCoverage.toSummary().toJSON();
8829
+ return {
8830
+ path: normalizeCoveragePath(file, rootDir),
8831
+ summary: normalizeSummary(summary)
8832
+ };
8833
+ }).filter((file) => Object.keys(file.summary).length > 0);
8834
+ return {
8835
+ status: "available",
8836
+ summary: normalizeSummary(value.getCoverageSummary().toJSON()),
8837
+ files
8838
+ };
8839
+ }
8840
+ function parseLcov(filePath, rootDir) {
8841
+ const text = readFileSync(filePath, "utf8");
8842
+ const files = [];
8843
+ let current;
8844
+ const flush = () => {
8845
+ if (!current?.path) return;
8846
+ const lineValues = Array.from(current.lines.values());
8847
+ const branchValues = current.branches;
8848
+ const functionValues = Array.from(current.functions.values());
8849
+ const summary = {
8850
+ lines: metricFromHits(lineValues)
8851
+ };
8852
+ if (branchValues.length > 0) summary.branches = metricFromHits(branchValues);
8853
+ if (functionValues.length > 0) summary.functions = metricFromHits(functionValues);
8854
+ files.push({ path: normalizeCoveragePath(current.path, rootDir), summary });
8855
+ };
8856
+ for (const rawLine of text.split(/\r?\n/)) {
8857
+ const line = rawLine.trim();
8858
+ if (!line) continue;
8859
+ if (line === "end_of_record") {
8860
+ flush();
8861
+ current = void 0;
8862
+ continue;
8863
+ }
8864
+ if (line.startsWith("SF:")) {
8865
+ flush();
8866
+ current = { path: line.slice(3), lines: /* @__PURE__ */ new Map(), branches: [], functions: /* @__PURE__ */ new Map() };
8867
+ continue;
8868
+ }
8869
+ if (!current) continue;
8870
+ if (line.startsWith("DA:")) {
8871
+ const [lineNumber, hits] = line.slice(3).split(",");
8872
+ const parsedLine = Number(lineNumber);
8873
+ const parsedHits = Number(hits);
8874
+ if (Number.isFinite(parsedLine) && Number.isFinite(parsedHits)) current.lines.set(parsedLine, parsedHits);
8875
+ continue;
8876
+ }
8877
+ if (line.startsWith("BRDA:")) {
8878
+ const parts = line.slice(5).split(",");
8879
+ const taken = parts[3] === "-" ? null : Number(parts[3]);
8880
+ current.branches.push(Number.isFinite(taken) ? taken : null);
8881
+ continue;
8882
+ }
8883
+ if (line.startsWith("FNDA:")) {
8884
+ const [hits, name] = line.slice(5).split(",");
8885
+ const parsedHits = Number(hits);
8886
+ if (name && Number.isFinite(parsedHits)) current.functions.set(name, parsedHits);
8887
+ }
8888
+ }
8889
+ flush();
8890
+ return {
8891
+ status: "available",
8892
+ summary: aggregateSummaries(files.map((file) => file.summary)),
8893
+ files
8894
+ };
8895
+ }
8896
+ function metricFromHits(values) {
8897
+ const total = values.length;
8898
+ const covered = values.filter((value) => typeof value === "number" && value > 0).length;
8899
+ return makeMetric(covered, total);
8900
+ }
8901
+ function normalizeSummary(value) {
8902
+ const summary = {};
8903
+ if (!isRecord(value)) return summary;
8904
+ for (const metric of metricNames) {
8905
+ const raw = value[metric];
8906
+ if (!isRecord(raw)) continue;
8907
+ const total = Number(raw.total);
8908
+ const covered = Number(raw.covered);
8909
+ const skipped = Number(raw.skipped);
8910
+ const pct = raw.pct === "Unknown" || raw.pct === void 0 || raw.pct === null ? calculatePct(covered, total) : Number(raw.pct);
8911
+ if (!Number.isFinite(total) || !Number.isFinite(covered)) continue;
8912
+ summary[metric] = {
8913
+ covered,
8914
+ total,
8915
+ skipped: Number.isFinite(skipped) ? skipped : void 0,
8916
+ pct: Number.isFinite(pct) ? clampPct(pct) : calculatePct(covered, total)
8917
+ };
8918
+ }
8919
+ return summary;
8920
+ }
8921
+ function aggregateSummaries(summaries) {
8922
+ const aggregate = {};
8923
+ for (const metric of metricNames) {
8924
+ let covered = 0;
8925
+ let total = 0;
8926
+ let skipped = 0;
8927
+ let hasMetric = false;
8928
+ for (const summary of summaries) {
8929
+ const value = summary[metric];
8930
+ if (!value) continue;
8931
+ covered += value.covered;
8932
+ total += value.total;
8933
+ skipped += value.skipped ?? 0;
8934
+ hasMetric = true;
8935
+ }
8936
+ if (hasMetric) aggregate[metric] = { ...makeMetric(covered, total), skipped };
8937
+ }
8938
+ return aggregate;
8939
+ }
8940
+ function applyThresholds(report, thresholds) {
8941
+ const entries = Object.entries(thresholds);
8942
+ if (entries.length === 0) return report;
8943
+ const diagnostics = [...report.diagnostics ?? []];
8944
+ const applied = entries.map(([metric, minimum]) => {
8945
+ const actual = report.summary?.[metric]?.pct ?? null;
8946
+ const status = actual !== null && actual < minimum ? "warning" : "passed";
8947
+ if (status === "warning") {
8948
+ diagnostics.push({
8949
+ severity: "warning",
8950
+ code: "threshold-warning",
8951
+ message: `${metric} coverage is ${actual?.toFixed(1)}%, below the configured ${minimum}% threshold.`
8952
+ });
8953
+ }
8954
+ return { metric, minimum, actual, status };
8955
+ });
8956
+ return {
8957
+ ...report,
8958
+ thresholds: applied,
8959
+ diagnostics: diagnostics.length > 0 ? diagnostics : void 0
8960
+ };
8961
+ }
8962
+ function findCandidates(rootDir, reportsDirectory, configuredPath) {
8963
+ const candidates = [];
8964
+ const add = (candidatePath, detected) => {
8965
+ if (!candidatePath) return;
8966
+ const resolved = resolvePath(rootDir, candidatePath);
8967
+ if (candidates.some((candidate) => candidate.path.toLowerCase() === resolved.toLowerCase())) return;
8968
+ candidates.push({ path: resolved, format: detectFormat(resolved), detected });
8969
+ };
8970
+ add(configuredPath, "configured");
8971
+ add(reportsDirectory ? path2__default.join(reportsDirectory, "coverage-summary.json") : void 0, "auto");
8972
+ add(reportsDirectory ? path2__default.join(reportsDirectory, "lcov.info") : void 0, "auto");
8973
+ add(path2__default.join(rootDir, "coverage", "coverage-summary.json"), "auto");
8974
+ add(path2__default.join(rootDir, "coverage", "lcov.info"), "auto");
8975
+ add(path2__default.join(process.cwd(), "coverage", "coverage-summary.json"), "auto");
8976
+ add(path2__default.join(process.cwd(), "coverage", "lcov.info"), "auto");
8977
+ return candidates.filter((candidate) => existsSync(candidate.path) || candidate.detected === "configured");
8978
+ }
8979
+ function detectFormat(filePath) {
8980
+ const normalized = filePath.replace(/\\/g, "/").toLowerCase();
8981
+ if (normalized.endsWith("/coverage-summary.json") || normalized.endsWith("\\coverage-summary.json")) return "istanbul-json-summary";
8982
+ if (normalized.endsWith(".info") || normalized.endsWith(".lcov")) return "lcov";
8983
+ return "unsupported";
8984
+ }
8985
+ function diagnosticReport(code, severity, message, filePath) {
8986
+ return {
8987
+ status: severity === "info" ? "not-collected" : "invalid",
8988
+ diagnostics: [{ severity, code, message, path: filePath }]
8989
+ };
8990
+ }
8991
+ function normalizeCoveragePath(filePath, rootDir) {
8992
+ const absolute = path2__default.isAbsolute(filePath) ? path2__default.normalize(filePath) : path2__default.resolve(rootDir, filePath);
8993
+ let relative = path2__default.relative(rootDir, absolute);
8994
+ if (!relative || relative.startsWith("..")) relative = filePath;
8995
+ return relative.replace(/\\/g, "/").replace(/^\.\/+/, "");
8996
+ }
8997
+ function makeMetric(covered, total) {
8998
+ return { covered, total, pct: calculatePct(covered, total) };
8999
+ }
9000
+ function calculatePct(covered, total) {
9001
+ if (!Number.isFinite(total) || total <= 0) return null;
9002
+ return clampPct(covered / total * 100);
9003
+ }
9004
+ function clampPct(value) {
9005
+ return Math.max(0, Math.min(100, Math.round(value * 10) / 10));
9006
+ }
9007
+ function isStale(fileStats, runStartedAt) {
9008
+ if (!runStartedAt || !Number.isFinite(runStartedAt)) return false;
9009
+ return fileStats.mtimeMs + 2e3 < runStartedAt;
9010
+ }
9011
+ function resolveThresholds(configured) {
9012
+ const thresholds = {};
9013
+ for (const metric of metricNames) {
9014
+ const fromOptions = configured?.[metric];
9015
+ const fromEnv = process.env[`LIVEDOC_COVERAGE_THRESHOLD_${metric.toUpperCase()}`];
9016
+ const parsed = Number(fromOptions ?? fromEnv);
9017
+ if (Number.isFinite(parsed) && parsed >= 0 && parsed <= 100) thresholds[metric] = parsed;
9018
+ }
9019
+ return thresholds;
9020
+ }
9021
+ function parseBoolean(value) {
9022
+ if (value === void 0) return void 0;
9023
+ const normalized = value.trim().toLowerCase();
9024
+ if (["1", "true", "yes", "on"].includes(normalized)) return true;
9025
+ if (["0", "false", "no", "off"].includes(normalized)) return false;
9026
+ return void 0;
9027
+ }
9028
+ function firstNonEmpty(...values) {
9029
+ for (const value of values) {
9030
+ const trimmed = value?.trim();
9031
+ if (trimmed) return trimmed;
9032
+ }
9033
+ return void 0;
9034
+ }
9035
+ function resolvePath(rootDir, value) {
9036
+ return path2__default.isAbsolute(value) ? path2__default.normalize(value) : path2__default.resolve(rootDir, value);
9037
+ }
9038
+ function isRecord(value) {
9039
+ return typeof value === "object" && value !== null && !Array.isArray(value);
9040
+ }
9041
+ function isIstanbulCoverageMap(value) {
9042
+ if (!isRecord(value)) return false;
9043
+ return typeof value.getCoverageSummary === "function" && typeof value.files === "function" && typeof value.fileCoverageFor === "function";
9044
+ }
8306
9045
 
8307
9046
  // _src/app/reporter/LiveDocViewerReporterV1.ts
8308
9047
  var LiveDocViewerReporter = class {
8309
9048
  options;
9049
+ hasExplicitRunType;
8310
9050
  constructor(options) {
9051
+ this.hasExplicitRunType = options?.runType !== void 0;
8311
9052
  this.options = {
8312
9053
  server: options?.server || "http://localhost:3100",
8313
- project: options?.project || "default",
9054
+ project: options?.project || "livedoc",
8314
9055
  environment: options?.environment || "local",
9056
+ runType: this.parseRunType(options?.runType ?? process.env.LIVEDOC_RUN_TYPE),
8315
9057
  timeout: options?.timeout || 1e4,
8316
- silent: options?.silent ?? true
9058
+ silent: options?.silent ?? true,
9059
+ coverage: options?.coverage || {}
8317
9060
  };
8318
9061
  }
8319
9062
  async execute(results, rawOptions) {
@@ -8339,7 +9082,7 @@ var LiveDocViewerReporter = class {
8339
9082
  const testCase = this.buildSuiteTestCase(suite, pathContext);
8340
9083
  await this.upsertTestCase(runId, testCase);
8341
9084
  }
8342
- await this.completeRun(runId, results);
9085
+ await this.completeRun(runId, results, rawOptions);
8343
9086
  } catch (error) {
8344
9087
  if (!this.options.silent) {
8345
9088
  console.error("LiveDocViewerReporter error:", error);
@@ -8352,6 +9095,11 @@ var LiveDocViewerReporter = class {
8352
9095
  */
8353
9096
  buildTestRun(results, rawOptions) {
8354
9097
  this.applyRawOptions(rawOptions);
9098
+ if (this.options.runType === "partial") {
9099
+ throw new Error(
9100
+ "partial-export-unsupported: Partial runs require server history and cannot be exported directly."
9101
+ );
9102
+ }
8355
9103
  const pathContext = this.buildPathContext(results);
8356
9104
  const documents = [];
8357
9105
  for (const feature3 of results.features) {
@@ -8365,6 +9113,7 @@ var LiveDocViewerReporter = class {
8365
9113
  }
8366
9114
  const { summary, duration } = this.calculateSummary(results);
8367
9115
  const status = this.calculateOverallStatus(results);
9116
+ const coverage = this.collectCoverage(rawOptions);
8368
9117
  return {
8369
9118
  protocolVersion: "1.0",
8370
9119
  runId: randomUUID(),
@@ -8375,7 +9124,8 @@ var LiveDocViewerReporter = class {
8375
9124
  duration,
8376
9125
  status,
8377
9126
  summary,
8378
- documents
9127
+ documents,
9128
+ ...coverage ? { coverage } : {}
8379
9129
  };
8380
9130
  }
8381
9131
  async startRunSession(rawOptions) {
@@ -8414,22 +9164,44 @@ var LiveDocViewerReporter = class {
8414
9164
  if (rawOptions?.["viewer-environment"]) {
8415
9165
  this.options.environment = rawOptions["viewer-environment"];
8416
9166
  }
9167
+ if (!this.hasExplicitRunType && rawOptions?.["viewer-run-type"]) {
9168
+ this.options.runType = this.parseRunType(rawOptions["viewer-run-type"]);
9169
+ }
8417
9170
  if (rawOptions?.["viewer-timeout"] !== void 0) {
8418
9171
  const parsed = Number(rawOptions["viewer-timeout"]);
8419
9172
  if (Number.isFinite(parsed) && parsed > 0) {
8420
9173
  this.options.timeout = parsed;
8421
9174
  }
8422
9175
  }
9176
+ const coverage = rawOptions?.coverage;
9177
+ if (coverage && typeof coverage === "object") {
9178
+ this.options.coverage = {
9179
+ ...this.options.coverage,
9180
+ enabled: coverage.enabled ?? this.options.coverage.enabled,
9181
+ path: coverage.path ?? coverage.artifactPath ?? this.options.coverage.path,
9182
+ thresholds: coverage.thresholds ?? this.options.coverage.thresholds
9183
+ };
9184
+ }
9185
+ if (rawOptions?.["coverage-path"]) {
9186
+ this.options.coverage.path = String(rawOptions["coverage-path"]);
9187
+ }
8423
9188
  }
8424
9189
  async startRun() {
8425
9190
  const request = {
8426
9191
  project: this.options.project,
8427
9192
  environment: this.options.environment,
8428
- framework: "vitest"
9193
+ framework: "vitest",
9194
+ runType: this.options.runType
8429
9195
  };
8430
9196
  const response = await this.post("/api/v1/runs/start", request);
8431
9197
  return response?.runId || null;
8432
9198
  }
9199
+ parseRunType(value) {
9200
+ if (value === void 0 || value === null || value === "") return "full";
9201
+ const normalized = typeof value === "string" ? value.trim().toLowerCase() : value;
9202
+ if (normalized === "full" || normalized === "partial") return normalized;
9203
+ throw new Error(`Invalid LiveDoc run type '${String(value)}'. Expected 'full' or 'partial'.`);
9204
+ }
8433
9205
  async upsertTestCase(runId, testCase) {
8434
9206
  await this.post(`/api/v1/runs/${runId}/testcases`, { testCase });
8435
9207
  }
@@ -8444,7 +9216,7 @@ var LiveDocViewerReporter = class {
8444
9216
  for (const s of results.suites || []) {
8445
9217
  if (s?.filename) filenames.push(s.filename);
8446
9218
  }
8447
- const abs = filenames.filter(Boolean).map((p) => path.isAbsolute(p) ? p : path.resolve(process.cwd(), p)).map((p) => p.replace(/\\/g, "/"));
9219
+ const abs = filenames.filter(Boolean).map((p) => path2.isAbsolute(p) ? p : path2.resolve(process.cwd(), p)).map((p) => p.replace(/\\/g, "/"));
8448
9220
  const rootPath = this.findCommonRootPath(abs);
8449
9221
  return { rootPath };
8450
9222
  }
@@ -8466,7 +9238,7 @@ var LiveDocViewerReporter = class {
8466
9238
  }
8467
9239
  buildFileInfo(filename, rootPath) {
8468
9240
  const raw = filename || "";
8469
- const abs = raw ? path.isAbsolute(raw) ? raw : path.resolve(process.cwd(), raw) : "";
9241
+ const abs = raw ? path2.isAbsolute(raw) ? raw : path2.resolve(process.cwd(), raw) : "";
8470
9242
  const normalized = abs.replace(/\\/g, "/");
8471
9243
  const root = (rootPath || "").replace(/\\/g, "/").replace(/\/+$/g, "");
8472
9244
  if (!normalized) {
@@ -9020,16 +9792,31 @@ Actual: ${step.exception.actual}` : void 0)
9020
9792
  if (value instanceof Date) return { value: value.toISOString(), type: "date" };
9021
9793
  return { value, type: "object" };
9022
9794
  }
9023
- async completeRun(runId, results) {
9795
+ async completeRun(runId, results, rawOptions) {
9024
9796
  const { summary, duration } = this.calculateSummary(results);
9025
9797
  const overallStatus = this.calculateOverallStatus(results);
9798
+ const coverage = this.collectCoverage(rawOptions);
9026
9799
  const request = {
9027
9800
  status: overallStatus,
9028
9801
  duration,
9029
- summary
9802
+ summary,
9803
+ ...coverage ? { coverage } : {}
9030
9804
  };
9031
9805
  await this.post(`/api/v1/runs/${runId}/complete`, request);
9032
9806
  }
9807
+ collectCoverage(rawOptions) {
9808
+ const context = rawOptions?.coverageContext ?? {};
9809
+ const options = {
9810
+ enabled: this.options.coverage.enabled ?? context.enabled,
9811
+ coverageMap: context.coverageMap,
9812
+ artifactPath: this.options.coverage.path ?? context.artifactPath,
9813
+ rootDir: context.rootDir,
9814
+ reportsDirectory: context.reportsDirectory,
9815
+ runStartedAt: context.runStartedAt,
9816
+ thresholds: this.options.coverage.thresholds ?? context.thresholds
9817
+ };
9818
+ return collectCoverageReport(options);
9819
+ }
9033
9820
  mapStatus(status) {
9034
9821
  switch (status) {
9035
9822
  case "pass" /* pass */:
@@ -9178,6 +9965,7 @@ var LiveDocSpecReporter = class {
9178
9965
  liveDocSpec;
9179
9966
  options;
9180
9967
  exportConfig = null;
9968
+ coverageContext = { runStartedAt: Date.now() };
9181
9969
  streamEnabled = true;
9182
9970
  taskById = /* @__PURE__ */ new Map();
9183
9971
  streamedStates = /* @__PURE__ */ new Map();
@@ -9204,6 +9992,7 @@ var LiveDocSpecReporter = class {
9204
9992
  livedoc$1.options.publish.server = options.publish.server ?? livedoc$1.options.publish.server;
9205
9993
  livedoc$1.options.publish.project = options.publish.project ?? livedoc$1.options.publish.project;
9206
9994
  livedoc$1.options.publish.environment = options.publish.environment ?? livedoc$1.options.publish.environment;
9995
+ livedoc$1.options.publish.runType = options.publish.runType ?? livedoc$1.options.publish.runType;
9207
9996
  }
9208
9997
  if (options.export) {
9209
9998
  this.exportConfig = {
@@ -9220,22 +10009,14 @@ var LiveDocSpecReporter = class {
9220
10009
  this.setLiveDocOptions(this.options);
9221
10010
  }
9222
10011
  async onInit(ctx) {
10012
+ this.captureCoverageContext(ctx);
9223
10013
  this.liveDocSpec.executionStart();
9224
- if (!livedoc$1.options.publish.enabled) {
9225
- const envServerUrl = process.env.LIVEDOC_SERVER_URL || process.env.LIVEDOC_PUBLISH_SERVER;
9226
- if (envServerUrl) {
10014
+ const envServerUrl = this.applyPublishEnvironmentOverrides();
10015
+ if (!envServerUrl && !livedoc$1.options.publish.enabled) {
10016
+ const serverInfo = await this.discoverLiveDocServer();
10017
+ if (serverInfo) {
9227
10018
  livedoc$1.options.publish.enabled = true;
9228
- livedoc$1.options.publish.server = envServerUrl;
9229
- } else {
9230
- try {
9231
- const { discoverServer } = await import('@swedevtools/livedoc-server');
9232
- const serverInfo = await discoverServer();
9233
- if (serverInfo) {
9234
- livedoc$1.options.publish.enabled = true;
9235
- livedoc$1.options.publish.server = serverInfo.url;
9236
- }
9237
- } catch {
9238
- }
10019
+ livedoc$1.options.publish.server = serverInfo.url;
9239
10020
  }
9240
10021
  }
9241
10022
  if (livedoc$1.options.publish.enabled) {
@@ -9247,6 +10028,102 @@ LiveDoc Viewer: Connecting to ${publishOptions.server}...`);
9247
10028
  `);
9248
10029
  }
9249
10030
  }
10031
+ onCoverage(coverageMap) {
10032
+ this.coverageContext.coverageMap = coverageMap;
10033
+ }
10034
+ captureCoverageContext(ctx) {
10035
+ const config = ctx?.config ?? {};
10036
+ const coverage = config.coverage ?? {};
10037
+ const rawOptions = this.options.rawOptions ?? {};
10038
+ const reporterCoverage = rawOptions.coverage ?? {};
10039
+ const rootDir = config.root || process.cwd();
10040
+ const reportsDirectory = coverage.reportsDirectory ? resolve(rootDir, coverage.reportsDirectory) : void 0;
10041
+ this.coverageContext = {
10042
+ enabled: Boolean(coverage.enabled) || Boolean(reporterCoverage.enabled),
10043
+ artifactPath: reporterCoverage.path ?? reporterCoverage.artifactPath,
10044
+ rootDir,
10045
+ reportsDirectory,
10046
+ runStartedAt: Date.now(),
10047
+ thresholds: reporterCoverage.thresholds
10048
+ };
10049
+ }
10050
+ applyPublishEnvironmentOverrides() {
10051
+ const envServerUrl = this.firstEnvironmentValue(
10052
+ "LIVEDOC_SERVER_URL",
10053
+ "LIVEDOC_PUBLISH_SERVER",
10054
+ "LIVEDOC_VIEWER_SERVER"
10055
+ );
10056
+ const envProject = this.firstEnvironmentValue(
10057
+ "LIVEDOC_PROJECT",
10058
+ "LIVEDOC_PUBLISH_PROJECT",
10059
+ "LIVEDOC_VIEWER_PROJECT"
10060
+ );
10061
+ const envEnvironment = this.firstEnvironmentValue(
10062
+ "LIVEDOC_ENVIRONMENT",
10063
+ "LIVEDOC_PUBLISH_ENV",
10064
+ "LIVEDOC_VIEWER_ENV"
10065
+ );
10066
+ const envRunType = this.firstEnvironmentValue("LIVEDOC_RUN_TYPE");
10067
+ if (envProject) {
10068
+ livedoc$1.options.publish.project = envProject;
10069
+ }
10070
+ if (envEnvironment) {
10071
+ livedoc$1.options.publish.environment = envEnvironment;
10072
+ }
10073
+ if (envRunType) {
10074
+ const normalizedRunType = envRunType.toLowerCase();
10075
+ if (normalizedRunType !== "full" && normalizedRunType !== "partial") {
10076
+ throw new Error(
10077
+ `Invalid LIVEDOC_RUN_TYPE value '${envRunType}'. Expected 'full' or 'partial'.`
10078
+ );
10079
+ }
10080
+ livedoc$1.options.publish.runType = normalizedRunType;
10081
+ }
10082
+ if (envServerUrl) {
10083
+ livedoc$1.options.publish.enabled = true;
10084
+ livedoc$1.options.publish.server = envServerUrl;
10085
+ }
10086
+ return envServerUrl;
10087
+ }
10088
+ firstEnvironmentValue(...names) {
10089
+ for (const name of names) {
10090
+ const value = process.env[name]?.trim();
10091
+ if (value) return value;
10092
+ }
10093
+ return void 0;
10094
+ }
10095
+ async discoverLiveDocServer() {
10096
+ try {
10097
+ const { discoverServer } = await import('@swedevtools/livedoc-server');
10098
+ const serverInfo = await discoverServer();
10099
+ if (serverInfo) return serverInfo;
10100
+ } catch {
10101
+ }
10102
+ return await this.discoverDefaultLocalServer();
10103
+ }
10104
+ async discoverDefaultLocalServer() {
10105
+ const defaultPort = 3100;
10106
+ const defaultUrl = `http://localhost:${defaultPort}`;
10107
+ if (await this.isLiveDocServerHealthy(defaultUrl)) {
10108
+ return { url: defaultUrl, port: defaultPort };
10109
+ }
10110
+ return null;
10111
+ }
10112
+ async isLiveDocServerHealthy(serverUrl) {
10113
+ const controller = new AbortController();
10114
+ const timeout = setTimeout(() => controller.abort(), 500);
10115
+ try {
10116
+ const response = await fetch(`${serverUrl}/api/health`, {
10117
+ headers: { "Connection": "close" },
10118
+ signal: controller.signal
10119
+ });
10120
+ return response.ok;
10121
+ } catch {
10122
+ return false;
10123
+ } finally {
10124
+ clearTimeout(timeout);
10125
+ }
10126
+ }
9250
10127
  onCollected(files) {
9251
10128
  if (!this.streamEnabled) return;
9252
10129
  this.taskById.clear();
@@ -9310,30 +10187,34 @@ LiveDoc Viewer: Connecting to ${publishOptions.server}...`);
9310
10187
  server: publishOptions.server,
9311
10188
  project: publishOptions.project,
9312
10189
  environment: publishOptions.environment,
10190
+ runType: publishOptions.runType,
9313
10191
  silent: false
9314
10192
  });
9315
- const rawOptions = this.options.rawOptions || {};
9316
- if (!rawOptions.postReporters) {
9317
- rawOptions.postReporters = [];
10193
+ const rawOptions2 = this.options.rawOptions || {};
10194
+ if (!rawOptions2.postReporters) {
10195
+ rawOptions2.postReporters = [];
9318
10196
  }
9319
- rawOptions.postReporters.push(viewerReporter);
10197
+ rawOptions2.postReporters.push(viewerReporter);
9320
10198
  }
9321
- await this.liveDocSpec.executionEnd(results, this.options.rawOptions);
10199
+ const rawOptions = this.options.rawOptions || {};
10200
+ rawOptions.coverageContext = this.coverageContext;
10201
+ await this.liveDocSpec.executionEnd(results, rawOptions);
9322
10202
  if (this.exportConfig) {
9323
- this.exportTestRunJson(results);
10203
+ this.exportTestRunJson(results, rawOptions);
9324
10204
  }
9325
10205
  }
9326
- exportTestRunJson(results) {
10206
+ exportTestRunJson(results, rawOptions) {
9327
10207
  const exportConfig = this.exportConfig;
9328
10208
  const outputPath = resolve(exportConfig.output);
9329
- const project = exportConfig.project || livedoc$1.options.publish.project || "default";
10209
+ const project = exportConfig.project || livedoc$1.options.publish.project || "livedoc";
9330
10210
  const environment = exportConfig.environment || livedoc$1.options.publish.environment || (process.env.CI ? "ci" : "local");
9331
10211
  const converter = new LiveDocViewerReporter({
9332
10212
  project,
9333
10213
  environment,
10214
+ runType: livedoc$1.options.publish.runType,
9334
10215
  silent: true
9335
10216
  });
9336
- const testRun = converter.buildTestRun(results);
10217
+ const testRun = converter.buildTestRun(results, rawOptions);
9337
10218
  try {
9338
10219
  mkdirSync(dirname(outputPath), { recursive: true });
9339
10220
  const json = JSON.stringify(testRun, null, 2);
@@ -9356,9 +10237,15 @@ LiveDoc Viewer: Connecting to ${publishOptions.server}...`);
9356
10237
  for (const suite of file.tasks || []) {
9357
10238
  if (suite.type === "suite") {
9358
10239
  if (suite.name.startsWith("Specification:")) {
9359
- specifications.push(this.buildSpecificationFromSuite(suite, file.filepath));
10240
+ const specification3 = this.buildSpecificationFromSuite(suite, file.filepath);
10241
+ if (specification3.rules.length > 0) {
10242
+ specifications.push(specification3);
10243
+ }
9360
10244
  } else if (suite.name.startsWith("Feature:")) {
9361
- features.push(this.buildFeatureFromSuite(suite, file.filepath));
10245
+ const feature3 = this.buildFeatureFromSuite(suite, file.filepath);
10246
+ if (feature3.scenarios.length > 0) {
10247
+ features.push(feature3);
10248
+ }
9362
10249
  } else {
9363
10250
  suites.push(this.buildVitestSuiteFromTask(suite, file.filepath));
9364
10251
  }
@@ -9403,6 +10290,7 @@ LiveDoc Viewer: Connecting to ${publishOptions.server}...`);
9403
10290
  }
9404
10291
  for (const task of tasks) {
9405
10292
  if (task === backgroundSuite) continue;
10293
+ if (!this.hasUnfilteredLiveDocLeaf(task)) continue;
9406
10294
  if (typeof task.name === "string" && task.name.startsWith("Scenario:")) {
9407
10295
  const exampleSuites = (task.tasks || []).filter(
9408
10296
  (t) => t.type === "suite" && t.name.startsWith("Example ")
@@ -9426,7 +10314,7 @@ LiveDoc Viewer: Connecting to ${publishOptions.server}...`);
9426
10314
  scenarioOutline3.description = parsed.description;
9427
10315
  scenarioOutline3.tags = parsed.tags;
9428
10316
  const exampleSuites = (suite.tasks || []).filter(
9429
- (t) => t.type === "suite" && t.name.startsWith("Example ")
10317
+ (t) => t.type === "suite" && t.name.startsWith("Example ") && this.hasUnfilteredLiveDocLeaf(t)
9430
10318
  );
9431
10319
  const firstExampleSuite = exampleSuites[0];
9432
10320
  const firstExampleLiveDoc = this.findFirstLiveDocStepMetaInSuite(firstExampleSuite);
@@ -9478,7 +10366,7 @@ LiveDoc Viewer: Connecting to ${publishOptions.server}...`);
9478
10366
  background3.tags = parsed.tags;
9479
10367
  const forcePending = suite?.mode === "skip" || suite?.mode === "todo";
9480
10368
  for (const task of suite.tasks || []) {
9481
- if (task.type === "test") {
10369
+ if (task.type === "test" && !this.isFilteredOutTask(task)) {
9482
10370
  const step = this.buildStepFromTest(task, background3, forcePending);
9483
10371
  background3.addStep(step);
9484
10372
  }
@@ -9493,7 +10381,7 @@ LiveDoc Viewer: Connecting to ${publishOptions.server}...`);
9493
10381
  scenario3.tags = parsed.tags;
9494
10382
  const forcePending = suite?.mode === "skip" || suite?.mode === "todo";
9495
10383
  for (const task of suite.tasks || []) {
9496
- if (task.type === "test") {
10384
+ if (task.type === "test" && !this.isFilteredOutTask(task)) {
9497
10385
  const step = this.buildStepFromTest(task, scenario3, forcePending);
9498
10386
  scenario3.addStep(step);
9499
10387
  }
@@ -9517,7 +10405,7 @@ LiveDoc Viewer: Connecting to ${publishOptions.server}...`);
9517
10405
  example.example = this.sanitizeExampleKeys(metaExampleValues);
9518
10406
  example.exampleRaw = example.example;
9519
10407
  for (const task of suite.tasks || []) {
9520
- if (task.type === "test") {
10408
+ if (task.type === "test" && !this.isFilteredOutTask(task)) {
9521
10409
  const step = this.buildStepFromTest(task, example, forcePending);
9522
10410
  example.addStep(step);
9523
10411
  }
@@ -9531,9 +10419,20 @@ LiveDoc Viewer: Connecting to ${publishOptions.server}...`);
9531
10419
  if (!livedoc3 || typeof livedoc3 !== "object") return void 0;
9532
10420
  return livedoc3;
9533
10421
  }
10422
+ isFilteredOutTask(task) {
10423
+ return this.getLiveDocMetaFromTask(task)?.filter?.filteredOut === true;
10424
+ }
10425
+ hasUnfilteredLiveDocLeaf(task) {
10426
+ if (task?.type === "test") {
10427
+ const kind = this.getLiveDocMetaFromTask(task)?.kind;
10428
+ return (kind === "step" || kind === "rule" || kind === "ruleExample") && !this.isFilteredOutTask(task);
10429
+ }
10430
+ return Array.isArray(task?.tasks) && task.tasks.some((child) => this.hasUnfilteredLiveDocLeaf(child));
10431
+ }
9534
10432
  findFirstLiveDocStepMetaInSuite(suite) {
9535
10433
  const tasks = suite?.tasks || [];
9536
10434
  for (const task of tasks) {
10435
+ if (this.isFilteredOutTask(task)) continue;
9537
10436
  if (task?.type !== "test") continue;
9538
10437
  const livedoc3 = this.getLiveDocMetaFromTask(task);
9539
10438
  if (livedoc3?.kind === "step") return livedoc3;
@@ -9721,8 +10620,10 @@ LiveDoc Viewer: Connecting to ${publishOptions.server}...`);
9721
10620
  specification3.tags = parsed.tags;
9722
10621
  specification3.filename = filepath;
9723
10622
  for (const task of suite.tasks || []) {
10623
+ if (!this.hasUnfilteredLiveDocLeaf(task)) continue;
9724
10624
  if (task.type === "suite") {
9725
10625
  const exampleTests = (task.tasks || []).filter((t) => {
10626
+ if (this.isFilteredOutTask(t)) return false;
9726
10627
  if (t.type !== "test") return false;
9727
10628
  const meta = this.getLiveDocMetaFromTask(t);
9728
10629
  return meta?.kind === "ruleExample";
@@ -9731,6 +10632,7 @@ LiveDoc Viewer: Connecting to ${publishOptions.server}...`);
9731
10632
  const ruleOutline3 = this.buildRuleOutlineFromSuite(task, specification3);
9732
10633
  specification3.rules.push(ruleOutline3);
9733
10634
  }
10635
+ if (this.isFilteredOutTask(task)) continue;
9734
10636
  } else if (task.type === "test") {
9735
10637
  if (task.name.startsWith("Rule:")) {
9736
10638
  const rule3 = this.buildRuleFromTest(task, specification3);
@@ -9779,6 +10681,7 @@ LiveDoc Viewer: Connecting to ${publishOptions.server}...`);
9779
10681
  buildRuleOutlineFromSuite(suite, specification3) {
9780
10682
  const ruleOutline3 = new RuleOutline(specification3);
9781
10683
  const exampleTests = (suite.tasks || []).filter((t) => {
10684
+ if (this.isFilteredOutTask(t)) return false;
9782
10685
  if (t.type !== "test") return false;
9783
10686
  const meta = this.getLiveDocMetaFromTask(t);
9784
10687
  return meta?.kind === "ruleExample";