@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.mjs CHANGED
@@ -1229,13 +1229,13 @@ var require_ast = __commonJS({
1229
1229
  helperExpression: function helperExpression(node) {
1230
1230
  return node.type === "SubExpression" || (node.type === "MustacheStatement" || node.type === "BlockStatement") && !!(node.params && node.params.length || node.hash);
1231
1231
  },
1232
- scopedId: function scopedId(path12) {
1233
- return /^\.|this\b/.test(path12.original);
1232
+ scopedId: function scopedId(path15) {
1233
+ return /^\.|this\b/.test(path15.original);
1234
1234
  },
1235
1235
  // an ID is simple if it only has one part, and that part is not
1236
1236
  // `..` or `this`.
1237
- simpleId: function simpleId(path12) {
1238
- return path12.parts.length === 1 && !AST.helpers.scopedId(path12) && !path12.depth;
1237
+ simpleId: function simpleId(path15) {
1238
+ return path15.parts.length === 1 && !AST.helpers.scopedId(path15) && !path15.depth;
1239
1239
  }
1240
1240
  }
1241
1241
  };
@@ -2309,12 +2309,12 @@ var require_helpers2 = __commonJS({
2309
2309
  loc
2310
2310
  };
2311
2311
  }
2312
- function prepareMustache(path12, params, hash, open, strip, locInfo) {
2312
+ function prepareMustache(path15, params, hash, open, strip, locInfo) {
2313
2313
  var escapeFlag = open.charAt(3) || open.charAt(2), escaped = escapeFlag !== "{" && escapeFlag !== "&";
2314
2314
  var decorator = /\*/.test(open);
2315
2315
  return {
2316
2316
  type: decorator ? "Decorator" : "MustacheStatement",
2317
- path: path12,
2317
+ path: path15,
2318
2318
  params,
2319
2319
  hash,
2320
2320
  escaped,
@@ -2634,9 +2634,9 @@ var require_compiler = __commonJS({
2634
2634
  },
2635
2635
  DecoratorBlock: function DecoratorBlock(decorator) {
2636
2636
  var program = decorator.program && this.compileProgram(decorator.program);
2637
- var params = this.setupFullMustacheParams(decorator, program, void 0), path12 = decorator.path;
2637
+ var params = this.setupFullMustacheParams(decorator, program, void 0), path15 = decorator.path;
2638
2638
  this.useDecorators = true;
2639
- this.opcode("registerDecorator", params.length, path12.original);
2639
+ this.opcode("registerDecorator", params.length, path15.original);
2640
2640
  },
2641
2641
  PartialStatement: function PartialStatement(partial) {
2642
2642
  this.usePartial = true;
@@ -2700,46 +2700,46 @@ var require_compiler = __commonJS({
2700
2700
  }
2701
2701
  },
2702
2702
  ambiguousSexpr: function ambiguousSexpr(sexpr, program, inverse) {
2703
- var path12 = sexpr.path, name = path12.parts[0], isBlock = program != null || inverse != null;
2704
- this.opcode("getContext", path12.depth);
2703
+ var path15 = sexpr.path, name = path15.parts[0], isBlock = program != null || inverse != null;
2704
+ this.opcode("getContext", path15.depth);
2705
2705
  this.opcode("pushProgram", program);
2706
2706
  this.opcode("pushProgram", inverse);
2707
- path12.strict = true;
2708
- this.accept(path12);
2707
+ path15.strict = true;
2708
+ this.accept(path15);
2709
2709
  this.opcode("invokeAmbiguous", name, isBlock);
2710
2710
  },
2711
2711
  simpleSexpr: function simpleSexpr(sexpr) {
2712
- var path12 = sexpr.path;
2713
- path12.strict = true;
2714
- this.accept(path12);
2712
+ var path15 = sexpr.path;
2713
+ path15.strict = true;
2714
+ this.accept(path15);
2715
2715
  this.opcode("resolvePossibleLambda");
2716
2716
  },
2717
2717
  helperSexpr: function helperSexpr(sexpr, program, inverse) {
2718
- var params = this.setupFullMustacheParams(sexpr, program, inverse), path12 = sexpr.path, name = path12.parts[0];
2718
+ var params = this.setupFullMustacheParams(sexpr, program, inverse), path15 = sexpr.path, name = path15.parts[0];
2719
2719
  if (this.options.knownHelpers[name]) {
2720
2720
  this.opcode("invokeKnownHelper", params.length, name);
2721
2721
  } else if (this.options.knownHelpersOnly) {
2722
2722
  throw new _exception2["default"]("You specified knownHelpersOnly, but used the unknown helper " + name, sexpr);
2723
2723
  } else {
2724
- path12.strict = true;
2725
- path12.falsy = true;
2726
- this.accept(path12);
2727
- this.opcode("invokeHelper", params.length, path12.original, _ast2["default"].helpers.simpleId(path12));
2724
+ path15.strict = true;
2725
+ path15.falsy = true;
2726
+ this.accept(path15);
2727
+ this.opcode("invokeHelper", params.length, path15.original, _ast2["default"].helpers.simpleId(path15));
2728
2728
  }
2729
2729
  },
2730
- PathExpression: function PathExpression(path12) {
2731
- this.addDepth(path12.depth);
2732
- this.opcode("getContext", path12.depth);
2733
- var name = path12.parts[0], scoped = _ast2["default"].helpers.scopedId(path12), blockParamId = !path12.depth && !scoped && this.blockParamIndex(name);
2730
+ PathExpression: function PathExpression(path15) {
2731
+ this.addDepth(path15.depth);
2732
+ this.opcode("getContext", path15.depth);
2733
+ var name = path15.parts[0], scoped = _ast2["default"].helpers.scopedId(path15), blockParamId = !path15.depth && !scoped && this.blockParamIndex(name);
2734
2734
  if (blockParamId) {
2735
- this.opcode("lookupBlockParam", blockParamId, path12.parts);
2735
+ this.opcode("lookupBlockParam", blockParamId, path15.parts);
2736
2736
  } else if (!name) {
2737
2737
  this.opcode("pushContext");
2738
- } else if (path12.data) {
2738
+ } else if (path15.data) {
2739
2739
  this.options.data = true;
2740
- this.opcode("lookupData", path12.depth, path12.parts, path12.strict);
2740
+ this.opcode("lookupData", path15.depth, path15.parts, path15.strict);
2741
2741
  } else {
2742
- this.opcode("lookupOnContext", path12.parts, path12.falsy, path12.strict, scoped);
2742
+ this.opcode("lookupOnContext", path15.parts, path15.falsy, path15.strict, scoped);
2743
2743
  }
2744
2744
  },
2745
2745
  StringLiteral: function StringLiteral(string) {
@@ -3095,16 +3095,16 @@ var require_util = __commonJS({
3095
3095
  }
3096
3096
  exports.urlGenerate = urlGenerate;
3097
3097
  function normalize(aPath) {
3098
- var path12 = aPath;
3098
+ var path15 = aPath;
3099
3099
  var url = urlParse(aPath);
3100
3100
  if (url) {
3101
3101
  if (!url.path) {
3102
3102
  return aPath;
3103
3103
  }
3104
- path12 = url.path;
3104
+ path15 = url.path;
3105
3105
  }
3106
- var isAbsolute = exports.isAbsolute(path12);
3107
- var parts = path12.split(/\/+/);
3106
+ var isAbsolute = exports.isAbsolute(path15);
3107
+ var parts = path15.split(/\/+/);
3108
3108
  for (var part, up = 0, i = parts.length - 1; i >= 0; i--) {
3109
3109
  part = parts[i];
3110
3110
  if (part === ".") {
@@ -3121,15 +3121,15 @@ var require_util = __commonJS({
3121
3121
  }
3122
3122
  }
3123
3123
  }
3124
- path12 = parts.join("/");
3125
- if (path12 === "") {
3126
- path12 = isAbsolute ? "/" : ".";
3124
+ path15 = parts.join("/");
3125
+ if (path15 === "") {
3126
+ path15 = isAbsolute ? "/" : ".";
3127
3127
  }
3128
3128
  if (url) {
3129
- url.path = path12;
3129
+ url.path = path15;
3130
3130
  return urlGenerate(url);
3131
3131
  }
3132
- return path12;
3132
+ return path15;
3133
3133
  }
3134
3134
  exports.normalize = normalize;
3135
3135
  function join(aRoot, aPath) {
@@ -5932,8 +5932,8 @@ var require_printer = __commonJS({
5932
5932
  return this.accept(sexpr.path) + " " + params + hash;
5933
5933
  };
5934
5934
  PrintVisitor.prototype.PathExpression = function(id) {
5935
- var path12 = id.parts.join("/");
5936
- return (id.data ? "@" : "") + "PATH:" + path12;
5935
+ var path15 = id.parts.join("/");
5936
+ return (id.data ? "@" : "") + "PATH:" + path15;
5937
5937
  };
5938
5938
  PrintVisitor.prototype.StringLiteral = function(string) {
5939
5939
  return '"' + string.value + '"';
@@ -5974,8 +5974,8 @@ var require_lib = __commonJS({
5974
5974
  handlebars.print = printer.print;
5975
5975
  module.exports = handlebars;
5976
5976
  function extension(module2, filename) {
5977
- var fs12 = __require("fs");
5978
- var templateString = fs12.readFileSync(filename, "utf8");
5977
+ var fs14 = __require("fs");
5978
+ var templateString = fs14.readFileSync(filename, "utf8");
5979
5979
  module2.exports = handlebars.compile(templateString);
5980
5980
  }
5981
5981
  if (typeof __require !== "undefined" && __require.extensions) {
@@ -6004,7 +6004,7 @@ var require_util2 = __commonJS({
6004
6004
  "node_modules/chrome-finder/lib/util.js"(exports, module) {
6005
6005
  "use strict";
6006
6006
  init_esm_shims();
6007
- var fs12 = __require("fs");
6007
+ var fs14 = __require("fs");
6008
6008
  var newLineRegex = /\r?\n/;
6009
6009
  function sort(installations, priorities) {
6010
6010
  const defaultPriority = 10;
@@ -6022,7 +6022,7 @@ var require_util2 = __commonJS({
6022
6022
  return false;
6023
6023
  }
6024
6024
  try {
6025
- fs12.accessSync(file);
6025
+ fs14.accessSync(file);
6026
6026
  return true;
6027
6027
  } catch (e) {
6028
6028
  return false;
@@ -6033,7 +6033,7 @@ var require_util2 = __commonJS({
6033
6033
  return false;
6034
6034
  }
6035
6035
  try {
6036
- var stat = fs12.statSync(file);
6036
+ var stat = fs14.statSync(file);
6037
6037
  return stat && typeof stat.isFile === "function" && stat.isFile();
6038
6038
  } catch (e) {
6039
6039
  return false;
@@ -6053,7 +6053,7 @@ var require_win32 = __commonJS({
6053
6053
  "node_modules/chrome-finder/lib/win32.js"(exports, module) {
6054
6054
  "use strict";
6055
6055
  init_esm_shims();
6056
- var path12 = __require("path");
6056
+ var path15 = __require("path");
6057
6057
  var { canAccess } = require_util2();
6058
6058
  function win32() {
6059
6059
  const installations = [];
@@ -6067,7 +6067,7 @@ var require_win32 = __commonJS({
6067
6067
  const prefixes = [process.env.LOCALAPPDATA, process.env.PROGRAMFILES, process.env["PROGRAMFILES(X86)"]];
6068
6068
  prefixes.forEach((prefix) => suffixes.forEach((suffix) => {
6069
6069
  if (prefix) {
6070
- const chromePath = path12.join(prefix, suffix);
6070
+ const chromePath = path15.join(prefix, suffix);
6071
6071
  if (canAccess(chromePath)) {
6072
6072
  installations.push(chromePath);
6073
6073
  }
@@ -6085,7 +6085,7 @@ var require_darwin = __commonJS({
6085
6085
  "use strict";
6086
6086
  init_esm_shims();
6087
6087
  var { execSync: execSync3 } = __require("child_process");
6088
- var path12 = __require("path");
6088
+ var path15 = __require("path");
6089
6089
  var { canAccess, newLineRegex, sort } = require_util2();
6090
6090
  function darwin() {
6091
6091
  const suffixes = ["/Contents/MacOS/Google Chrome Canary", "/Contents/MacOS/Google Chrome", "/Contents/MacOS/Chromium"];
@@ -6095,7 +6095,7 @@ var require_darwin = __commonJS({
6095
6095
  ${LSREGISTER} -dump | grep -E -i -o '/.+(google chrome( canary)?|chromium)\\.app(\\s|$)' | grep -E -v 'Caches|TimeMachine|Temporary|/Volumes|\\.Trash'
6096
6096
  `).toString().split(newLineRegex).forEach((inst) => {
6097
6097
  suffixes.forEach((suffix) => {
6098
- const execPath = path12.join(inst.trim(), suffix);
6098
+ const execPath = path15.join(inst.trim(), suffix);
6099
6099
  if (canAccess(execPath)) {
6100
6100
  installations.push(execPath);
6101
6101
  }
@@ -6124,8 +6124,8 @@ var require_linux = __commonJS({
6124
6124
  "use strict";
6125
6125
  init_esm_shims();
6126
6126
  var { execSync: execSync3, execFileSync: execFileSync2 } = __require("child_process");
6127
- var path12 = __require("path");
6128
- var fs12 = __require("fs");
6127
+ var path15 = __require("path");
6128
+ var fs14 = __require("fs");
6129
6129
  var { canAccess, sort, isExecutable, newLineRegex } = require_util2();
6130
6130
  function findChromeExecutablesForLinuxDesktop(folder) {
6131
6131
  const argumentsRegex = /(^[^ ]+).*/;
@@ -6142,7 +6142,7 @@ var require_linux = __commonJS({
6142
6142
  function linux() {
6143
6143
  let installations = [];
6144
6144
  const desktopInstallationFolders = [
6145
- path12.join(__require("os").homedir(), ".local/share/applications/"),
6145
+ path15.join(__require("os").homedir(), ".local/share/applications/"),
6146
6146
  "/usr/share/applications/"
6147
6147
  ];
6148
6148
  desktopInstallationFolders.forEach((folder) => {
@@ -6168,7 +6168,7 @@ var require_linux = __commonJS({
6168
6168
  ].map((possiblePath) => {
6169
6169
  try {
6170
6170
  const chromePathToTest = possiblePath + "/" + executable;
6171
- if (fs12.existsSync(chromePathToTest) && canAccess(chromePathToTest) && isExecutable(chromePathToTest)) {
6171
+ if (fs14.existsSync(chromePathToTest) && canAccess(chromePathToTest) && isExecutable(chromePathToTest)) {
6172
6172
  installations.push(chromePathToTest);
6173
6173
  return chromePathToTest;
6174
6174
  }
@@ -6750,8 +6750,8 @@ function getErrorMap() {
6750
6750
  // node_modules/zod/v3/helpers/parseUtil.js
6751
6751
  init_esm_shims();
6752
6752
  var makeIssue = (params) => {
6753
- const { data, path: path12, errorMaps, issueData } = params;
6754
- const fullPath = [...path12, ...issueData.path || []];
6753
+ const { data, path: path15, errorMaps, issueData } = params;
6754
+ const fullPath = [...path15, ...issueData.path || []];
6755
6755
  const fullIssue = {
6756
6756
  ...issueData,
6757
6757
  path: fullPath
@@ -6871,11 +6871,11 @@ var errorUtil;
6871
6871
 
6872
6872
  // node_modules/zod/v3/types.js
6873
6873
  var ParseInputLazyPath = class {
6874
- constructor(parent, value, path12, key) {
6874
+ constructor(parent, value, path15, key) {
6875
6875
  this._cachedPath = [];
6876
6876
  this.parent = parent;
6877
6877
  this.data = value;
6878
- this._path = path12;
6878
+ this._path = path15;
6879
6879
  this._key = key;
6880
6880
  }
6881
6881
  get path() {
@@ -10355,6 +10355,16 @@ var notifyConfigSchema = external_exports.object({
10355
10355
  discord: channelConfigSchema.optional(),
10356
10356
  email: emailChannelConfigSchema.optional()
10357
10357
  }).optional();
10358
+ var failureAnalysisConfigSchema = external_exports.object({
10359
+ enabled: external_exports.boolean().default(true),
10360
+ maxClusters: external_exports.number().int().min(1).max(50).default(10),
10361
+ // Qualitative floor (not a numeric % — consistent with the match-strength design).
10362
+ minStrength: external_exports.enum(["weak", "moderate", "strong"]).default("weak"),
10363
+ maxFailuresToAnalyse: external_exports.number().int().min(1).default(500),
10364
+ // Consumed by the Plan 3 feedback collector; inert until then.
10365
+ collectUnclassified: external_exports.boolean().default(true),
10366
+ autoUpdateModel: external_exports.boolean().default(true)
10367
+ }).default({});
10358
10368
  var ReporterOptionsSchema = external_exports.object({
10359
10369
  outputFile: external_exports.string().optional().default(DEFAULT_OPTIONS.outputFile),
10360
10370
  template: external_exports.union([
@@ -10384,6 +10394,7 @@ var ReporterOptionsSchema = external_exports.object({
10384
10394
  historySize: external_exports.number().int().min(2).optional().default(DEFAULT_OPTIONS.historySize),
10385
10395
  showTrend: external_exports.boolean().optional().default(DEFAULT_OPTIONS.showTrend),
10386
10396
  flakinessTopN: external_exports.number().int().min(0, "flakinessTopN must be 0 or greater").optional().default(DEFAULT_OPTIONS.flakinessTopN),
10397
+ failureAnalysis: failureAnalysisConfigSchema,
10387
10398
  templatePath: external_exports.union([
10388
10399
  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"),
10389
10400
  external_exports.array(
@@ -10623,6 +10634,17 @@ function clearCache() {
10623
10634
  init_esm_shims();
10624
10635
  var LICENSE_PUBLIC_KEY_PEM = "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEAbvknstk0gxoJ7UwATdlxR8btxvO7u7RdpO1uV90PtuE=\n-----END PUBLIC KEY-----";
10625
10636
 
10637
+ // src/analysis/ModelStore.ts
10638
+ init_esm_shims();
10639
+ import fs3 from "fs";
10640
+ import os4 from "os";
10641
+ import path4 from "path";
10642
+ import { createPublicKey as createPublicKey2, verify as verify2 } from "crypto";
10643
+
10644
+ // src/analysis/model-bundle.ts
10645
+ init_esm_shims();
10646
+ 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 };
10647
+
10626
10648
  // src/utils/logger.ts
10627
10649
  init_esm_shims();
10628
10650
  var PREFIX = "[reportforge]";
@@ -10644,6 +10666,83 @@ ${JSON.stringify(data, null, 2)}` : "";
10644
10666
  }
10645
10667
  };
10646
10668
 
10669
+ // src/analysis/ModelStore.ts
10670
+ var MODEL_SIG_DOMAIN = "reportforge-model-sig:v1\n";
10671
+ var testDeps = null;
10672
+ function cacheFile() {
10673
+ return testDeps?.cacheFile ?? path4.join(os4.homedir(), ".reportforge", "model.json");
10674
+ }
10675
+ function publicKey() {
10676
+ const pem = testDeps?.publicKeyPem ?? LICENSE_PUBLIC_KEY_PEM;
10677
+ try {
10678
+ return pem && pem.includes("PUBLIC KEY") ? createPublicKey2(pem) : null;
10679
+ } catch {
10680
+ return null;
10681
+ }
10682
+ }
10683
+ function verifyModelSignature(modelJson, signatureB64url) {
10684
+ const key = publicKey();
10685
+ if (!key) return false;
10686
+ try {
10687
+ return verify2(null, Buffer.from(MODEL_SIG_DOMAIN + modelJson), key, Buffer.from(signatureB64url, "base64url"));
10688
+ } catch {
10689
+ return false;
10690
+ }
10691
+ }
10692
+ function schemaValid(m) {
10693
+ const x = m;
10694
+ 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";
10695
+ }
10696
+ function readCache2() {
10697
+ try {
10698
+ const raw = fs3.readFileSync(cacheFile(), "utf8");
10699
+ const c = JSON.parse(raw);
10700
+ if (typeof c.version === "number" && typeof c.modelJson === "string" && typeof c.signature === "string") {
10701
+ return c;
10702
+ }
10703
+ } catch {
10704
+ }
10705
+ return null;
10706
+ }
10707
+ function loadModel(autoUpdate) {
10708
+ if (!autoUpdate) return MODEL;
10709
+ const c = readCache2();
10710
+ if (!c || c.version <= MODEL.version) return MODEL;
10711
+ if (!verifyModelSignature(c.modelJson, c.signature)) {
10712
+ logger.debug(`cached model v${c.version} signature invalid \u2014 using bundled (possible tampering or key mismatch)`);
10713
+ return MODEL;
10714
+ }
10715
+ try {
10716
+ const m = JSON.parse(c.modelJson);
10717
+ if (schemaValid(m)) return m;
10718
+ logger.debug(`cached model v${c.version} failed schema validation \u2014 using bundled`);
10719
+ } catch {
10720
+ logger.debug(`cached model v${c.version} JSON parse failed \u2014 using bundled`);
10721
+ }
10722
+ return MODEL;
10723
+ }
10724
+ function acceptDeliveredModel(d) {
10725
+ try {
10726
+ const current = readCache2()?.version ?? MODEL.version;
10727
+ if (d.version <= current) return;
10728
+ if (!verifyModelSignature(d.modelJson, d.signature)) {
10729
+ logger.debug(`delivered model v${d.version} signature invalid \u2014 rejected`);
10730
+ return;
10731
+ }
10732
+ const m = JSON.parse(d.modelJson);
10733
+ if (!schemaValid(m)) {
10734
+ logger.debug(`delivered model v${d.version} failed schema validation \u2014 rejected`);
10735
+ return;
10736
+ }
10737
+ const file = cacheFile();
10738
+ fs3.mkdirSync(path4.dirname(file), { recursive: true });
10739
+ const tmp = `${file}.${process.pid}.tmp`;
10740
+ fs3.writeFileSync(tmp, JSON.stringify({ version: d.version, modelJson: d.modelJson, signature: d.signature }), { mode: 384 });
10741
+ fs3.renameSync(tmp, file);
10742
+ } catch {
10743
+ }
10744
+ }
10745
+
10647
10746
  // src/license/LicenseClient.ts
10648
10747
  var DEFAULT_SERVER_URL = "https://reportforge.org";
10649
10748
  var REFRESH_THRESHOLD_MS = 24 * 60 * 60 * 1e3;
@@ -10657,6 +10756,8 @@ var LicenseClient = class {
10657
10756
  this.serverUrl = normalizeUrl(
10658
10757
  opts.serverUrl ?? process.env.REPORTFORGE_SERVER_URL ?? DEFAULT_SERVER_URL
10659
10758
  );
10759
+ this.autoUpdateModel = opts.autoUpdateModel ?? true;
10760
+ this.modelVersionForServer = this.autoUpdateModel ? opts.currentModelVersion ?? 0 : Number.MAX_SAFE_INTEGER;
10660
10761
  }
10661
10762
  async resolve() {
10662
10763
  const key = this.rawKey || process.env.RF_LICENSE_KEY?.trim();
@@ -10737,10 +10838,14 @@ var LicenseClient = class {
10737
10838
  machinesUsed: result.machinesUsed,
10738
10839
  machineLimit: result.machineLimit
10739
10840
  });
10841
+ if (this.autoUpdateModel && result.model) {
10842
+ acceptDeliveredModel(result.model);
10843
+ }
10740
10844
  return {
10741
10845
  plan: "subscription",
10742
10846
  source: "jwt-online",
10743
10847
  key: normalizedKey,
10848
+ jwt: result.jwt,
10744
10849
  expiry: new Date(payload.exp * 1e3),
10745
10850
  machinesUsed: result.machinesUsed,
10746
10851
  machineLimit: result.machineLimit
@@ -10772,13 +10877,15 @@ var LicenseClient = class {
10772
10877
  return this.post("/api/license/activate", {
10773
10878
  licenseKey,
10774
10879
  fingerprint: this.fingerprint.hash,
10775
- label: this.fingerprint.label
10880
+ label: this.fingerprint.label,
10881
+ modelVersion: this.modelVersionForServer
10776
10882
  });
10777
10883
  }
10778
10884
  async refresh(jwt) {
10779
10885
  return this.post("/api/license/refresh", {
10780
10886
  jwt,
10781
- label: this.fingerprint.label
10887
+ label: this.fingerprint.label,
10888
+ modelVersion: this.modelVersionForServer
10782
10889
  });
10783
10890
  }
10784
10891
  async post(route, body) {
@@ -10823,6 +10930,7 @@ function toLicenseInfo(payload, cached, normalizedKey, source) {
10823
10930
  plan: "subscription",
10824
10931
  source,
10825
10932
  key: normalizedKey,
10933
+ jwt: cached.jwt,
10826
10934
  expiry: new Date(payload.exp * 1e3),
10827
10935
  // Surface the snapshot from the last activate/refresh. Undefined for
10828
10936
  // caches written by older reporter versions before these fields were
@@ -10835,11 +10943,307 @@ function normalizeUrl(url) {
10835
10943
  return url.endsWith("/") ? url.slice(0, -1) : url;
10836
10944
  }
10837
10945
 
10946
+ // src/analysis/index.ts
10947
+ init_esm_shims();
10948
+
10949
+ // src/analysis/FailureClusterer.ts
10950
+ init_esm_shims();
10951
+
10952
+ // src/analysis/NaiveBayesClassifier.ts
10953
+ init_esm_shims();
10954
+
10955
+ // src/analysis/tokenize.ts
10956
+ init_esm_shims();
10957
+ function tokenize(text) {
10958
+ 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);
10959
+ }
10960
+
10961
+ // src/analysis/NaiveBayesClassifier.ts
10962
+ function classify(model, text) {
10963
+ const tokens = tokenize(text);
10964
+ const vocabTokens = tokens.filter(
10965
+ (tok) => model.labels.some((l) => model.logLikelihoods[l]?.[tok] !== void 0)
10966
+ );
10967
+ if (vocabTokens.length === 0) {
10968
+ return { label: "unknown", margin: 0 };
10969
+ }
10970
+ const scores = {};
10971
+ for (const label of model.labels) {
10972
+ let s = model.priors[label];
10973
+ const oov = Math.log(model.alpha / (model.totals[label] + model.alpha * model.vocabSize));
10974
+ const ll = model.logLikelihoods[label];
10975
+ for (const tok of vocabTokens) {
10976
+ s += ll[tok] ?? oov;
10977
+ }
10978
+ scores[label] = s;
10979
+ }
10980
+ const ranked = Object.entries(scores).sort((a, b) => b[1] - a[1]);
10981
+ const topLog = ranked[0][1];
10982
+ const secondLog = ranked[1]?.[1] ?? topLog;
10983
+ const margin = (topLog - secondLog) / vocabTokens.length;
10984
+ return { label: ranked[0][0], margin };
10985
+ }
10986
+
10987
+ // src/analysis/constants.ts
10988
+ init_esm_shims();
10989
+ var STRENGTH = {
10990
+ unknownMaxMargin: 0.05,
10991
+ collectMaxMargin: 0.1,
10992
+ harvestMinMargin: 0.25,
10993
+ moderateMinMargin: 0.1,
10994
+ strongMinMargin: 0.25
10995
+ };
10996
+ var STRENGTH_LEVELS = ["weak", "moderate", "strong"];
10997
+ var FLAKY_MARGIN = 999;
10998
+
10999
+ // src/analysis/FailureClusterer.ts
11000
+ var MAX_TESTS_PER_CLUSTER = 20;
11001
+ var MAX_MESSAGE_CHARS = 200;
11002
+ function strengthOf(margin) {
11003
+ if (margin >= STRENGTH.strongMinMargin) return "strong";
11004
+ if (margin >= STRENGTH.moderateMinMargin) return "moderate";
11005
+ return "weak";
11006
+ }
11007
+ function classifyInput(f) {
11008
+ const head = (f.error.stack ?? "").split("\n").slice(0, 3).join(" ");
11009
+ return `${f.error.message} ${head}`;
11010
+ }
11011
+ function topFrame(stack) {
11012
+ if (!stack) return void 0;
11013
+ for (const line of stack.split("\n")) {
11014
+ const t = line.trim();
11015
+ if (t.startsWith("at ") && !t.replace(/\\/g, "/").includes("node_modules/playwright")) return t;
11016
+ }
11017
+ return void 0;
11018
+ }
11019
+ function representativeMessage(message) {
11020
+ const firstLine = message.split("\n")[0];
11021
+ return firstLine.length > MAX_MESSAGE_CHARS ? `${firstLine.slice(0, MAX_MESSAGE_CHARS)}\u2026` : firstLine;
11022
+ }
11023
+ function classifyOne(f, model) {
11024
+ if (f.retryHistory.some((r) => r.status === "passed")) {
11025
+ return { category: "flaky-by-retry", margin: FLAKY_MARGIN, failure: f, frame: topFrame(f.error.stack) };
11026
+ }
11027
+ const { label, margin } = classify(model, classifyInput(f));
11028
+ return { category: label, margin, failure: f, frame: topFrame(f.error.stack) };
11029
+ }
11030
+ function classifyAll(failures, model) {
11031
+ return failures.map((f) => classifyOne(f, model));
11032
+ }
11033
+ function clusterClassified(classified) {
11034
+ const groups = /* @__PURE__ */ new Map();
11035
+ for (const c of classified) {
11036
+ const key = `${c.category}::${c.frame ?? "__no-stack__"}`;
11037
+ const bucket = groups.get(key);
11038
+ if (bucket) bucket.push(c);
11039
+ else groups.set(key, [c]);
11040
+ }
11041
+ const clusters = [];
11042
+ for (const members of groups.values()) {
11043
+ const margin = Math.max(...members.map((m) => m.margin));
11044
+ clusters.push({
11045
+ category: members[0].category,
11046
+ strength: strengthOf(margin),
11047
+ margin,
11048
+ count: members.length,
11049
+ tests: members.slice(0, MAX_TESTS_PER_CLUSTER).map((m) => m.failure.testTitle),
11050
+ testsTotal: members.length,
11051
+ representativeMessage: representativeMessage(members[0].failure.error.message),
11052
+ stackFrame: members[0].frame
11053
+ });
11054
+ }
11055
+ return clusters;
11056
+ }
11057
+
11058
+ // src/analysis/SummaryWriter.ts
11059
+ init_esm_shims();
11060
+ var CATEGORY_LABEL = {
11061
+ assertion: "Assertion failure",
11062
+ timeout: "Timeout",
11063
+ "locator-not-found": "Element not found",
11064
+ network: "Network error",
11065
+ navigation: "Navigation error",
11066
+ "env-config": "Environment / config",
11067
+ "flaky-by-retry": "Flaky (passed on retry)",
11068
+ unknown: "Unclassified"
11069
+ };
11070
+ var SHORT_LABEL = {
11071
+ assertion: "assertion",
11072
+ timeout: "timeout",
11073
+ "locator-not-found": "element not found",
11074
+ network: "network",
11075
+ navigation: "navigation",
11076
+ "env-config": "env/config",
11077
+ "flaky-by-retry": "flaky",
11078
+ unknown: "unclassified"
11079
+ };
11080
+ function buildOneLiner(clusters) {
11081
+ if (clusters.length === 0) return "";
11082
+ const byCategory = /* @__PURE__ */ new Map();
11083
+ for (const c of clusters) {
11084
+ byCategory.set(c.category, (byCategory.get(c.category) ?? 0) + c.count);
11085
+ }
11086
+ const head = `${clusters.length} root ${clusters.length === 1 ? "cause" : "causes"}`;
11087
+ const tally = [...byCategory.entries()].sort((a, b) => b[1] - a[1]).map(([cat, count]) => `${count} ${SHORT_LABEL[cat]}`);
11088
+ return [head, ...tally].join(" \xB7 ");
11089
+ }
11090
+ function dominantCategory(clusters) {
11091
+ return clusters[0]?.category ?? "unknown";
11092
+ }
11093
+
11094
+ // src/analysis/UnclassifiedCollector.ts
11095
+ init_esm_shims();
11096
+ import fs4 from "fs";
11097
+ import os5 from "os";
11098
+ import path5 from "path";
11099
+
11100
+ // src/analysis/feedback-csv.ts
11101
+ init_esm_shims();
11102
+ var FEEDBACK_HEADER = "category,margin,text";
11103
+ function parseFeedbackRows(content) {
11104
+ return content.trim().split("\n").slice(1).filter(Boolean).map((line) => {
11105
+ const parts = line.split(",");
11106
+ return { category: parts[0], margin: parts[1], text: parts.slice(2).join(","), raw: line };
11107
+ });
11108
+ }
11109
+ function feedbackIdentity(category, text) {
11110
+ return `${category} ${text}`;
11111
+ }
11112
+
11113
+ // src/analysis/UnclassifiedCollector.ts
11114
+ var MAX_ROWS = 500;
11115
+ var FILENAME = "unclassified.csv";
11116
+ var URI_RE = /[a-z][\w+.-]*:\/\/\S+/gi;
11117
+ var EMAIL_RE = /\b[\w.+-]+@[\w.-]+\.\w+\b/g;
11118
+ var SECRET_RE = /\b(?=[A-Za-z0-9_+/=-]*[0-9])(?=[A-Za-z0-9_+/=-]*[A-Za-z])[A-Za-z0-9_+/=-]{16,}\b/g;
11119
+ var IP_RE = /\b\d{1,3}(?:\.\d{1,3}){3}\b/g;
11120
+ var WIN_PATH_RE = /[A-Za-z]:\\[^\s'"]+/g;
11121
+ var UNIX_PATH_RE = /(?:\/[\w.-]+){2,}/g;
11122
+ function redactForStorage(raw) {
11123
+ 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 ");
11124
+ return tokenize(redacted).join(" ");
11125
+ }
11126
+ function shouldCollect(c) {
11127
+ return c.category === "unknown" || c.margin <= STRENGTH.collectMaxMargin;
11128
+ }
11129
+ function tildeify(p) {
11130
+ const home = os5.homedir();
11131
+ return p.startsWith(home) ? `~${p.slice(home.length)}` : p;
11132
+ }
11133
+ function collectUnclassified(classified, projectDir) {
11134
+ 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);
11135
+ if (rows.length === 0) return;
11136
+ const file = path5.join(projectDir, FILENAME);
11137
+ const firstTime = !fs4.existsSync(file);
11138
+ const byIdentity = /* @__PURE__ */ new Map();
11139
+ if (!firstTime) {
11140
+ for (const r of parseFeedbackRows(fs4.readFileSync(file, "utf8"))) {
11141
+ byIdentity.set(feedbackIdentity(r.category, r.text), r.raw);
11142
+ }
11143
+ }
11144
+ let added = false;
11145
+ for (const r of rows) {
11146
+ const id = feedbackIdentity(r.category, r.text);
11147
+ byIdentity.delete(id);
11148
+ byIdentity.set(id, `${r.category},${r.margin},${r.text}`);
11149
+ added = true;
11150
+ }
11151
+ if (!added) return;
11152
+ const all = [...byIdentity.values()].slice(-MAX_ROWS);
11153
+ fs4.mkdirSync(projectDir, { recursive: true });
11154
+ fs4.writeFileSync(file, `${FEEDBACK_HEADER}
11155
+ ${all.join("\n")}
11156
+ `, "utf8");
11157
+ if (firstTime) {
11158
+ logger.info(
11159
+ `Unrecognized failures collected locally at ${tildeify(file)} to improve offline classification. Local only - never sent anywhere. Disable: failureAnalysis.collectUnclassified=false`
11160
+ );
11161
+ }
11162
+ }
11163
+
11164
+ // src/utils/paths.ts
11165
+ init_esm_shims();
11166
+ import os6 from "os";
11167
+ import path6 from "path";
11168
+ import crypto from "crypto";
11169
+ function resolveProjectDir(options, cwd) {
11170
+ const projectKey = crypto.createHash("sha256").update(cwd + (options.outputFile ?? "")).digest("hex").slice(0, 8);
11171
+ return path6.join(os6.homedir(), ".reportforge", projectKey);
11172
+ }
11173
+
11174
+ // src/analysis/index.ts
11175
+ var DEFAULTS = { maxClusters: 10, minStrength: "weak", maxFailuresToAnalyse: 500 };
11176
+ var STRENGTH_RANK = Object.fromEntries(
11177
+ STRENGTH_LEVELS.map((level, i) => [level, i])
11178
+ );
11179
+ function analyseClassified(classified, totalFailures, opts = {}) {
11180
+ const maxClusters = opts.maxClusters ?? DEFAULTS.maxClusters;
11181
+ const minRank = STRENGTH_RANK[opts.minStrength ?? DEFAULTS.minStrength];
11182
+ const cap = opts.maxFailuresToAnalyse ?? DEFAULTS.maxFailuresToAnalyse;
11183
+ const clusters = clusterClassified(classified).filter((c) => STRENGTH_RANK[c.strength] >= minRank).sort((a, b) => b.count - a.count).slice(0, maxClusters);
11184
+ return {
11185
+ clusters,
11186
+ totalFailures,
11187
+ analysedCount: classified.length,
11188
+ truncated: totalFailures > cap,
11189
+ // dominantCategory is part of the AnalysisResult contract; reserved for
11190
+ // consumers (e.g. a future executive badge / notify summary). Not rendered
11191
+ // by the v1 partials.
11192
+ dominantCategory: dominantCategory(clusters),
11193
+ oneLiner: buildOneLiner(clusters)
11194
+ };
11195
+ }
11196
+ function templatesConsumeAnalysis(options) {
11197
+ if (options.templatePath && options.templatePath.length > 0) return true;
11198
+ const templates = Array.isArray(options.template) ? options.template : [options.template];
11199
+ return templates.some((t) => t === "detailed" || t === "executive");
11200
+ }
11201
+ function runFailureAnalysis(reportData, options, cwd) {
11202
+ const fa = options.failureAnalysis;
11203
+ if (reportData.stats.failed === 0 || !fa.enabled) return;
11204
+ const cap = fa.maxFailuresToAnalyse;
11205
+ const analysed = reportData.failures.slice(0, cap);
11206
+ const model = loadModel(fa.autoUpdateModel);
11207
+ const classified = classifyAll(analysed, model);
11208
+ if (templatesConsumeAnalysis(options)) {
11209
+ reportData.analysis = analyseClassified(classified, reportData.failures.length, fa);
11210
+ }
11211
+ if (fa.collectUnclassified) {
11212
+ collectUnclassified(classified, resolveProjectDir(options, cwd));
11213
+ }
11214
+ }
11215
+
10838
11216
  // src/collector/DataCollector.ts
10839
11217
  init_esm_shims();
10840
11218
 
10841
11219
  // src/collector/SuiteWalker.ts
10842
11220
  init_esm_shims();
11221
+
11222
+ // src/collector/stats-utils.ts
11223
+ init_esm_shims();
11224
+ function statsFromTests(tests) {
11225
+ return {
11226
+ total: tests.length,
11227
+ passed: tests.filter((t) => t.status === "passed").length,
11228
+ failed: tests.filter((t) => t.status === "failed" || t.status === "timedOut").length,
11229
+ skipped: tests.filter((t) => t.status === "skipped").length,
11230
+ flaky: tests.filter((t) => t.status === "flaky").length
11231
+ };
11232
+ }
11233
+ function aggregateStats(parts) {
11234
+ return parts.reduce(
11235
+ (acc, s) => ({
11236
+ total: acc.total + s.total,
11237
+ passed: acc.passed + s.passed,
11238
+ failed: acc.failed + s.failed,
11239
+ skipped: acc.skipped + s.skipped,
11240
+ flaky: acc.flaky + s.flaky
11241
+ }),
11242
+ { total: 0, passed: 0, failed: 0, skipped: 0, flaky: 0 }
11243
+ );
11244
+ }
11245
+
11246
+ // src/collector/SuiteWalker.ts
10843
11247
  var SuiteWalker = class {
10844
11248
  build(rootSuite, resultMap) {
10845
11249
  return rootSuite.suites.map(
@@ -10850,15 +11254,15 @@ var SuiteWalker = class {
10850
11254
  const suites = projectSuite.suites.map(
10851
11255
  (s) => this.buildSuite(s, resultMap)
10852
11256
  );
10853
- const stats = this.aggregateStats(suites.map((s) => s.stats));
11257
+ const stats = aggregateStats(suites.map((s) => s.stats));
10854
11258
  return { name: projectSuite.title || "default", suites, stats };
10855
11259
  }
10856
11260
  buildSuite(suite, resultMap) {
10857
11261
  const childSuites = suite.suites.map((s) => this.buildSuite(s, resultMap));
10858
11262
  const tests = suite.tests.map((t) => this.buildTest(t, resultMap));
10859
- const stats = this.aggregateStats([
11263
+ const stats = aggregateStats([
10860
11264
  ...childSuites.map((s) => s.stats),
10861
- this.statsFromTests(tests)
11265
+ statsFromTests(tests)
10862
11266
  ]);
10863
11267
  return {
10864
11268
  title: suite.title,
@@ -10895,27 +11299,6 @@ var SuiteWalker = class {
10895
11299
  if (actual === "failed" && expected === "failed") return "passed";
10896
11300
  return "failed";
10897
11301
  }
10898
- statsFromTests(tests) {
10899
- return {
10900
- total: tests.length,
10901
- passed: tests.filter((t) => t.status === "passed").length,
10902
- failed: tests.filter((t) => t.status === "failed" || t.status === "timedOut").length,
10903
- skipped: tests.filter((t) => t.status === "skipped").length,
10904
- flaky: tests.filter((t) => t.status === "flaky").length
10905
- };
10906
- }
10907
- aggregateStats(parts) {
10908
- return parts.reduce(
10909
- (acc, s) => ({
10910
- total: acc.total + s.total,
10911
- passed: acc.passed + s.passed,
10912
- failed: acc.failed + s.failed,
10913
- skipped: acc.skipped + s.skipped,
10914
- flaky: acc.flaky + s.flaky
10915
- }),
10916
- { total: 0, passed: 0, failed: 0, skipped: 0, flaky: 0 }
10917
- );
10918
- }
10919
11302
  };
10920
11303
 
10921
11304
  // src/utils/env.ts
@@ -11251,7 +11634,7 @@ var ShardMerger = class {
11251
11634
  }
11252
11635
  return Array.from(projectMap.entries()).map(([name, projectSuites]) => {
11253
11636
  const suites = this.mergeFileSuites(projectSuites);
11254
- const stats = this.aggregateStats(suites.map((s) => s.stats));
11637
+ const stats = aggregateStats(suites.map((s) => s.stats));
11255
11638
  return { name, suites, stats };
11256
11639
  });
11257
11640
  }
@@ -11272,9 +11655,9 @@ var ShardMerger = class {
11272
11655
  const suitePath = [ref.title ?? ""];
11273
11656
  const tests = allSpecs.map((spec) => this.specToTestNode(spec, projectName, suitePath));
11274
11657
  const childSuiteNodes = this.buildDescribeSuites(allChildSuites, projectName, suitePath);
11275
- const stats = this.aggregateStats([
11658
+ const stats = aggregateStats([
11276
11659
  ...childSuiteNodes.map((s) => s.stats),
11277
- this.statsFromTests(tests)
11660
+ statsFromTests(tests)
11278
11661
  ]);
11279
11662
  return {
11280
11663
  title: ref.title ?? ref.file ?? "unknown",
@@ -11291,9 +11674,9 @@ var ShardMerger = class {
11291
11674
  const suitePath = [...parentPath, suite.title];
11292
11675
  const tests = suite.specs.map((spec) => this.specToTestNode(spec, projectName, suitePath));
11293
11676
  const childSuites = this.buildDescribeSuites(suite.suites ?? [], projectName, suitePath);
11294
- const stats = this.aggregateStats([
11677
+ const stats = aggregateStats([
11295
11678
  ...childSuites.map((s) => s.stats),
11296
- this.statsFromTests(tests)
11679
+ statsFromTests(tests)
11297
11680
  ]);
11298
11681
  return {
11299
11682
  title: suite.title,
@@ -11448,39 +11831,18 @@ var ShardMerger = class {
11448
11831
  suiteResults
11449
11832
  };
11450
11833
  }
11451
- statsFromTests(tests) {
11452
- return {
11453
- total: tests.length,
11454
- passed: tests.filter((t) => t.status === "passed").length,
11455
- failed: tests.filter((t) => t.status === "failed" || t.status === "timedOut").length,
11456
- skipped: tests.filter((t) => t.status === "skipped").length,
11457
- flaky: tests.filter((t) => t.status === "flaky").length
11458
- };
11459
- }
11460
- aggregateStats(parts) {
11461
- return parts.reduce(
11462
- (acc, s) => ({
11463
- total: acc.total + s.total,
11464
- passed: acc.passed + s.passed,
11465
- failed: acc.failed + s.failed,
11466
- skipped: acc.skipped + s.skipped,
11467
- flaky: acc.flaky + s.flaky
11468
- }),
11469
- { total: 0, passed: 0, failed: 0, skipped: 0, flaky: 0 }
11470
- );
11471
- }
11472
11834
  };
11473
11835
 
11474
11836
  // src/pdf/PdfGenerator.ts
11475
11837
  init_esm_shims();
11476
- import fs10 from "fs";
11477
- import path9 from "path";
11838
+ import fs12 from "fs";
11839
+ import path12 from "path";
11478
11840
 
11479
11841
  // src/templates/engine.ts
11480
11842
  init_esm_shims();
11481
11843
  var import_handlebars = __toESM(require_lib());
11482
- import fs3 from "fs";
11483
- import path4 from "path";
11844
+ import fs5 from "fs";
11845
+ import path7 from "path";
11484
11846
 
11485
11847
  // src/utils/duration.ts
11486
11848
  init_esm_shims();
@@ -11500,9 +11862,9 @@ var TemplateEngine = class {
11500
11862
  this.fontsBundleCss = "";
11501
11863
  this.customTemplateCache = /* @__PURE__ */ new Map();
11502
11864
  this.handlebars = import_handlebars.default.create();
11503
- this.templatesDir = path4.resolve(__dirname, "templates");
11504
- if (!fs3.existsSync(this.templatesDir)) {
11505
- this.templatesDir = path4.resolve(__dirname, "../templates");
11865
+ this.templatesDir = path7.resolve(__dirname, "templates");
11866
+ if (!fs5.existsSync(this.templatesDir)) {
11867
+ this.templatesDir = path7.resolve(__dirname, "../templates");
11506
11868
  }
11507
11869
  this.registerHelpers();
11508
11870
  this.registerPartials();
@@ -11519,21 +11881,21 @@ var TemplateEngine = class {
11519
11881
  this.chartjsInline = inline;
11520
11882
  }
11521
11883
  render(data, template) {
11522
- const layoutPath = path4.join(this.templatesDir, "layouts", `${template}.hbs`);
11523
- if (!fs3.existsSync(layoutPath)) {
11884
+ const layoutPath = path7.join(this.templatesDir, "layouts", `${template}.hbs`);
11885
+ if (!fs5.existsSync(layoutPath)) {
11524
11886
  throw new Error(`[reportforge] Template layout not found: ${layoutPath}`);
11525
11887
  }
11526
- const layoutSource = fs3.readFileSync(layoutPath, "utf8");
11888
+ const layoutSource = fs5.readFileSync(layoutPath, "utf8");
11527
11889
  const compiledLayout = this.handlebars.compile(layoutSource);
11528
11890
  const ctx = this.buildContext(data, template);
11529
11891
  return compiledLayout(ctx);
11530
11892
  }
11531
11893
  renderFromPath(data, filePath) {
11532
11894
  if (!this.customTemplateCache.has(filePath)) {
11533
- if (!fs3.existsSync(filePath)) {
11895
+ if (!fs5.existsSync(filePath)) {
11534
11896
  throw new Error(`[reportforge] Custom template not found: ${filePath}`);
11535
11897
  }
11536
- const source = fs3.readFileSync(filePath, "utf8");
11898
+ const source = fs5.readFileSync(filePath, "utf8");
11537
11899
  this.customTemplateCache.set(filePath, this.handlebars.compile(source));
11538
11900
  }
11539
11901
  const compiledFn = this.customTemplateCache.get(filePath);
@@ -11646,17 +12008,17 @@ var TemplateEngine = class {
11646
12008
  return tests.sort((a, b) => b.durationMs - a.durationMs).slice(0, 10);
11647
12009
  }
11648
12010
  readStyle(filename) {
11649
- const stylePath = path4.join(this.templatesDir, "styles", filename);
11650
- if (!fs3.existsSync(stylePath)) return "";
11651
- return fs3.readFileSync(stylePath, "utf8");
12011
+ const stylePath = path7.join(this.templatesDir, "styles", filename);
12012
+ if (!fs5.existsSync(stylePath)) return "";
12013
+ return fs5.readFileSync(stylePath, "utf8");
11652
12014
  }
11653
12015
  registerPartials() {
11654
- const partialsDir = path4.join(this.templatesDir, "partials");
11655
- if (!fs3.existsSync(partialsDir)) return;
11656
- for (const file of fs3.readdirSync(partialsDir)) {
12016
+ const partialsDir = path7.join(this.templatesDir, "partials");
12017
+ if (!fs5.existsSync(partialsDir)) return;
12018
+ for (const file of fs5.readdirSync(partialsDir)) {
11657
12019
  if (!file.endsWith(".hbs")) continue;
11658
- const name = path4.basename(file, ".hbs");
11659
- const src = fs3.readFileSync(path4.join(partialsDir, file), "utf8");
12020
+ const name = path7.basename(file, ".hbs");
12021
+ const src = fs5.readFileSync(path7.join(partialsDir, file), "utf8");
11660
12022
  this.handlebars.registerPartial(name, src);
11661
12023
  }
11662
12024
  }
@@ -11719,6 +12081,10 @@ var TemplateEngine = class {
11719
12081
  );
11720
12082
  this.handlebars.registerHelper("lt", (a, b) => a < b);
11721
12083
  this.handlebars.registerHelper("or", (a, b) => Boolean(a) || Boolean(b));
12084
+ this.handlebars.registerHelper("categoryLabel", (cat) => CATEGORY_LABEL[cat] ?? cat);
12085
+ this.handlebars.registerHelper("strengthLabel", (s) => s ? s.charAt(0).toUpperCase() + s.slice(1) : "");
12086
+ this.handlebars.registerHelper("pluralise", (n, one, many) => n === 1 ? one : many);
12087
+ this.handlebars.registerHelper("subtract", (a, b) => (a ?? 0) - (b ?? 0));
11722
12088
  this.handlebars.registerHelper("statusBadge", (status) => {
11723
12089
  const map = {
11724
12090
  passed: "badge--passed",
@@ -11740,7 +12106,7 @@ var TemplateEngine = class {
11740
12106
 
11741
12107
  // src/screenshots/ScreenshotEmbedder.ts
11742
12108
  init_esm_shims();
11743
- import fs4 from "fs";
12109
+ import fs6 from "fs";
11744
12110
  import sharp from "sharp";
11745
12111
  var ScreenshotEmbedder = class {
11746
12112
  async embedAll(failures, opts) {
@@ -11749,7 +12115,7 @@ var ScreenshotEmbedder = class {
11749
12115
  async embed(failure, opts) {
11750
12116
  if (!failure.screenshotPath && !failure.screenshotBuffer) return;
11751
12117
  try {
11752
- const raw = failure.screenshotBuffer ?? await fs4.promises.readFile(failure.screenshotPath);
12118
+ const raw = failure.screenshotBuffer ?? await fs6.promises.readFile(failure.screenshotPath);
11753
12119
  const { buffer, mime } = opts.reencode ? await this.recompress(raw, opts) : { buffer: raw, mime: this.detectMimeType(raw) };
11754
12120
  failure.screenshotBase64 = `data:${mime};base64,${buffer.toString("base64")}`;
11755
12121
  } catch (e) {
@@ -11761,7 +12127,7 @@ var ScreenshotEmbedder = class {
11761
12127
  }
11762
12128
  async embedLogoFile(logoPath) {
11763
12129
  try {
11764
- const buffer = await fs4.promises.readFile(logoPath);
12130
+ const buffer = await fs6.promises.readFile(logoPath);
11765
12131
  const ext = logoPath.split(".").pop()?.toLowerCase();
11766
12132
  let mime = "image/png";
11767
12133
  if (ext === "jpg" || ext === "jpeg") mime = "image/jpeg";
@@ -11806,7 +12172,7 @@ var ScreenshotEmbedder = class {
11806
12172
  // src/pdf/BrowserManager.ts
11807
12173
  init_esm_shims();
11808
12174
  import { execSync as execSync2 } from "child_process";
11809
- import fs5 from "fs";
12175
+ import fs7 from "fs";
11810
12176
  var BrowserManager = class {
11811
12177
  async launch(executablePath) {
11812
12178
  const puppeteer = __require("puppeteer-core");
@@ -11834,13 +12200,13 @@ var BrowserManager = class {
11834
12200
  }
11835
12201
  }
11836
12202
  resolveChromePath(explicit, puppeteer) {
11837
- if (explicit && fs5.existsSync(explicit)) return explicit;
12203
+ if (explicit && fs7.existsSync(explicit)) return explicit;
11838
12204
  const envPath = process.env.PUPPETEER_EXECUTABLE_PATH;
11839
- if (envPath && fs5.existsSync(envPath)) return envPath;
12205
+ if (envPath && fs7.existsSync(envPath)) return envPath;
11840
12206
  try {
11841
12207
  const chromeFinder = require_lib2();
11842
12208
  const found = chromeFinder();
11843
- if (found && fs5.existsSync(found)) return found;
12209
+ if (found && fs7.existsSync(found)) return found;
11844
12210
  } catch {
11845
12211
  }
11846
12212
  const linuxPaths = [
@@ -11851,19 +12217,19 @@ var BrowserManager = class {
11851
12217
  "/snap/bin/chromium"
11852
12218
  ];
11853
12219
  for (const p of linuxPaths) {
11854
- if (fs5.existsSync(p)) return p;
12220
+ if (fs7.existsSync(p)) return p;
11855
12221
  }
11856
12222
  try {
11857
12223
  const found = execSync2("which google-chrome chromium chromium-browser 2>/dev/null", {
11858
12224
  stdio: "pipe"
11859
12225
  }).toString().trim().split("\n")[0];
11860
- if (found && fs5.existsSync(found)) return found;
12226
+ if (found && fs7.existsSync(found)) return found;
11861
12227
  } catch {
11862
12228
  }
11863
12229
  if (puppeteer.executablePath) {
11864
12230
  try {
11865
12231
  const bundled = puppeteer.executablePath();
11866
- if (bundled && fs5.existsSync(bundled)) return bundled;
12232
+ if (bundled && fs7.existsSync(bundled)) return bundled;
11867
12233
  } catch {
11868
12234
  }
11869
12235
  }
@@ -11876,21 +12242,21 @@ var BrowserManager = class {
11876
12242
 
11877
12243
  // src/pdf/HtmlWriter.ts
11878
12244
  init_esm_shims();
11879
- import fs6 from "fs";
11880
- import os4 from "os";
11881
- import path5 from "path";
11882
- import crypto from "crypto";
12245
+ import fs8 from "fs";
12246
+ import os7 from "os";
12247
+ import path8 from "path";
12248
+ import crypto2 from "crypto";
11883
12249
  var HtmlWriter = class {
11884
12250
  async write(html) {
11885
- const tmpDir = os4.tmpdir();
11886
- const filename = `rf-report-${crypto.randomBytes(8).toString("hex")}.html`;
11887
- this.tempPath = path5.join(tmpDir, filename);
11888
- await fs6.promises.writeFile(this.tempPath, html, "utf8");
12251
+ const tmpDir = os7.tmpdir();
12252
+ const filename = `rf-report-${crypto2.randomBytes(8).toString("hex")}.html`;
12253
+ this.tempPath = path8.join(tmpDir, filename);
12254
+ await fs8.promises.writeFile(this.tempPath, html, "utf8");
11889
12255
  return this.tempPath;
11890
12256
  }
11891
12257
  async cleanup() {
11892
12258
  if (this.tempPath) {
11893
- await fs6.promises.unlink(this.tempPath).catch(() => {
12259
+ await fs8.promises.unlink(this.tempPath).catch(() => {
11894
12260
  });
11895
12261
  this.tempPath = void 0;
11896
12262
  }
@@ -11924,10 +12290,10 @@ var ChartRenderer = class {
11924
12290
  // src/pdf/PdfEncryptor.ts
11925
12291
  init_esm_shims();
11926
12292
  import { execFileSync } from "child_process";
11927
- import fs7 from "fs";
11928
- import path6 from "path";
11929
- import os5 from "os";
11930
- import crypto2 from "crypto";
12293
+ import fs9 from "fs";
12294
+ import path9 from "path";
12295
+ import os8 from "os";
12296
+ import crypto3 from "crypto";
11931
12297
  var PdfEncryptor = class {
11932
12298
  async encrypt(pdfPath, password) {
11933
12299
  if (!password) return;
@@ -11937,10 +12303,10 @@ var PdfEncryptor = class {
11937
12303
  );
11938
12304
  return;
11939
12305
  }
11940
- const tmpPath = path6.join(os5.tmpdir(), `rf-encrypted-${Date.now()}.pdf`);
11941
- const pwFile = path6.join(os5.tmpdir(), `rf-pw-${crypto2.randomBytes(8).toString("hex")}`);
12306
+ const tmpPath = path9.join(os8.tmpdir(), `rf-encrypted-${Date.now()}.pdf`);
12307
+ const pwFile = path9.join(os8.tmpdir(), `rf-pw-${crypto3.randomBytes(8).toString("hex")}`);
11942
12308
  try {
11943
- fs7.writeFileSync(pwFile, `${password}
12309
+ fs9.writeFileSync(pwFile, `${password}
11944
12310
  ${password}`, { mode: 384 });
11945
12311
  execFileSync("qpdf", [
11946
12312
  `--password-file=${pwFile}`,
@@ -11950,13 +12316,13 @@ ${password}`, { mode: 384 });
11950
12316
  pdfPath,
11951
12317
  tmpPath
11952
12318
  ], { stdio: "pipe" });
11953
- fs7.renameSync(tmpPath, pdfPath);
12319
+ fs9.renameSync(tmpPath, pdfPath);
11954
12320
  logger.info(`PDF encrypted with password.`);
11955
12321
  } catch (e) {
11956
12322
  logger.warn(`qpdf encryption failed: ${e.message}`);
11957
12323
  } finally {
11958
- if (fs7.existsSync(tmpPath)) fs7.unlinkSync(tmpPath);
11959
- fs7.rmSync(pwFile, { force: true });
12324
+ if (fs9.existsSync(tmpPath)) fs9.unlinkSync(tmpPath);
12325
+ fs9.rmSync(pwFile, { force: true });
11960
12326
  }
11961
12327
  }
11962
12328
  isQpdfAvailable() {
@@ -11971,8 +12337,8 @@ ${password}`, { mode: 384 });
11971
12337
 
11972
12338
  // src/filename/FilenameResolver.ts
11973
12339
  init_esm_shims();
11974
- import path7 from "path";
11975
- import fs8 from "fs";
12340
+ import path10 from "path";
12341
+ import fs10 from "fs";
11976
12342
  var MAX_BRANCH_LENGTH = 50;
11977
12343
  var FilenameResolver = class {
11978
12344
  /**
@@ -11989,8 +12355,8 @@ var FilenameResolver = class {
11989
12355
  const branch = this.sanitiseBranch(ctx.branch);
11990
12356
  const status = ctx.status;
11991
12357
  const resolved = template.replace(/\{date\}/g, date).replace(/\{branch\}/g, branch).replace(/\{status\}/g, status);
11992
- const absolute = path7.isAbsolute(resolved) ? resolved : path7.resolve(process.cwd(), resolved);
11993
- fs8.mkdirSync(path7.dirname(absolute), { recursive: true });
12358
+ const absolute = path10.isAbsolute(resolved) ? resolved : path10.resolve(process.cwd(), resolved);
12359
+ fs10.mkdirSync(path10.dirname(absolute), { recursive: true });
11994
12360
  return absolute;
11995
12361
  }
11996
12362
  /**
@@ -12002,7 +12368,7 @@ var FilenameResolver = class {
12002
12368
  * // → '/out/2026-report-executive.pdf'
12003
12369
  */
12004
12370
  injectTemplateSuffix(outputPath, suffix) {
12005
- const ext = path7.extname(outputPath);
12371
+ const ext = path10.extname(outputPath);
12006
12372
  const base = outputPath.slice(0, outputPath.length - ext.length);
12007
12373
  return `${base}-${suffix}${ext}`;
12008
12374
  }
@@ -12060,8 +12426,8 @@ var PRESETS = {
12060
12426
 
12061
12427
  // src/pdf/FailurePaginator.ts
12062
12428
  init_esm_shims();
12063
- import fs9 from "fs";
12064
- import path8 from "path";
12429
+ import fs11 from "fs";
12430
+ import path11 from "path";
12065
12431
  var FailurePaginator = class {
12066
12432
  async paginate(failures, maxInline, pdfOutputPath) {
12067
12433
  if (failures.length <= maxInline) {
@@ -12071,13 +12437,13 @@ var FailurePaginator = class {
12071
12437
  const overflow = failures.slice(maxInline);
12072
12438
  const sidecarPath = this.sidecarPathFor(pdfOutputPath);
12073
12439
  try {
12074
- await fs9.promises.writeFile(
12440
+ await fs11.promises.writeFile(
12075
12441
  sidecarPath,
12076
12442
  JSON.stringify(this.serialize(overflow), null, 2),
12077
12443
  "utf8"
12078
12444
  );
12079
12445
  logger.info(
12080
- `Emitted ${overflow.length} overflow failures \u2192 ${path8.basename(sidecarPath)}`
12446
+ `Emitted ${overflow.length} overflow failures \u2192 ${path11.basename(sidecarPath)}`
12081
12447
  );
12082
12448
  } catch (e) {
12083
12449
  logger.warn(`Could not write failure sidecar: ${e.message}`);
@@ -12086,9 +12452,9 @@ var FailurePaginator = class {
12086
12452
  return { inline, overflowCount: overflow.length, sidecarPath };
12087
12453
  }
12088
12454
  sidecarPathFor(pdfPath) {
12089
- const dir = path8.dirname(pdfPath);
12090
- const base = path8.basename(pdfPath, path8.extname(pdfPath));
12091
- return path8.join(dir, `${base}-failures.json`);
12455
+ const dir = path11.dirname(pdfPath);
12456
+ const base = path11.basename(pdfPath, path11.extname(pdfPath));
12457
+ return path11.join(dir, `${base}-failures.json`);
12092
12458
  }
12093
12459
  /**
12094
12460
  * Drops the inlined image bytes — a 150-entry overflow would otherwise
@@ -12138,14 +12504,35 @@ var PdfGenerator = class {
12138
12504
  *
12139
12505
  * @param data - Assembled `ReportData` (projects, stats, failures, charts).
12140
12506
  * @param options - Validated reporter options.
12141
- * @param _licenseInfo - Kept for API symmetry; unused in this method.
12507
+ * @param licenseInfo - Verified license. JWT must be present and valid for
12508
+ * non-demo sources; `source: 'demo'` is the only allowed JWT-free path.
12142
12509
  * @returns Absolute paths of written PDF files.
12143
12510
  */
12144
- async generateAll(data, options, _licenseInfo) {
12511
+ async generateAll(data, options, licenseInfo) {
12512
+ if (false) {
12513
+ if (licenseInfo.expiry <= /* @__PURE__ */ new Date()) {
12514
+ throw new Error("[reportforge] Demo license expired \u2014 PDF generation skipped.");
12515
+ }
12516
+ } else {
12517
+ if (!licenseInfo.jwt) {
12518
+ throw new Error("[reportforge] License JWT missing \u2014 PDF generation skipped.");
12519
+ }
12520
+ if (!LICENSE_PUBLIC_KEY_PEM) {
12521
+ throw new Error("[reportforge] LICENSE_PUBLIC_KEY_PEM missing \u2014 cannot verify license JWT.");
12522
+ }
12523
+ try {
12524
+ const payload = new JwtVerifier(LICENSE_PUBLIC_KEY_PEM).verify(licenseInfo.jwt);
12525
+ if (payload.exp * 1e3 <= Date.now()) {
12526
+ throw new Error("JWT expired");
12527
+ }
12528
+ } catch (err) {
12529
+ throw new Error(`[reportforge] License JWT verification failed: ${err.message}`);
12530
+ }
12531
+ }
12145
12532
  const sources = this.resolveTemplateSources(options);
12146
12533
  const isMulti = sources.length > 1;
12147
12534
  for (const source of sources) {
12148
- if (source.kind === "custom" && !fs10.existsSync(source.absolutePath)) {
12535
+ if (source.kind === "custom" && !fs12.existsSync(source.absolutePath)) {
12149
12536
  throw new Error(
12150
12537
  `[reportforge] Custom template not found: ${source.absolutePath}`
12151
12538
  );
@@ -12176,14 +12563,14 @@ var PdfGenerator = class {
12176
12563
  await this.renderPdf(templateData, templateOptions, source, compression, outputPath, page);
12177
12564
  const capBytes = (options.maxFileSizeMb ?? 0) * 1024 * 1024;
12178
12565
  if (capBytes > 0) {
12179
- const sizeBytes = (await fs10.promises.stat(outputPath)).size;
12566
+ const sizeBytes = (await fs12.promises.stat(outputPath)).size;
12180
12567
  if (sizeBytes > capBytes) {
12181
12568
  const tighter = this.tightenForCap(compression, sizeBytes, capBytes);
12182
12569
  logger.warn(
12183
12570
  `PDF size ${fmtMb(sizeBytes)} exceeds cap ${fmtMb(capBytes)} \u2014 re-rendering (jpeg-q=${tighter.quality}, max-w=${tighter.maxWidth}px, inline-failures\u2264${tighter.maxInlineFailures}).`
12184
12571
  );
12185
12572
  await this.renderPdf(templateData, templateOptions, source, tighter, outputPath, page);
12186
- const finalSize = (await fs10.promises.stat(outputPath)).size;
12573
+ const finalSize = (await fs12.promises.stat(outputPath)).size;
12187
12574
  if (finalSize > capBytes) {
12188
12575
  logger.warn(
12189
12576
  `PDF still ${fmtMb(finalSize)} after retune. Consider lowering maxInlineFailures or splitting the report.`
@@ -12196,7 +12583,7 @@ var PdfGenerator = class {
12196
12583
  if (options.pdfPassword) {
12197
12584
  await this.pdfEncryptor.encrypt(outputPath, options.pdfPassword);
12198
12585
  }
12199
- const finalSizeMb = fmtMb((await fs10.promises.stat(outputPath)).size);
12586
+ const finalSizeMb = fmtMb((await fs12.promises.stat(outputPath)).size);
12200
12587
  logger.info(`PDF report generated: ${outputPath} (${finalSizeMb})`);
12201
12588
  outputPaths.push(outputPath);
12202
12589
  }
@@ -12209,8 +12596,8 @@ var PdfGenerator = class {
12209
12596
  if (options.templatePath && options.templatePath.length > 0) {
12210
12597
  return [...new Set(options.templatePath)].map((p) => ({
12211
12598
  kind: "custom",
12212
- absolutePath: path9.isAbsolute(p) ? p : path9.resolve(process.cwd(), p),
12213
- label: path9.basename(p, ".hbs")
12599
+ absolutePath: path12.isAbsolute(p) ? p : path12.resolve(process.cwd(), p),
12600
+ label: path12.basename(p, ".hbs")
12214
12601
  }));
12215
12602
  }
12216
12603
  const rawIds = Array.isArray(options.template) ? options.template : [options.template];
@@ -12260,7 +12647,7 @@ var PdfGenerator = class {
12260
12647
  ...data,
12261
12648
  failures: paginated.inline,
12262
12649
  overflowFailureCount: paginated.overflowCount,
12263
- overflowSidecarName: paginated.sidecarPath ? path9.basename(paginated.sidecarPath) : void 0
12650
+ overflowSidecarName: paginated.sidecarPath ? path12.basename(paginated.sidecarPath) : void 0
12264
12651
  };
12265
12652
  for (const f of renderData.failures) f.screenshotBase64 = void 0;
12266
12653
  if (options.includeScreenshots !== false) {
@@ -12280,7 +12667,7 @@ var PdfGenerator = class {
12280
12667
  await page.goto(fileUrl, { waitUntil: "domcontentloaded", timeout: 6e4 });
12281
12668
  const showCharts = source.kind === "builtin" ? source.id !== "minimal" : true;
12282
12669
  await this.chartRenderer.waitForCharts(page, showCharts);
12283
- logger.info(`Generating PDF \u2192 ${path9.relative(process.cwd(), outputPath)}`);
12670
+ logger.info(`Generating PDF \u2192 ${path12.relative(process.cwd(), outputPath)}`);
12284
12671
  await page.pdf({
12285
12672
  path: outputPath,
12286
12673
  format: "A4",
@@ -12297,21 +12684,19 @@ function fmtMb(bytes) {
12297
12684
  }
12298
12685
 
12299
12686
  // src/reporter.ts
12300
- import path11 from "path";
12301
- import crypto3 from "crypto";
12302
- import os6 from "os";
12687
+ import path14 from "path";
12303
12688
 
12304
12689
  // src/history/HistoryManager.ts
12305
12690
  init_esm_shims();
12306
- import fs11 from "fs";
12307
- import path10 from "path";
12691
+ import fs13 from "fs";
12692
+ import path13 from "path";
12308
12693
  var HistoryManager = class {
12309
12694
  constructor(filePath) {
12310
12695
  this.filePath = filePath;
12311
12696
  }
12312
12697
  read() {
12313
12698
  try {
12314
- const raw = fs11.readFileSync(this.filePath, "utf8");
12699
+ const raw = fs13.readFileSync(this.filePath, "utf8");
12315
12700
  const parsed = JSON.parse(raw);
12316
12701
  if (parsed.version !== 1) {
12317
12702
  logger.warn(`history.version-mismatch: path=${this.filePath} version=${String(parsed.version)}`);
@@ -12325,12 +12710,12 @@ var HistoryManager = class {
12325
12710
  }
12326
12711
  append(entry, maxSize) {
12327
12712
  try {
12328
- fs11.mkdirSync(path10.dirname(this.filePath), { recursive: true });
12713
+ fs13.mkdirSync(path13.dirname(this.filePath), { recursive: true });
12329
12714
  const next = {
12330
12715
  version: 1,
12331
12716
  entries: [entry, ...this.read()].slice(0, maxSize)
12332
12717
  };
12333
- fs11.writeFileSync(this.filePath, JSON.stringify(next, null, 2), { mode: 420 });
12718
+ fs13.writeFileSync(this.filePath, JSON.stringify(next, null, 2), { mode: 420 });
12334
12719
  return next.entries;
12335
12720
  } catch (err) {
12336
12721
  logger.warn(`history.write-failed: path=${this.filePath} error=${err.message}`);
@@ -12671,11 +13056,13 @@ var PdfReporter = class {
12671
13056
  this.dataCollector = new DataCollector();
12672
13057
  this.pdfGenerator = new PdfGenerator();
12673
13058
  this.options = parseOptions(rawOptions);
12674
- const version = true ? "0.2.2" : "0.x";
13059
+ const version = true ? "0.4.0" : "0.x";
12675
13060
  logger.info(`@reportforge/playwright-pdf v${version} initialised`);
12676
13061
  this.licenseClient = new LicenseClient({
12677
13062
  licenseKey: this.options.licenseKey,
12678
- serverUrl: this.options.serverUrl
13063
+ serverUrl: this.options.serverUrl,
13064
+ autoUpdateModel: this.options.failureAnalysis.autoUpdateModel,
13065
+ currentModelVersion: MODEL.version
12679
13066
  });
12680
13067
  }
12681
13068
  onBegin(config, suite) {
@@ -12697,6 +13084,7 @@ var PdfReporter = class {
12697
13084
  }
12698
13085
  }
12699
13086
  const reportData = this._buildReportData(collected, licenseInfo);
13087
+ runFailureAnalysis(reportData, this.options, this.cwd);
12700
13088
  let pdfPaths = [];
12701
13089
  try {
12702
13090
  pdfPaths = await this.pdfGenerator.generateAll(reportData, this.options, licenseInfo);
@@ -12803,7 +13191,7 @@ var PdfReporter = class {
12803
13191
  }
12804
13192
  resolveProjectName() {
12805
13193
  try {
12806
- const pkgPath = path11.resolve(process.cwd(), "package.json");
13194
+ const pkgPath = path14.resolve(process.cwd(), "package.json");
12807
13195
  const pkg = __require(pkgPath);
12808
13196
  return pkg.name ?? "Playwright Tests";
12809
13197
  } catch {
@@ -12853,10 +13241,9 @@ function deriveVerdict(stats) {
12853
13241
  }
12854
13242
  function resolveHistoryPath(options, cwd) {
12855
13243
  if (options.historyFile) {
12856
- return path11.isAbsolute(options.historyFile) ? options.historyFile : path11.resolve(cwd, options.historyFile);
13244
+ return path14.isAbsolute(options.historyFile) ? options.historyFile : path14.resolve(cwd, options.historyFile);
12857
13245
  }
12858
- const projectKey = crypto3.createHash("sha256").update(cwd + (options.outputFile ?? "")).digest("hex").slice(0, 8);
12859
- return path11.join(os6.homedir(), ".reportforge", projectKey, "history.json");
13246
+ return path14.join(resolveProjectDir(options, cwd), "history.json");
12860
13247
  }
12861
13248
  function flattenSuiteNodes(suites) {
12862
13249
  return suites.flatMap((s) => [s, ...flattenSuiteNodes(s.suites)]);