@reportforge/playwright-pdf 0.2.2 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1221,13 +1221,13 @@ var require_ast = __commonJS({
1221
1221
  helperExpression: function helperExpression(node) {
1222
1222
  return node.type === "SubExpression" || (node.type === "MustacheStatement" || node.type === "BlockStatement") && !!(node.params && node.params.length || node.hash);
1223
1223
  },
1224
- scopedId: function scopedId(path11) {
1225
- return /^\.|this\b/.test(path11.original);
1224
+ scopedId: function scopedId(path14) {
1225
+ return /^\.|this\b/.test(path14.original);
1226
1226
  },
1227
1227
  // an ID is simple if it only has one part, and that part is not
1228
1228
  // `..` or `this`.
1229
- simpleId: function simpleId(path11) {
1230
- return path11.parts.length === 1 && !AST.helpers.scopedId(path11) && !path11.depth;
1229
+ simpleId: function simpleId(path14) {
1230
+ return path14.parts.length === 1 && !AST.helpers.scopedId(path14) && !path14.depth;
1231
1231
  }
1232
1232
  }
1233
1233
  };
@@ -2301,12 +2301,12 @@ var require_helpers2 = __commonJS({
2301
2301
  loc
2302
2302
  };
2303
2303
  }
2304
- function prepareMustache(path11, params, hash, open, strip, locInfo) {
2304
+ function prepareMustache(path14, params, hash, open, strip, locInfo) {
2305
2305
  var escapeFlag = open.charAt(3) || open.charAt(2), escaped = escapeFlag !== "{" && escapeFlag !== "&";
2306
2306
  var decorator = /\*/.test(open);
2307
2307
  return {
2308
2308
  type: decorator ? "Decorator" : "MustacheStatement",
2309
- path: path11,
2309
+ path: path14,
2310
2310
  params,
2311
2311
  hash,
2312
2312
  escaped,
@@ -2626,9 +2626,9 @@ var require_compiler = __commonJS({
2626
2626
  },
2627
2627
  DecoratorBlock: function DecoratorBlock(decorator) {
2628
2628
  var program = decorator.program && this.compileProgram(decorator.program);
2629
- var params = this.setupFullMustacheParams(decorator, program, void 0), path11 = decorator.path;
2629
+ var params = this.setupFullMustacheParams(decorator, program, void 0), path14 = decorator.path;
2630
2630
  this.useDecorators = true;
2631
- this.opcode("registerDecorator", params.length, path11.original);
2631
+ this.opcode("registerDecorator", params.length, path14.original);
2632
2632
  },
2633
2633
  PartialStatement: function PartialStatement(partial) {
2634
2634
  this.usePartial = true;
@@ -2692,46 +2692,46 @@ var require_compiler = __commonJS({
2692
2692
  }
2693
2693
  },
2694
2694
  ambiguousSexpr: function ambiguousSexpr(sexpr, program, inverse) {
2695
- var path11 = sexpr.path, name = path11.parts[0], isBlock = program != null || inverse != null;
2696
- this.opcode("getContext", path11.depth);
2695
+ var path14 = sexpr.path, name = path14.parts[0], isBlock = program != null || inverse != null;
2696
+ this.opcode("getContext", path14.depth);
2697
2697
  this.opcode("pushProgram", program);
2698
2698
  this.opcode("pushProgram", inverse);
2699
- path11.strict = true;
2700
- this.accept(path11);
2699
+ path14.strict = true;
2700
+ this.accept(path14);
2701
2701
  this.opcode("invokeAmbiguous", name, isBlock);
2702
2702
  },
2703
2703
  simpleSexpr: function simpleSexpr(sexpr) {
2704
- var path11 = sexpr.path;
2705
- path11.strict = true;
2706
- this.accept(path11);
2704
+ var path14 = sexpr.path;
2705
+ path14.strict = true;
2706
+ this.accept(path14);
2707
2707
  this.opcode("resolvePossibleLambda");
2708
2708
  },
2709
2709
  helperSexpr: function helperSexpr(sexpr, program, inverse) {
2710
- var params = this.setupFullMustacheParams(sexpr, program, inverse), path11 = sexpr.path, name = path11.parts[0];
2710
+ var params = this.setupFullMustacheParams(sexpr, program, inverse), path14 = sexpr.path, name = path14.parts[0];
2711
2711
  if (this.options.knownHelpers[name]) {
2712
2712
  this.opcode("invokeKnownHelper", params.length, name);
2713
2713
  } else if (this.options.knownHelpersOnly) {
2714
2714
  throw new _exception2["default"]("You specified knownHelpersOnly, but used the unknown helper " + name, sexpr);
2715
2715
  } else {
2716
- path11.strict = true;
2717
- path11.falsy = true;
2718
- this.accept(path11);
2719
- this.opcode("invokeHelper", params.length, path11.original, _ast2["default"].helpers.simpleId(path11));
2716
+ path14.strict = true;
2717
+ path14.falsy = true;
2718
+ this.accept(path14);
2719
+ this.opcode("invokeHelper", params.length, path14.original, _ast2["default"].helpers.simpleId(path14));
2720
2720
  }
2721
2721
  },
2722
- PathExpression: function PathExpression(path11) {
2723
- this.addDepth(path11.depth);
2724
- this.opcode("getContext", path11.depth);
2725
- var name = path11.parts[0], scoped = _ast2["default"].helpers.scopedId(path11), blockParamId = !path11.depth && !scoped && this.blockParamIndex(name);
2722
+ PathExpression: function PathExpression(path14) {
2723
+ this.addDepth(path14.depth);
2724
+ this.opcode("getContext", path14.depth);
2725
+ var name = path14.parts[0], scoped = _ast2["default"].helpers.scopedId(path14), blockParamId = !path14.depth && !scoped && this.blockParamIndex(name);
2726
2726
  if (blockParamId) {
2727
- this.opcode("lookupBlockParam", blockParamId, path11.parts);
2727
+ this.opcode("lookupBlockParam", blockParamId, path14.parts);
2728
2728
  } else if (!name) {
2729
2729
  this.opcode("pushContext");
2730
- } else if (path11.data) {
2730
+ } else if (path14.data) {
2731
2731
  this.options.data = true;
2732
- this.opcode("lookupData", path11.depth, path11.parts, path11.strict);
2732
+ this.opcode("lookupData", path14.depth, path14.parts, path14.strict);
2733
2733
  } else {
2734
- this.opcode("lookupOnContext", path11.parts, path11.falsy, path11.strict, scoped);
2734
+ this.opcode("lookupOnContext", path14.parts, path14.falsy, path14.strict, scoped);
2735
2735
  }
2736
2736
  },
2737
2737
  StringLiteral: function StringLiteral(string) {
@@ -3087,16 +3087,16 @@ var require_util = __commonJS({
3087
3087
  }
3088
3088
  exports2.urlGenerate = urlGenerate;
3089
3089
  function normalize(aPath) {
3090
- var path11 = aPath;
3090
+ var path14 = aPath;
3091
3091
  var url = urlParse(aPath);
3092
3092
  if (url) {
3093
3093
  if (!url.path) {
3094
3094
  return aPath;
3095
3095
  }
3096
- path11 = url.path;
3096
+ path14 = url.path;
3097
3097
  }
3098
- var isAbsolute = exports2.isAbsolute(path11);
3099
- var parts = path11.split(/\/+/);
3098
+ var isAbsolute = exports2.isAbsolute(path14);
3099
+ var parts = path14.split(/\/+/);
3100
3100
  for (var part, up = 0, i = parts.length - 1; i >= 0; i--) {
3101
3101
  part = parts[i];
3102
3102
  if (part === ".") {
@@ -3113,15 +3113,15 @@ var require_util = __commonJS({
3113
3113
  }
3114
3114
  }
3115
3115
  }
3116
- path11 = parts.join("/");
3117
- if (path11 === "") {
3118
- path11 = isAbsolute ? "/" : ".";
3116
+ path14 = parts.join("/");
3117
+ if (path14 === "") {
3118
+ path14 = isAbsolute ? "/" : ".";
3119
3119
  }
3120
3120
  if (url) {
3121
- url.path = path11;
3121
+ url.path = path14;
3122
3122
  return urlGenerate(url);
3123
3123
  }
3124
- return path11;
3124
+ return path14;
3125
3125
  }
3126
3126
  exports2.normalize = normalize;
3127
3127
  function join(aRoot, aPath) {
@@ -5924,8 +5924,8 @@ var require_printer = __commonJS({
5924
5924
  return this.accept(sexpr.path) + " " + params + hash;
5925
5925
  };
5926
5926
  PrintVisitor.prototype.PathExpression = function(id) {
5927
- var path11 = id.parts.join("/");
5928
- return (id.data ? "@" : "") + "PATH:" + path11;
5927
+ var path14 = id.parts.join("/");
5928
+ return (id.data ? "@" : "") + "PATH:" + path14;
5929
5929
  };
5930
5930
  PrintVisitor.prototype.StringLiteral = function(string) {
5931
5931
  return '"' + string.value + '"';
@@ -5966,8 +5966,8 @@ var require_lib = __commonJS({
5966
5966
  handlebars.print = printer.print;
5967
5967
  module2.exports = handlebars;
5968
5968
  function extension(module3, filename) {
5969
- var fs12 = require("fs");
5970
- var templateString = fs12.readFileSync(filename, "utf8");
5969
+ var fs14 = require("fs");
5970
+ var templateString = fs14.readFileSync(filename, "utf8");
5971
5971
  module3.exports = handlebars.compile(templateString);
5972
5972
  }
5973
5973
  if (typeof require !== "undefined" && require.extensions) {
@@ -5996,7 +5996,7 @@ var require_util2 = __commonJS({
5996
5996
  "node_modules/chrome-finder/lib/util.js"(exports2, module2) {
5997
5997
  "use strict";
5998
5998
  init_cjs_shims();
5999
- var fs12 = require("fs");
5999
+ var fs14 = require("fs");
6000
6000
  var newLineRegex = /\r?\n/;
6001
6001
  function sort(installations, priorities) {
6002
6002
  const defaultPriority = 10;
@@ -6014,7 +6014,7 @@ var require_util2 = __commonJS({
6014
6014
  return false;
6015
6015
  }
6016
6016
  try {
6017
- fs12.accessSync(file);
6017
+ fs14.accessSync(file);
6018
6018
  return true;
6019
6019
  } catch (e) {
6020
6020
  return false;
@@ -6025,7 +6025,7 @@ var require_util2 = __commonJS({
6025
6025
  return false;
6026
6026
  }
6027
6027
  try {
6028
- var stat = fs12.statSync(file);
6028
+ var stat = fs14.statSync(file);
6029
6029
  return stat && typeof stat.isFile === "function" && stat.isFile();
6030
6030
  } catch (e) {
6031
6031
  return false;
@@ -6045,7 +6045,7 @@ var require_win32 = __commonJS({
6045
6045
  "node_modules/chrome-finder/lib/win32.js"(exports2, module2) {
6046
6046
  "use strict";
6047
6047
  init_cjs_shims();
6048
- var path11 = require("path");
6048
+ var path14 = require("path");
6049
6049
  var { canAccess } = require_util2();
6050
6050
  function win32() {
6051
6051
  const installations = [];
@@ -6059,7 +6059,7 @@ var require_win32 = __commonJS({
6059
6059
  const prefixes = [process.env.LOCALAPPDATA, process.env.PROGRAMFILES, process.env["PROGRAMFILES(X86)"]];
6060
6060
  prefixes.forEach((prefix) => suffixes.forEach((suffix) => {
6061
6061
  if (prefix) {
6062
- const chromePath = path11.join(prefix, suffix);
6062
+ const chromePath = path14.join(prefix, suffix);
6063
6063
  if (canAccess(chromePath)) {
6064
6064
  installations.push(chromePath);
6065
6065
  }
@@ -6077,7 +6077,7 @@ var require_darwin = __commonJS({
6077
6077
  "use strict";
6078
6078
  init_cjs_shims();
6079
6079
  var { execSync: execSync3 } = require("child_process");
6080
- var path11 = require("path");
6080
+ var path14 = require("path");
6081
6081
  var { canAccess, newLineRegex, sort } = require_util2();
6082
6082
  function darwin() {
6083
6083
  const suffixes = ["/Contents/MacOS/Google Chrome Canary", "/Contents/MacOS/Google Chrome", "/Contents/MacOS/Chromium"];
@@ -6087,7 +6087,7 @@ var require_darwin = __commonJS({
6087
6087
  ${LSREGISTER} -dump | grep -E -i -o '/.+(google chrome( canary)?|chromium)\\.app(\\s|$)' | grep -E -v 'Caches|TimeMachine|Temporary|/Volumes|\\.Trash'
6088
6088
  `).toString().split(newLineRegex).forEach((inst) => {
6089
6089
  suffixes.forEach((suffix) => {
6090
- const execPath = path11.join(inst.trim(), suffix);
6090
+ const execPath = path14.join(inst.trim(), suffix);
6091
6091
  if (canAccess(execPath)) {
6092
6092
  installations.push(execPath);
6093
6093
  }
@@ -6116,8 +6116,8 @@ var require_linux = __commonJS({
6116
6116
  "use strict";
6117
6117
  init_cjs_shims();
6118
6118
  var { execSync: execSync3, execFileSync: execFileSync2 } = require("child_process");
6119
- var path11 = require("path");
6120
- var fs12 = require("fs");
6119
+ var path14 = require("path");
6120
+ var fs14 = require("fs");
6121
6121
  var { canAccess, sort, isExecutable, newLineRegex } = require_util2();
6122
6122
  function findChromeExecutablesForLinuxDesktop(folder) {
6123
6123
  const argumentsRegex = /(^[^ ]+).*/;
@@ -6134,7 +6134,7 @@ var require_linux = __commonJS({
6134
6134
  function linux() {
6135
6135
  let installations = [];
6136
6136
  const desktopInstallationFolders = [
6137
- path11.join(require("os").homedir(), ".local/share/applications/"),
6137
+ path14.join(require("os").homedir(), ".local/share/applications/"),
6138
6138
  "/usr/share/applications/"
6139
6139
  ];
6140
6140
  desktopInstallationFolders.forEach((folder) => {
@@ -6160,7 +6160,7 @@ var require_linux = __commonJS({
6160
6160
  ].map((possiblePath) => {
6161
6161
  try {
6162
6162
  const chromePathToTest = possiblePath + "/" + executable;
6163
- if (fs12.existsSync(chromePathToTest) && canAccess(chromePathToTest) && isExecutable(chromePathToTest)) {
6163
+ if (fs14.existsSync(chromePathToTest) && canAccess(chromePathToTest) && isExecutable(chromePathToTest)) {
6164
6164
  installations.push(chromePathToTest);
6165
6165
  return chromePathToTest;
6166
6166
  }
@@ -6749,8 +6749,8 @@ function getErrorMap() {
6749
6749
  // node_modules/zod/v3/helpers/parseUtil.js
6750
6750
  init_cjs_shims();
6751
6751
  var makeIssue = (params) => {
6752
- const { data, path: path11, errorMaps, issueData } = params;
6753
- const fullPath = [...path11, ...issueData.path || []];
6752
+ const { data, path: path14, errorMaps, issueData } = params;
6753
+ const fullPath = [...path14, ...issueData.path || []];
6754
6754
  const fullIssue = {
6755
6755
  ...issueData,
6756
6756
  path: fullPath
@@ -6870,11 +6870,11 @@ var errorUtil;
6870
6870
 
6871
6871
  // node_modules/zod/v3/types.js
6872
6872
  var ParseInputLazyPath = class {
6873
- constructor(parent, value, path11, key) {
6873
+ constructor(parent, value, path14, key) {
6874
6874
  this._cachedPath = [];
6875
6875
  this.parent = parent;
6876
6876
  this.data = value;
6877
- this._path = path11;
6877
+ this._path = path14;
6878
6878
  this._key = key;
6879
6879
  }
6880
6880
  get path() {
@@ -10354,6 +10354,16 @@ var notifyConfigSchema = external_exports.object({
10354
10354
  discord: channelConfigSchema.optional(),
10355
10355
  email: emailChannelConfigSchema.optional()
10356
10356
  }).optional();
10357
+ var failureAnalysisConfigSchema = external_exports.object({
10358
+ enabled: external_exports.boolean().default(true),
10359
+ maxClusters: external_exports.number().int().min(1).max(50).default(10),
10360
+ // Qualitative floor (not a numeric % — consistent with the match-strength design).
10361
+ minStrength: external_exports.enum(["weak", "moderate", "strong"]).default("weak"),
10362
+ maxFailuresToAnalyse: external_exports.number().int().min(1).default(500),
10363
+ // Consumed by the Plan 3 feedback collector; inert until then.
10364
+ collectUnclassified: external_exports.boolean().default(true),
10365
+ autoUpdateModel: external_exports.boolean().default(true)
10366
+ }).default({});
10357
10367
  var ReporterOptionsSchema = external_exports.object({
10358
10368
  outputFile: external_exports.string().optional().default(DEFAULT_OPTIONS.outputFile),
10359
10369
  template: external_exports.union([
@@ -10383,6 +10393,7 @@ var ReporterOptionsSchema = external_exports.object({
10383
10393
  historySize: external_exports.number().int().min(2).optional().default(DEFAULT_OPTIONS.historySize),
10384
10394
  showTrend: external_exports.boolean().optional().default(DEFAULT_OPTIONS.showTrend),
10385
10395
  flakinessTopN: external_exports.number().int().min(0, "flakinessTopN must be 0 or greater").optional().default(DEFAULT_OPTIONS.flakinessTopN),
10396
+ failureAnalysis: failureAnalysisConfigSchema,
10386
10397
  templatePath: external_exports.union([
10387
10398
  external_exports.string().regex(/\.hbs$/, "templatePath must end with .hbs").refine((v) => v.slice(0, -4).trim().length > 0, "templatePath basename must not be empty"),
10388
10399
  external_exports.array(
@@ -10622,6 +10633,17 @@ function clearCache() {
10622
10633
  init_cjs_shims();
10623
10634
  var LICENSE_PUBLIC_KEY_PEM = "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEAbvknstk0gxoJ7UwATdlxR8btxvO7u7RdpO1uV90PtuE=\n-----END PUBLIC KEY-----";
10624
10635
 
10636
+ // src/analysis/ModelStore.ts
10637
+ init_cjs_shims();
10638
+ var import_node_fs = __toESM(require("fs"));
10639
+ var import_node_os = __toESM(require("os"));
10640
+ var import_node_path = __toESM(require("path"));
10641
+ var import_node_crypto = require("crypto");
10642
+
10643
+ // src/analysis/model-bundle.ts
10644
+ init_cjs_shims();
10645
+ var MODEL = { "alpha": 1, "labels": ["assertion", "env-config", "flaky-by-retry", "locator-not-found", "navigation", "network", "timeout"], "logLikelihoods": { "assertion": { "NUM": -4.574710978503383, "assertionerror": -4.980176086611547, "be": -4.980176086611547, "cart": -4.980176086611547, "checkout": -4.980176086611547, "com": -4.980176086611547, "dashboard": -4.980176086611547, "deep": -4.980176086611547, "equal": -4.980176086611547, "equality": -4.980176086611547, "error": -4.980176086611547, "expect": -3.5938817254916566, "expected": -3.1083739097099556, "failed": -4.980176086611547, "false": -4.980176086611547, "got": -4.980176086611547, "id": -4.574710978503383, "john": -4.980176086611547, "locator": -4.574710978503383, "login": -4.980176086611547, "must": -4.980176086611547, "page": -4.980176086611547, "pattern": -4.980176086611547, "received": -3.275427994373122, "string": -4.980176086611547, "to": -4.980176086611547, "tobe": -4.980176086611547, "tobetruthy": -4.980176086611547, "toequal": -4.980176086611547, "tohavecount": -4.980176086611547, "tohavetext": -4.980176086611547, "tohavetitle": -4.980176086611547, "tohavevalue": -4.980176086611547, "truthy": -4.980176086611547, "value": -4.980176086611547 }, "env-config": { "NUM": -4.997212273764115, "api": -4.997212273764115, "at": -4.997212273764115, "back": -4.997212273764115, "base": -4.997212273764115, "baseurl": -4.997212273764115, "browsers": -4.997212273764115, "cannot": -4.997212273764115, "ci": -4.997212273764115, "config": -4.304065093204169, "directory": -4.591747165655951, "dotenv": -4.997212273764115, "enoent": -4.997212273764115, "env": -4.591747165655951, "environment": -4.591747165655951, "error": -4.08092154188996, "existent": -4.997212273764115, "failed": -4.997212273764115, "falling": -4.997212273764115, "file": -4.997212273764115, "find": -4.997212273764115, "from": -4.997212273764115, "in": -4.591747165655951, "invalid": -4.997212273764115, "is": -4.997212273764115, "json": -4.591747165655951, "key": -4.997212273764115, "load": -4.997212273764115, "missing": -4.997212273764115, "module": -4.997212273764115, "no": -4.997212273764115, "non": -4.997212273764115, "not": -4.997212273764115, "open": -4.997212273764115, "or": -4.997212273764115, "path": -4.997212273764115, "playwright": -4.591747165655951, "points": -4.997212273764115, "position": -4.997212273764115, "required": -4.997212273764115, "set": -4.997212273764115, "spec": -4.997212273764115, "such": -4.997212273764115, "syntaxerror": -4.997212273764115, "test": -4.997212273764115, "to": -4.304065093204169, "token": -4.997212273764115, "ts": -4.997212273764115, "tsconfig": -4.997212273764115, "undefined": -4.591747165655951, "unexpected": -4.997212273764115, "url": -4.997212273764115, "values": -4.997212273764115, "variable": -4.591747165655951 }, "flaky-by-retry": { "after": -4.58836306767171, "and": -4.993828175779875, "animation": -4.993828175779875, "attempt": -4.07753744390572, "between": -4.993828175779875, "blip": -4.993828175779875, "cause": -4.993828175779875, "change": -4.993828175779875, "click": -4.993828175779875, "code": -4.993828175779875, "commit": -4.993828175779875, "condition": -4.993828175779875, "dependency": -4.993828175779875, "detached": -4.993828175779875, "deterministic": -4.993828175779875, "element": -4.993828175779875, "failed": -4.300680995219929, "fails": -4.993828175779875, "flaky": -4.07753744390572, "green": -4.993828175779875, "in": -4.993828175779875, "intermittent": -4.993828175779875, "locator": -4.993828175779875, "marked": -4.993828175779875, "network": -4.993828175779875, "no": -4.993828175779875, "non": -4.993828175779875, "on": -3.895215887111765, "order": -4.993828175779875, "passed": -3.895215887111765, "quarantined": -4.993828175779875, "race": -4.993828175779875, "reproducible": -4.993828175779875, "rerun": -4.993828175779875, "retry": -4.07753744390572, "runs": -4.993828175779875, "same": -4.993828175779875, "second": -4.993828175779875, "stale": -4.993828175779875, "succeeded": -4.993828175779875, "test": -4.58836306767171, "the": -4.993828175779875, "then": -4.993828175779875, "timeout": -4.993828175779875, "unstable": -4.993828175779875, "with": -4.993828175779875, "without": -4.993828175779875 }, "locator-not-found": { "an": -4.993828175779875, "attached": -4.993828175779875, "btn": -4.993828175779875, "button": -4.58836306767171, "cart": -4.993828175779875, "checkout": -4.993828175779875, "click": -4.993828175779875, "data": -4.993828175779875, "div": -4.993828175779875, "dom": -4.993828175779875, "element": -4.07753744390572, "elements": -4.300680995219929, "error": -4.58836306767171, "find": -4.993828175779875, "for": -4.58836306767171, "found": -4.58836306767171, "getbyrole": -4.993828175779875, "getbytext": -4.993828175779875, "icon": -4.993828175779875, "id": -4.993828175779875, "is": -4.993828175779875, "locator": -4.07753744390572, "matched": -4.993828175779875, "matches": -4.993828175779875, "missing": -4.993828175779875, "mode": -4.58836306767171, "name": -4.993828175779875, "no": -4.58836306767171, "node": -4.993828175779875, "not": -4.58836306767171, "resolved": -4.58836306767171, "save": -4.993828175779875, "selector": -4.58836306767171, "strict": -4.58836306767171, "submit": -4.993828175779875, "testid": -4.993828175779875, "textcontent": -4.993828175779875, "the": -4.58836306767171, "to": -4.07753744390572, "total": -4.993828175779875, "unable": -4.993828175779875, "violation": -4.58836306767171, "waiting": -4.993828175779875, "with": -4.993828175779875, "xpath": -4.993828175779875 }, "navigation": { "NUM": -4.983606621708336, "aborted": -4.983606621708336, "another": -4.983606621708336, "back": -4.983606621708336, "because": -4.578141513600172, "by": -4.578141513600172, "checkout": -4.983606621708336, "context": -4.983606621708336, "crashed": -4.983606621708336, "dashboard": -4.983606621708336, "destroyed": -4.983606621708336, "detached": -4.983606621708336, "entry": -4.983606621708336, "err": -4.578141513600172, "exceeded": -4.983606621708336, "execution": -4.983606621708336, "failed": -4.983606621708336, "frame": -4.578141513600172, "goback": -4.983606621708336, "goto": -4.290459441148391, "history": -4.983606621708336, "home": -4.983606621708336, "interrupted": -4.578141513600172, "is": -4.983606621708336, "likely": -4.983606621708336, "login": -4.983606621708336, "many": -4.983606621708336, "maybe": -4.983606621708336, "most": -4.983606621708336, "navigate": -4.983606621708336, "navigating": -4.983606621708336, "navigation": -3.7308436532129683, "net": -4.578141513600172, "no": -4.983606621708336, "of": -4.983606621708336, "page": -3.884994333040227, "redirects": -4.983606621708336, "timeout": -4.983606621708336, "to": -3.7308436532129683, "too": -4.983606621708336, "waitforurl": -4.983606621708336, "was": -4.578141513600172 }, "network": { "NUM": -4.57126863431241, "URL": -4.283586561860629, "again": -4.976733742420574, "api": -4.976733742420574, "apirequestcontext": -4.976733742420574, "at": -4.976733742420574, "body": -4.976733742420574, "connect": -4.976733742420574, "connection": -4.57126863431241, "disconnected": -4.976733742420574, "eai": -4.976733742420574, "econnrefused": -4.976733742420574, "err": -3.8781214537524646, "error": -4.976733742420574, "failed": -4.283586561860629, "fetch": -4.976733742420574, "fetching": -4.976733742420574, "from": -4.976733742420574, "get": -4.976733742420574, "getaddrinfo": -4.976733742420574, "goto": -4.976733742420574, "hang": -4.976733742420574, "internet": -4.976733742420574, "loading": -4.976733742420574, "name": -4.976733742420574, "net": -3.8781214537524646, "not": -4.976733742420574, "on": -4.976733742420574, "out": -4.976733742420574, "page": -4.57126863431241, "products": -4.976733742420574, "protocol": -4.976733742420574, "reading": -4.976733742420574, "reason": -4.976733742420574, "refused": -4.976733742420574, "request": -4.57126863431241, "resolved": -4.976733742420574, "resource": -4.976733742420574, "response": -4.976733742420574, "route": -4.976733742420574, "socket": -4.976733742420574, "ssl": -4.976733742420574, "timed": -4.976733742420574, "to": -4.976733742420574, "up": -4.976733742420574, "upstream": -4.976733742420574, "while": -4.976733742420574 }, "timeout": { "NUM": -3.4965075614664802, "after": -5.000584958242754, "be": -4.59511985013459, "cart": -5.000584958242754, "click": -5.000584958242754, "element": -5.000584958242754, "event": -5.000584958242754, "exceeded": -3.6142905971228636, "expect": -5.000584958242754, "fill": -5.000584958242754, "for": -3.7478219897473863, "function": -5.000584958242754, "load": -5.000584958242754, "locator": -4.084294226368599, "navigation": -5.000584958242754, "of": -5.000584958242754, "out": -5.000584958242754, "page": -4.59511985013459, "running": -5.000584958242754, "selector": -5.000584958242754, "submit": -5.000584958242754, "test": -4.59511985013459, "timed": -5.000584958242754, "timeout": -3.6142905971228636, "timeouterror": -4.59511985013459, "to": -4.59511985013459, "tobevisible": -5.000584958242754, "visible": -4.59511985013459, "waitforloadstate": -5.000584958242754, "waitforselector": -5.000584958242754, "waiting": -3.7478219897473863, "while": -5.000584958242754 } }, "playwrightVersion": "1.59.1", "priors": { "assertion": -1.9636097261547143, "env-config": -1.9636097261547143, "flaky-by-retry": -1.9636097261547143, "locator-not-found": -1.9636097261547143, "navigation": -1.9636097261547143, "network": -1.9636097261547143, "timeout": -1.845826690498331 }, "totals": { "assertion": 64, "env-config": 69, "flaky-by-retry": 68, "locator-not-found": 68, "navigation": 65, "network": 63, "timeout": 70 }, "version": 1, "vocabSize": 227 };
10646
+
10625
10647
  // src/utils/logger.ts
10626
10648
  init_cjs_shims();
10627
10649
  var PREFIX = "[reportforge]";
@@ -10643,6 +10665,83 @@ ${JSON.stringify(data, null, 2)}` : "";
10643
10665
  }
10644
10666
  };
10645
10667
 
10668
+ // src/analysis/ModelStore.ts
10669
+ var MODEL_SIG_DOMAIN = "reportforge-model-sig:v1\n";
10670
+ var testDeps = null;
10671
+ function cacheFile() {
10672
+ return testDeps?.cacheFile ?? import_node_path.default.join(import_node_os.default.homedir(), ".reportforge", "model.json");
10673
+ }
10674
+ function publicKey() {
10675
+ const pem = testDeps?.publicKeyPem ?? LICENSE_PUBLIC_KEY_PEM;
10676
+ try {
10677
+ return pem && pem.includes("PUBLIC KEY") ? (0, import_node_crypto.createPublicKey)(pem) : null;
10678
+ } catch {
10679
+ return null;
10680
+ }
10681
+ }
10682
+ function verifyModelSignature(modelJson, signatureB64url) {
10683
+ const key = publicKey();
10684
+ if (!key) return false;
10685
+ try {
10686
+ return (0, import_node_crypto.verify)(null, Buffer.from(MODEL_SIG_DOMAIN + modelJson), key, Buffer.from(signatureB64url, "base64url"));
10687
+ } catch {
10688
+ return false;
10689
+ }
10690
+ }
10691
+ function schemaValid(m) {
10692
+ const x = m;
10693
+ return !!x && Array.isArray(x.labels) && x.labels.length > 0 && typeof x.version === "number" && !!x.priors && !!x.totals && !!x.logLikelihoods && typeof x.vocabSize === "number" && typeof x.alpha === "number";
10694
+ }
10695
+ function readCache2() {
10696
+ try {
10697
+ const raw = import_node_fs.default.readFileSync(cacheFile(), "utf8");
10698
+ const c = JSON.parse(raw);
10699
+ if (typeof c.version === "number" && typeof c.modelJson === "string" && typeof c.signature === "string") {
10700
+ return c;
10701
+ }
10702
+ } catch {
10703
+ }
10704
+ return null;
10705
+ }
10706
+ function loadModel(autoUpdate) {
10707
+ if (!autoUpdate) return MODEL;
10708
+ const c = readCache2();
10709
+ if (!c || c.version <= MODEL.version) return MODEL;
10710
+ if (!verifyModelSignature(c.modelJson, c.signature)) {
10711
+ logger.debug(`cached model v${c.version} signature invalid \u2014 using bundled (possible tampering or key mismatch)`);
10712
+ return MODEL;
10713
+ }
10714
+ try {
10715
+ const m = JSON.parse(c.modelJson);
10716
+ if (schemaValid(m)) return m;
10717
+ logger.debug(`cached model v${c.version} failed schema validation \u2014 using bundled`);
10718
+ } catch {
10719
+ logger.debug(`cached model v${c.version} JSON parse failed \u2014 using bundled`);
10720
+ }
10721
+ return MODEL;
10722
+ }
10723
+ function acceptDeliveredModel(d) {
10724
+ try {
10725
+ const current = readCache2()?.version ?? MODEL.version;
10726
+ if (d.version <= current) return;
10727
+ if (!verifyModelSignature(d.modelJson, d.signature)) {
10728
+ logger.debug(`delivered model v${d.version} signature invalid \u2014 rejected`);
10729
+ return;
10730
+ }
10731
+ const m = JSON.parse(d.modelJson);
10732
+ if (!schemaValid(m)) {
10733
+ logger.debug(`delivered model v${d.version} failed schema validation \u2014 rejected`);
10734
+ return;
10735
+ }
10736
+ const file = cacheFile();
10737
+ import_node_fs.default.mkdirSync(import_node_path.default.dirname(file), { recursive: true });
10738
+ const tmp = `${file}.${process.pid}.tmp`;
10739
+ import_node_fs.default.writeFileSync(tmp, JSON.stringify({ version: d.version, modelJson: d.modelJson, signature: d.signature }), { mode: 384 });
10740
+ import_node_fs.default.renameSync(tmp, file);
10741
+ } catch {
10742
+ }
10743
+ }
10744
+
10646
10745
  // src/license/LicenseClient.ts
10647
10746
  var DEFAULT_SERVER_URL = "https://reportforge.org";
10648
10747
  var REFRESH_THRESHOLD_MS = 24 * 60 * 60 * 1e3;
@@ -10656,6 +10755,8 @@ var LicenseClient = class {
10656
10755
  this.serverUrl = normalizeUrl(
10657
10756
  opts.serverUrl ?? process.env.REPORTFORGE_SERVER_URL ?? DEFAULT_SERVER_URL
10658
10757
  );
10758
+ this.autoUpdateModel = opts.autoUpdateModel ?? true;
10759
+ this.modelVersionForServer = this.autoUpdateModel ? opts.currentModelVersion ?? 0 : Number.MAX_SAFE_INTEGER;
10659
10760
  }
10660
10761
  async resolve() {
10661
10762
  const key = this.rawKey || process.env.RF_LICENSE_KEY?.trim();
@@ -10736,10 +10837,14 @@ var LicenseClient = class {
10736
10837
  machinesUsed: result.machinesUsed,
10737
10838
  machineLimit: result.machineLimit
10738
10839
  });
10840
+ if (this.autoUpdateModel && result.model) {
10841
+ acceptDeliveredModel(result.model);
10842
+ }
10739
10843
  return {
10740
10844
  plan: "subscription",
10741
10845
  source: "jwt-online",
10742
10846
  key: normalizedKey,
10847
+ jwt: result.jwt,
10743
10848
  expiry: new Date(payload.exp * 1e3),
10744
10849
  machinesUsed: result.machinesUsed,
10745
10850
  machineLimit: result.machineLimit
@@ -10771,13 +10876,15 @@ var LicenseClient = class {
10771
10876
  return this.post("/api/license/activate", {
10772
10877
  licenseKey,
10773
10878
  fingerprint: this.fingerprint.hash,
10774
- label: this.fingerprint.label
10879
+ label: this.fingerprint.label,
10880
+ modelVersion: this.modelVersionForServer
10775
10881
  });
10776
10882
  }
10777
10883
  async refresh(jwt) {
10778
10884
  return this.post("/api/license/refresh", {
10779
10885
  jwt,
10780
- label: this.fingerprint.label
10886
+ label: this.fingerprint.label,
10887
+ modelVersion: this.modelVersionForServer
10781
10888
  });
10782
10889
  }
10783
10890
  async post(route, body) {
@@ -10822,6 +10929,7 @@ function toLicenseInfo(payload, cached, normalizedKey, source) {
10822
10929
  plan: "subscription",
10823
10930
  source,
10824
10931
  key: normalizedKey,
10932
+ jwt: cached.jwt,
10825
10933
  expiry: new Date(payload.exp * 1e3),
10826
10934
  // Surface the snapshot from the last activate/refresh. Undefined for
10827
10935
  // caches written by older reporter versions before these fields were
@@ -10834,11 +10942,307 @@ function normalizeUrl(url) {
10834
10942
  return url.endsWith("/") ? url.slice(0, -1) : url;
10835
10943
  }
10836
10944
 
10945
+ // src/analysis/index.ts
10946
+ init_cjs_shims();
10947
+
10948
+ // src/analysis/FailureClusterer.ts
10949
+ init_cjs_shims();
10950
+
10951
+ // src/analysis/NaiveBayesClassifier.ts
10952
+ init_cjs_shims();
10953
+
10954
+ // src/analysis/tokenize.ts
10955
+ init_cjs_shims();
10956
+ function tokenize(text) {
10957
+ return text.toLowerCase().replace(/https?:\/\/\S+/gi, " URL ").replace(/[^a-zA-Z0-9\s]/g, " ").replace(/\b\d{3,}[a-z]*\b/g, " NUM ").split(/\s+/).filter((t) => t.length > 1);
10958
+ }
10959
+
10960
+ // src/analysis/NaiveBayesClassifier.ts
10961
+ function classify(model, text) {
10962
+ const tokens = tokenize(text);
10963
+ const vocabTokens = tokens.filter(
10964
+ (tok) => model.labels.some((l) => model.logLikelihoods[l]?.[tok] !== void 0)
10965
+ );
10966
+ if (vocabTokens.length === 0) {
10967
+ return { label: "unknown", margin: 0 };
10968
+ }
10969
+ const scores = {};
10970
+ for (const label of model.labels) {
10971
+ let s = model.priors[label];
10972
+ const oov = Math.log(model.alpha / (model.totals[label] + model.alpha * model.vocabSize));
10973
+ const ll = model.logLikelihoods[label];
10974
+ for (const tok of vocabTokens) {
10975
+ s += ll[tok] ?? oov;
10976
+ }
10977
+ scores[label] = s;
10978
+ }
10979
+ const ranked = Object.entries(scores).sort((a, b) => b[1] - a[1]);
10980
+ const topLog = ranked[0][1];
10981
+ const secondLog = ranked[1]?.[1] ?? topLog;
10982
+ const margin = (topLog - secondLog) / vocabTokens.length;
10983
+ return { label: ranked[0][0], margin };
10984
+ }
10985
+
10986
+ // src/analysis/constants.ts
10987
+ init_cjs_shims();
10988
+ var STRENGTH = {
10989
+ unknownMaxMargin: 0.05,
10990
+ collectMaxMargin: 0.1,
10991
+ harvestMinMargin: 0.25,
10992
+ moderateMinMargin: 0.1,
10993
+ strongMinMargin: 0.25
10994
+ };
10995
+ var STRENGTH_LEVELS = ["weak", "moderate", "strong"];
10996
+ var FLAKY_MARGIN = 999;
10997
+
10998
+ // src/analysis/FailureClusterer.ts
10999
+ var MAX_TESTS_PER_CLUSTER = 20;
11000
+ var MAX_MESSAGE_CHARS = 200;
11001
+ function strengthOf(margin) {
11002
+ if (margin >= STRENGTH.strongMinMargin) return "strong";
11003
+ if (margin >= STRENGTH.moderateMinMargin) return "moderate";
11004
+ return "weak";
11005
+ }
11006
+ function classifyInput(f) {
11007
+ const head = (f.error.stack ?? "").split("\n").slice(0, 3).join(" ");
11008
+ return `${f.error.message} ${head}`;
11009
+ }
11010
+ function topFrame(stack) {
11011
+ if (!stack) return void 0;
11012
+ for (const line of stack.split("\n")) {
11013
+ const t = line.trim();
11014
+ if (t.startsWith("at ") && !t.replace(/\\/g, "/").includes("node_modules/playwright")) return t;
11015
+ }
11016
+ return void 0;
11017
+ }
11018
+ function representativeMessage(message) {
11019
+ const firstLine = message.split("\n")[0];
11020
+ return firstLine.length > MAX_MESSAGE_CHARS ? `${firstLine.slice(0, MAX_MESSAGE_CHARS)}\u2026` : firstLine;
11021
+ }
11022
+ function classifyOne(f, model) {
11023
+ if (f.retryHistory.some((r) => r.status === "passed")) {
11024
+ return { category: "flaky-by-retry", margin: FLAKY_MARGIN, failure: f, frame: topFrame(f.error.stack) };
11025
+ }
11026
+ const { label, margin } = classify(model, classifyInput(f));
11027
+ return { category: label, margin, failure: f, frame: topFrame(f.error.stack) };
11028
+ }
11029
+ function classifyAll(failures, model) {
11030
+ return failures.map((f) => classifyOne(f, model));
11031
+ }
11032
+ function clusterClassified(classified) {
11033
+ const groups = /* @__PURE__ */ new Map();
11034
+ for (const c of classified) {
11035
+ const key = `${c.category}::${c.frame ?? "__no-stack__"}`;
11036
+ const bucket = groups.get(key);
11037
+ if (bucket) bucket.push(c);
11038
+ else groups.set(key, [c]);
11039
+ }
11040
+ const clusters = [];
11041
+ for (const members of groups.values()) {
11042
+ const margin = Math.max(...members.map((m) => m.margin));
11043
+ clusters.push({
11044
+ category: members[0].category,
11045
+ strength: strengthOf(margin),
11046
+ margin,
11047
+ count: members.length,
11048
+ tests: members.slice(0, MAX_TESTS_PER_CLUSTER).map((m) => m.failure.testTitle),
11049
+ testsTotal: members.length,
11050
+ representativeMessage: representativeMessage(members[0].failure.error.message),
11051
+ stackFrame: members[0].frame
11052
+ });
11053
+ }
11054
+ return clusters;
11055
+ }
11056
+
11057
+ // src/analysis/SummaryWriter.ts
11058
+ init_cjs_shims();
11059
+ var CATEGORY_LABEL = {
11060
+ assertion: "Assertion failure",
11061
+ timeout: "Timeout",
11062
+ "locator-not-found": "Element not found",
11063
+ network: "Network error",
11064
+ navigation: "Navigation error",
11065
+ "env-config": "Environment / config",
11066
+ "flaky-by-retry": "Flaky (passed on retry)",
11067
+ unknown: "Unclassified"
11068
+ };
11069
+ var SHORT_LABEL = {
11070
+ assertion: "assertion",
11071
+ timeout: "timeout",
11072
+ "locator-not-found": "element not found",
11073
+ network: "network",
11074
+ navigation: "navigation",
11075
+ "env-config": "env/config",
11076
+ "flaky-by-retry": "flaky",
11077
+ unknown: "unclassified"
11078
+ };
11079
+ function buildOneLiner(clusters) {
11080
+ if (clusters.length === 0) return "";
11081
+ const byCategory = /* @__PURE__ */ new Map();
11082
+ for (const c of clusters) {
11083
+ byCategory.set(c.category, (byCategory.get(c.category) ?? 0) + c.count);
11084
+ }
11085
+ const head = `${clusters.length} root ${clusters.length === 1 ? "cause" : "causes"}`;
11086
+ const tally = [...byCategory.entries()].sort((a, b) => b[1] - a[1]).map(([cat, count]) => `${count} ${SHORT_LABEL[cat]}`);
11087
+ return [head, ...tally].join(" \xB7 ");
11088
+ }
11089
+ function dominantCategory(clusters) {
11090
+ return clusters[0]?.category ?? "unknown";
11091
+ }
11092
+
11093
+ // src/analysis/UnclassifiedCollector.ts
11094
+ init_cjs_shims();
11095
+ var import_node_fs2 = __toESM(require("fs"));
11096
+ var import_node_os2 = __toESM(require("os"));
11097
+ var import_node_path2 = __toESM(require("path"));
11098
+
11099
+ // src/analysis/feedback-csv.ts
11100
+ init_cjs_shims();
11101
+ var FEEDBACK_HEADER = "category,margin,text";
11102
+ function parseFeedbackRows(content) {
11103
+ return content.trim().split("\n").slice(1).filter(Boolean).map((line) => {
11104
+ const parts = line.split(",");
11105
+ return { category: parts[0], margin: parts[1], text: parts.slice(2).join(","), raw: line };
11106
+ });
11107
+ }
11108
+ function feedbackIdentity(category, text) {
11109
+ return `${category} ${text}`;
11110
+ }
11111
+
11112
+ // src/analysis/UnclassifiedCollector.ts
11113
+ var MAX_ROWS = 500;
11114
+ var FILENAME = "unclassified.csv";
11115
+ var URI_RE = /[a-z][\w+.-]*:\/\/\S+/gi;
11116
+ var EMAIL_RE = /\b[\w.+-]+@[\w.-]+\.\w+\b/g;
11117
+ var SECRET_RE = /\b(?=[A-Za-z0-9_+/=-]*[0-9])(?=[A-Za-z0-9_+/=-]*[A-Za-z])[A-Za-z0-9_+/=-]{16,}\b/g;
11118
+ var IP_RE = /\b\d{1,3}(?:\.\d{1,3}){3}\b/g;
11119
+ var WIN_PATH_RE = /[A-Za-z]:\\[^\s'"]+/g;
11120
+ var UNIX_PATH_RE = /(?:\/[\w.-]+){2,}/g;
11121
+ function redactForStorage(raw) {
11122
+ const redacted = raw.replace(URI_RE, " URL ").replace(EMAIL_RE, " EMAIL ").replace(SECRET_RE, " SECRET ").replace(IP_RE, " IP ").replace(WIN_PATH_RE, " PATH ").replace(UNIX_PATH_RE, " PATH ");
11123
+ return tokenize(redacted).join(" ");
11124
+ }
11125
+ function shouldCollect(c) {
11126
+ return c.category === "unknown" || c.margin <= STRENGTH.collectMaxMargin;
11127
+ }
11128
+ function tildeify(p) {
11129
+ const home = import_node_os2.default.homedir();
11130
+ return p.startsWith(home) ? `~${p.slice(home.length)}` : p;
11131
+ }
11132
+ function collectUnclassified(classified, projectDir) {
11133
+ const rows = classified.filter(shouldCollect).map((c) => ({ text: redactForStorage(c.failure.error.message), category: c.category, margin: c.margin })).filter((r) => r.text.length > 0);
11134
+ if (rows.length === 0) return;
11135
+ const file = import_node_path2.default.join(projectDir, FILENAME);
11136
+ const firstTime = !import_node_fs2.default.existsSync(file);
11137
+ const byIdentity = /* @__PURE__ */ new Map();
11138
+ if (!firstTime) {
11139
+ for (const r of parseFeedbackRows(import_node_fs2.default.readFileSync(file, "utf8"))) {
11140
+ byIdentity.set(feedbackIdentity(r.category, r.text), r.raw);
11141
+ }
11142
+ }
11143
+ let added = false;
11144
+ for (const r of rows) {
11145
+ const id = feedbackIdentity(r.category, r.text);
11146
+ byIdentity.delete(id);
11147
+ byIdentity.set(id, `${r.category},${r.margin},${r.text}`);
11148
+ added = true;
11149
+ }
11150
+ if (!added) return;
11151
+ const all = [...byIdentity.values()].slice(-MAX_ROWS);
11152
+ import_node_fs2.default.mkdirSync(projectDir, { recursive: true });
11153
+ import_node_fs2.default.writeFileSync(file, `${FEEDBACK_HEADER}
11154
+ ${all.join("\n")}
11155
+ `, "utf8");
11156
+ if (firstTime) {
11157
+ logger.info(
11158
+ `Unrecognized failures collected locally at ${tildeify(file)} to improve offline classification. Local only - never sent anywhere. Disable: failureAnalysis.collectUnclassified=false`
11159
+ );
11160
+ }
11161
+ }
11162
+
11163
+ // src/utils/paths.ts
11164
+ init_cjs_shims();
11165
+ var import_node_os3 = __toESM(require("os"));
11166
+ var import_node_path3 = __toESM(require("path"));
11167
+ var import_node_crypto2 = __toESM(require("crypto"));
11168
+ function resolveProjectDir(options, cwd) {
11169
+ const projectKey = import_node_crypto2.default.createHash("sha256").update(cwd + (options.outputFile ?? "")).digest("hex").slice(0, 8);
11170
+ return import_node_path3.default.join(import_node_os3.default.homedir(), ".reportforge", projectKey);
11171
+ }
11172
+
11173
+ // src/analysis/index.ts
11174
+ var DEFAULTS = { maxClusters: 10, minStrength: "weak", maxFailuresToAnalyse: 500 };
11175
+ var STRENGTH_RANK = Object.fromEntries(
11176
+ STRENGTH_LEVELS.map((level, i) => [level, i])
11177
+ );
11178
+ function analyseClassified(classified, totalFailures, opts = {}) {
11179
+ const maxClusters = opts.maxClusters ?? DEFAULTS.maxClusters;
11180
+ const minRank = STRENGTH_RANK[opts.minStrength ?? DEFAULTS.minStrength];
11181
+ const cap = opts.maxFailuresToAnalyse ?? DEFAULTS.maxFailuresToAnalyse;
11182
+ const clusters = clusterClassified(classified).filter((c) => STRENGTH_RANK[c.strength] >= minRank).sort((a, b) => b.count - a.count).slice(0, maxClusters);
11183
+ return {
11184
+ clusters,
11185
+ totalFailures,
11186
+ analysedCount: classified.length,
11187
+ truncated: totalFailures > cap,
11188
+ // dominantCategory is part of the AnalysisResult contract; reserved for
11189
+ // consumers (e.g. a future executive badge / notify summary). Not rendered
11190
+ // by the v1 partials.
11191
+ dominantCategory: dominantCategory(clusters),
11192
+ oneLiner: buildOneLiner(clusters)
11193
+ };
11194
+ }
11195
+ function templatesConsumeAnalysis(options) {
11196
+ if (options.templatePath && options.templatePath.length > 0) return true;
11197
+ const templates = Array.isArray(options.template) ? options.template : [options.template];
11198
+ return templates.some((t) => t === "detailed" || t === "executive");
11199
+ }
11200
+ function runFailureAnalysis(reportData, options, cwd) {
11201
+ const fa = options.failureAnalysis;
11202
+ if (reportData.stats.failed === 0 || !fa.enabled) return;
11203
+ const cap = fa.maxFailuresToAnalyse;
11204
+ const analysed = reportData.failures.slice(0, cap);
11205
+ const model = loadModel(fa.autoUpdateModel);
11206
+ const classified = classifyAll(analysed, model);
11207
+ if (templatesConsumeAnalysis(options)) {
11208
+ reportData.analysis = analyseClassified(classified, reportData.failures.length, fa);
11209
+ }
11210
+ if (fa.collectUnclassified) {
11211
+ collectUnclassified(classified, resolveProjectDir(options, cwd));
11212
+ }
11213
+ }
11214
+
10837
11215
  // src/collector/DataCollector.ts
10838
11216
  init_cjs_shims();
10839
11217
 
10840
11218
  // src/collector/SuiteWalker.ts
10841
11219
  init_cjs_shims();
11220
+
11221
+ // src/collector/stats-utils.ts
11222
+ init_cjs_shims();
11223
+ function statsFromTests(tests) {
11224
+ return {
11225
+ total: tests.length,
11226
+ passed: tests.filter((t) => t.status === "passed").length,
11227
+ failed: tests.filter((t) => t.status === "failed" || t.status === "timedOut").length,
11228
+ skipped: tests.filter((t) => t.status === "skipped").length,
11229
+ flaky: tests.filter((t) => t.status === "flaky").length
11230
+ };
11231
+ }
11232
+ function aggregateStats(parts) {
11233
+ return parts.reduce(
11234
+ (acc, s) => ({
11235
+ total: acc.total + s.total,
11236
+ passed: acc.passed + s.passed,
11237
+ failed: acc.failed + s.failed,
11238
+ skipped: acc.skipped + s.skipped,
11239
+ flaky: acc.flaky + s.flaky
11240
+ }),
11241
+ { total: 0, passed: 0, failed: 0, skipped: 0, flaky: 0 }
11242
+ );
11243
+ }
11244
+
11245
+ // src/collector/SuiteWalker.ts
10842
11246
  var SuiteWalker = class {
10843
11247
  build(rootSuite, resultMap) {
10844
11248
  return rootSuite.suites.map(
@@ -10849,15 +11253,15 @@ var SuiteWalker = class {
10849
11253
  const suites = projectSuite.suites.map(
10850
11254
  (s) => this.buildSuite(s, resultMap)
10851
11255
  );
10852
- const stats = this.aggregateStats(suites.map((s) => s.stats));
11256
+ const stats = aggregateStats(suites.map((s) => s.stats));
10853
11257
  return { name: projectSuite.title || "default", suites, stats };
10854
11258
  }
10855
11259
  buildSuite(suite, resultMap) {
10856
11260
  const childSuites = suite.suites.map((s) => this.buildSuite(s, resultMap));
10857
11261
  const tests = suite.tests.map((t) => this.buildTest(t, resultMap));
10858
- const stats = this.aggregateStats([
11262
+ const stats = aggregateStats([
10859
11263
  ...childSuites.map((s) => s.stats),
10860
- this.statsFromTests(tests)
11264
+ statsFromTests(tests)
10861
11265
  ]);
10862
11266
  return {
10863
11267
  title: suite.title,
@@ -10894,27 +11298,6 @@ var SuiteWalker = class {
10894
11298
  if (actual === "failed" && expected === "failed") return "passed";
10895
11299
  return "failed";
10896
11300
  }
10897
- statsFromTests(tests) {
10898
- return {
10899
- total: tests.length,
10900
- passed: tests.filter((t) => t.status === "passed").length,
10901
- failed: tests.filter((t) => t.status === "failed" || t.status === "timedOut").length,
10902
- skipped: tests.filter((t) => t.status === "skipped").length,
10903
- flaky: tests.filter((t) => t.status === "flaky").length
10904
- };
10905
- }
10906
- aggregateStats(parts) {
10907
- return parts.reduce(
10908
- (acc, s) => ({
10909
- total: acc.total + s.total,
10910
- passed: acc.passed + s.passed,
10911
- failed: acc.failed + s.failed,
10912
- skipped: acc.skipped + s.skipped,
10913
- flaky: acc.flaky + s.flaky
10914
- }),
10915
- { total: 0, passed: 0, failed: 0, skipped: 0, flaky: 0 }
10916
- );
10917
- }
10918
11301
  };
10919
11302
 
10920
11303
  // src/utils/env.ts
@@ -11250,7 +11633,7 @@ var ShardMerger = class {
11250
11633
  }
11251
11634
  return Array.from(projectMap.entries()).map(([name, projectSuites]) => {
11252
11635
  const suites = this.mergeFileSuites(projectSuites);
11253
- const stats = this.aggregateStats(suites.map((s) => s.stats));
11636
+ const stats = aggregateStats(suites.map((s) => s.stats));
11254
11637
  return { name, suites, stats };
11255
11638
  });
11256
11639
  }
@@ -11271,9 +11654,9 @@ var ShardMerger = class {
11271
11654
  const suitePath = [ref.title ?? ""];
11272
11655
  const tests = allSpecs.map((spec) => this.specToTestNode(spec, projectName, suitePath));
11273
11656
  const childSuiteNodes = this.buildDescribeSuites(allChildSuites, projectName, suitePath);
11274
- const stats = this.aggregateStats([
11657
+ const stats = aggregateStats([
11275
11658
  ...childSuiteNodes.map((s) => s.stats),
11276
- this.statsFromTests(tests)
11659
+ statsFromTests(tests)
11277
11660
  ]);
11278
11661
  return {
11279
11662
  title: ref.title ?? ref.file ?? "unknown",
@@ -11290,9 +11673,9 @@ var ShardMerger = class {
11290
11673
  const suitePath = [...parentPath, suite.title];
11291
11674
  const tests = suite.specs.map((spec) => this.specToTestNode(spec, projectName, suitePath));
11292
11675
  const childSuites = this.buildDescribeSuites(suite.suites ?? [], projectName, suitePath);
11293
- const stats = this.aggregateStats([
11676
+ const stats = aggregateStats([
11294
11677
  ...childSuites.map((s) => s.stats),
11295
- this.statsFromTests(tests)
11678
+ statsFromTests(tests)
11296
11679
  ]);
11297
11680
  return {
11298
11681
  title: suite.title,
@@ -11447,27 +11830,6 @@ var ShardMerger = class {
11447
11830
  suiteResults
11448
11831
  };
11449
11832
  }
11450
- statsFromTests(tests) {
11451
- return {
11452
- total: tests.length,
11453
- passed: tests.filter((t) => t.status === "passed").length,
11454
- failed: tests.filter((t) => t.status === "failed" || t.status === "timedOut").length,
11455
- skipped: tests.filter((t) => t.status === "skipped").length,
11456
- flaky: tests.filter((t) => t.status === "flaky").length
11457
- };
11458
- }
11459
- aggregateStats(parts) {
11460
- return parts.reduce(
11461
- (acc, s) => ({
11462
- total: acc.total + s.total,
11463
- passed: acc.passed + s.passed,
11464
- failed: acc.failed + s.failed,
11465
- skipped: acc.skipped + s.skipped,
11466
- flaky: acc.flaky + s.flaky
11467
- }),
11468
- { total: 0, passed: 0, failed: 0, skipped: 0, flaky: 0 }
11469
- );
11470
- }
11471
11833
  };
11472
11834
 
11473
11835
  // src/pdf/PdfGenerator.ts
@@ -11718,6 +12080,10 @@ var TemplateEngine = class {
11718
12080
  );
11719
12081
  this.handlebars.registerHelper("lt", (a, b) => a < b);
11720
12082
  this.handlebars.registerHelper("or", (a, b) => Boolean(a) || Boolean(b));
12083
+ this.handlebars.registerHelper("categoryLabel", (cat) => CATEGORY_LABEL[cat] ?? cat);
12084
+ this.handlebars.registerHelper("strengthLabel", (s) => s ? s.charAt(0).toUpperCase() + s.slice(1) : "");
12085
+ this.handlebars.registerHelper("pluralise", (n, one, many) => n === 1 ? one : many);
12086
+ this.handlebars.registerHelper("subtract", (a, b) => (a ?? 0) - (b ?? 0));
11721
12087
  this.handlebars.registerHelper("statusBadge", (status) => {
11722
12088
  const map = {
11723
12089
  passed: "badge--passed",
@@ -12137,10 +12503,31 @@ var PdfGenerator = class {
12137
12503
  *
12138
12504
  * @param data - Assembled `ReportData` (projects, stats, failures, charts).
12139
12505
  * @param options - Validated reporter options.
12140
- * @param _licenseInfo - Kept for API symmetry; unused in this method.
12506
+ * @param licenseInfo - Verified license. JWT must be present and valid for
12507
+ * non-demo sources; `source: 'demo'` is the only allowed JWT-free path.
12141
12508
  * @returns Absolute paths of written PDF files.
12142
12509
  */
12143
- async generateAll(data, options, _licenseInfo) {
12510
+ async generateAll(data, options, licenseInfo) {
12511
+ if (false) {
12512
+ if (licenseInfo.expiry <= /* @__PURE__ */ new Date()) {
12513
+ throw new Error("[reportforge] Demo license expired \u2014 PDF generation skipped.");
12514
+ }
12515
+ } else {
12516
+ if (!licenseInfo.jwt) {
12517
+ throw new Error("[reportforge] License JWT missing \u2014 PDF generation skipped.");
12518
+ }
12519
+ if (!LICENSE_PUBLIC_KEY_PEM) {
12520
+ throw new Error("[reportforge] LICENSE_PUBLIC_KEY_PEM missing \u2014 cannot verify license JWT.");
12521
+ }
12522
+ try {
12523
+ const payload = new JwtVerifier(LICENSE_PUBLIC_KEY_PEM).verify(licenseInfo.jwt);
12524
+ if (payload.exp * 1e3 <= Date.now()) {
12525
+ throw new Error("JWT expired");
12526
+ }
12527
+ } catch (err) {
12528
+ throw new Error(`[reportforge] License JWT verification failed: ${err.message}`);
12529
+ }
12530
+ }
12144
12531
  const sources = this.resolveTemplateSources(options);
12145
12532
  const isMulti = sources.length > 1;
12146
12533
  for (const source of sources) {
@@ -12297,8 +12684,6 @@ function fmtMb(bytes) {
12297
12684
 
12298
12685
  // src/reporter.ts
12299
12686
  var import_path12 = __toESM(require("path"));
12300
- var import_crypto7 = __toESM(require("crypto"));
12301
- var import_os6 = __toESM(require("os"));
12302
12687
 
12303
12688
  // src/history/HistoryManager.ts
12304
12689
  init_cjs_shims();
@@ -12670,11 +13055,13 @@ var PdfReporter = class {
12670
13055
  this.dataCollector = new DataCollector();
12671
13056
  this.pdfGenerator = new PdfGenerator();
12672
13057
  this.options = parseOptions(rawOptions);
12673
- const version = true ? "0.2.2" : "0.x";
13058
+ const version = true ? "0.4.0" : "0.x";
12674
13059
  logger.info(`@reportforge/playwright-pdf v${version} initialised`);
12675
13060
  this.licenseClient = new LicenseClient({
12676
13061
  licenseKey: this.options.licenseKey,
12677
- serverUrl: this.options.serverUrl
13062
+ serverUrl: this.options.serverUrl,
13063
+ autoUpdateModel: this.options.failureAnalysis.autoUpdateModel,
13064
+ currentModelVersion: MODEL.version
12678
13065
  });
12679
13066
  }
12680
13067
  onBegin(config, suite) {
@@ -12696,6 +13083,7 @@ var PdfReporter = class {
12696
13083
  }
12697
13084
  }
12698
13085
  const reportData = this._buildReportData(collected, licenseInfo);
13086
+ runFailureAnalysis(reportData, this.options, this.cwd);
12699
13087
  let pdfPaths = [];
12700
13088
  try {
12701
13089
  pdfPaths = await this.pdfGenerator.generateAll(reportData, this.options, licenseInfo);
@@ -12854,8 +13242,7 @@ function resolveHistoryPath(options, cwd) {
12854
13242
  if (options.historyFile) {
12855
13243
  return import_path12.default.isAbsolute(options.historyFile) ? options.historyFile : import_path12.default.resolve(cwd, options.historyFile);
12856
13244
  }
12857
- const projectKey = import_crypto7.default.createHash("sha256").update(cwd + (options.outputFile ?? "")).digest("hex").slice(0, 8);
12858
- return import_path12.default.join(import_os6.default.homedir(), ".reportforge", projectKey, "history.json");
13245
+ return import_path12.default.join(resolveProjectDir(options, cwd), "history.json");
12859
13246
  }
12860
13247
  function flattenSuiteNodes(suites) {
12861
13248
  return suites.flatMap((s) => [s, ...flattenSuiteNodes(s.suites)]);