@reportforge/playwright-pdf 0.3.1 → 0.4.1

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,6 +10837,9 @@ 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",
@@ -10772,13 +10876,15 @@ var LicenseClient = class {
10772
10876
  return this.post("/api/license/activate", {
10773
10877
  licenseKey,
10774
10878
  fingerprint: this.fingerprint.hash,
10775
- label: this.fingerprint.label
10879
+ label: this.fingerprint.label,
10880
+ modelVersion: this.modelVersionForServer
10776
10881
  });
10777
10882
  }
10778
10883
  async refresh(jwt) {
10779
10884
  return this.post("/api/license/refresh", {
10780
10885
  jwt,
10781
- label: this.fingerprint.label
10886
+ label: this.fingerprint.label,
10887
+ modelVersion: this.modelVersionForServer
10782
10888
  });
10783
10889
  }
10784
10890
  async post(route, body) {
@@ -10836,9 +10942,292 @@ function normalizeUrl(url) {
10836
10942
  return url.endsWith("/") ? url.slice(0, -1) : url;
10837
10943
  }
10838
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
+
10839
11215
  // src/collector/DataCollector.ts
10840
11216
  init_cjs_shims();
10841
11217
 
11218
+ // src/utils/strip-ansi.ts
11219
+ init_cjs_shims();
11220
+ var ANSI_PATTERN = new RegExp(
11221
+ [
11222
+ "[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]+)*|[a-zA-Z\\d]+(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)",
11223
+ "(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-ntqry=><~]))"
11224
+ ].join("|"),
11225
+ "g"
11226
+ );
11227
+ function stripAnsi(input) {
11228
+ return input.replace(ANSI_PATTERN, "");
11229
+ }
11230
+
10842
11231
  // src/collector/SuiteWalker.ts
10843
11232
  init_cjs_shims();
10844
11233
 
@@ -11073,8 +11462,8 @@ var DataCollector = class {
11073
11462
  testTitle: test.title,
11074
11463
  suitePath,
11075
11464
  error: {
11076
- message: error.message ?? String(error),
11077
- stack: error.stack
11465
+ message: stripAnsi(error.message ?? String(error)),
11466
+ stack: error.stack ? stripAnsi(error.stack) : void 0
11078
11467
  },
11079
11468
  screenshotPath: screenshotAttachment?.path,
11080
11469
  screenshotBuffer: screenshotAttachment?.body,
@@ -11396,8 +11785,8 @@ var ShardMerger = class {
11396
11785
  testTitle: spec.title,
11397
11786
  suitePath,
11398
11787
  error: {
11399
- message: error?.message ?? "Test failed",
11400
- stack: error?.stack
11788
+ message: stripAnsi(error?.message ?? "Test failed"),
11789
+ stack: error?.stack ? stripAnsi(error.stack) : void 0
11401
11790
  },
11402
11791
  screenshotPath: lastResult?.attachments.find(
11403
11792
  (a) => a.name.toLowerCase().includes("screenshot") && a.contentType.startsWith("image/")
@@ -11704,6 +12093,10 @@ var TemplateEngine = class {
11704
12093
  );
11705
12094
  this.handlebars.registerHelper("lt", (a, b) => a < b);
11706
12095
  this.handlebars.registerHelper("or", (a, b) => Boolean(a) || Boolean(b));
12096
+ this.handlebars.registerHelper("categoryLabel", (cat) => CATEGORY_LABEL[cat] ?? cat);
12097
+ this.handlebars.registerHelper("strengthLabel", (s) => s ? s.charAt(0).toUpperCase() + s.slice(1) : "");
12098
+ this.handlebars.registerHelper("pluralise", (n, one, many) => n === 1 ? one : many);
12099
+ this.handlebars.registerHelper("subtract", (a, b) => (a ?? 0) - (b ?? 0));
11707
12100
  this.handlebars.registerHelper("statusBadge", (status) => {
11708
12101
  const map = {
11709
12102
  passed: "badge--passed",
@@ -12304,8 +12697,6 @@ function fmtMb(bytes) {
12304
12697
 
12305
12698
  // src/reporter.ts
12306
12699
  var import_path12 = __toESM(require("path"));
12307
- var import_crypto7 = __toESM(require("crypto"));
12308
- var import_os6 = __toESM(require("os"));
12309
12700
 
12310
12701
  // src/history/HistoryManager.ts
12311
12702
  init_cjs_shims();
@@ -12677,11 +13068,13 @@ var PdfReporter = class {
12677
13068
  this.dataCollector = new DataCollector();
12678
13069
  this.pdfGenerator = new PdfGenerator();
12679
13070
  this.options = parseOptions(rawOptions);
12680
- const version = true ? "0.3.1" : "0.x";
13071
+ const version = true ? "0.4.1" : "0.x";
12681
13072
  logger.info(`@reportforge/playwright-pdf v${version} initialised`);
12682
13073
  this.licenseClient = new LicenseClient({
12683
13074
  licenseKey: this.options.licenseKey,
12684
- serverUrl: this.options.serverUrl
13075
+ serverUrl: this.options.serverUrl,
13076
+ autoUpdateModel: this.options.failureAnalysis.autoUpdateModel,
13077
+ currentModelVersion: MODEL.version
12685
13078
  });
12686
13079
  }
12687
13080
  onBegin(config, suite) {
@@ -12703,6 +13096,7 @@ var PdfReporter = class {
12703
13096
  }
12704
13097
  }
12705
13098
  const reportData = this._buildReportData(collected, licenseInfo);
13099
+ runFailureAnalysis(reportData, this.options, this.cwd);
12706
13100
  let pdfPaths = [];
12707
13101
  try {
12708
13102
  pdfPaths = await this.pdfGenerator.generateAll(reportData, this.options, licenseInfo);
@@ -12861,8 +13255,7 @@ function resolveHistoryPath(options, cwd) {
12861
13255
  if (options.historyFile) {
12862
13256
  return import_path12.default.isAbsolute(options.historyFile) ? options.historyFile : import_path12.default.resolve(cwd, options.historyFile);
12863
13257
  }
12864
- const projectKey = import_crypto7.default.createHash("sha256").update(cwd + (options.outputFile ?? "")).digest("hex").slice(0, 8);
12865
- return import_path12.default.join(import_os6.default.homedir(), ".reportforge", projectKey, "history.json");
13258
+ return import_path12.default.join(resolveProjectDir(options, cwd), "history.json");
12866
13259
  }
12867
13260
  function flattenSuiteNodes(suites) {
12868
13261
  return suites.flatMap((s) => [s, ...flattenSuiteNodes(s.suites)]);