@reportforge/playwright-pdf 0.19.0 → 0.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1221,13 +1221,13 @@ var require_ast = __commonJS({
1221
1221
  helperExpression: function helperExpression(node) {
1222
1222
  return node.type === "SubExpression" || (node.type === "MustacheStatement" || node.type === "BlockStatement") && !!(node.params && node.params.length || node.hash);
1223
1223
  },
1224
- scopedId: function scopedId(path14) {
1225
- return /^\.|this\b/.test(path14.original);
1224
+ scopedId: function scopedId(path16) {
1225
+ return /^\.|this\b/.test(path16.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(path14) {
1230
- return path14.parts.length === 1 && !AST.helpers.scopedId(path14) && !path14.depth;
1229
+ simpleId: function simpleId(path16) {
1230
+ return path16.parts.length === 1 && !AST.helpers.scopedId(path16) && !path16.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(path14, params, hash, open, strip, locInfo) {
2304
+ function prepareMustache(path16, 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: path14,
2309
+ path: path16,
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), path14 = decorator.path;
2629
+ var params = this.setupFullMustacheParams(decorator, program, void 0), path16 = decorator.path;
2630
2630
  this.useDecorators = true;
2631
- this.opcode("registerDecorator", params.length, path14.original);
2631
+ this.opcode("registerDecorator", params.length, path16.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 path14 = sexpr.path, name = path14.parts[0], isBlock = program != null || inverse != null;
2696
- this.opcode("getContext", path14.depth);
2695
+ var path16 = sexpr.path, name = path16.parts[0], isBlock = program != null || inverse != null;
2696
+ this.opcode("getContext", path16.depth);
2697
2697
  this.opcode("pushProgram", program);
2698
2698
  this.opcode("pushProgram", inverse);
2699
- path14.strict = true;
2700
- this.accept(path14);
2699
+ path16.strict = true;
2700
+ this.accept(path16);
2701
2701
  this.opcode("invokeAmbiguous", name, isBlock);
2702
2702
  },
2703
2703
  simpleSexpr: function simpleSexpr(sexpr) {
2704
- var path14 = sexpr.path;
2705
- path14.strict = true;
2706
- this.accept(path14);
2704
+ var path16 = sexpr.path;
2705
+ path16.strict = true;
2706
+ this.accept(path16);
2707
2707
  this.opcode("resolvePossibleLambda");
2708
2708
  },
2709
2709
  helperSexpr: function helperSexpr(sexpr, program, inverse) {
2710
- var params = this.setupFullMustacheParams(sexpr, program, inverse), path14 = sexpr.path, name = path14.parts[0];
2710
+ var params = this.setupFullMustacheParams(sexpr, program, inverse), path16 = sexpr.path, name = path16.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
- path14.strict = true;
2717
- path14.falsy = true;
2718
- this.accept(path14);
2719
- this.opcode("invokeHelper", params.length, path14.original, _ast2["default"].helpers.simpleId(path14));
2716
+ path16.strict = true;
2717
+ path16.falsy = true;
2718
+ this.accept(path16);
2719
+ this.opcode("invokeHelper", params.length, path16.original, _ast2["default"].helpers.simpleId(path16));
2720
2720
  }
2721
2721
  },
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);
2722
+ PathExpression: function PathExpression(path16) {
2723
+ this.addDepth(path16.depth);
2724
+ this.opcode("getContext", path16.depth);
2725
+ var name = path16.parts[0], scoped = _ast2["default"].helpers.scopedId(path16), blockParamId = !path16.depth && !scoped && this.blockParamIndex(name);
2726
2726
  if (blockParamId) {
2727
- this.opcode("lookupBlockParam", blockParamId, path14.parts);
2727
+ this.opcode("lookupBlockParam", blockParamId, path16.parts);
2728
2728
  } else if (!name) {
2729
2729
  this.opcode("pushContext");
2730
- } else if (path14.data) {
2730
+ } else if (path16.data) {
2731
2731
  this.options.data = true;
2732
- this.opcode("lookupData", path14.depth, path14.parts, path14.strict);
2732
+ this.opcode("lookupData", path16.depth, path16.parts, path16.strict);
2733
2733
  } else {
2734
- this.opcode("lookupOnContext", path14.parts, path14.falsy, path14.strict, scoped);
2734
+ this.opcode("lookupOnContext", path16.parts, path16.falsy, path16.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 path14 = aPath;
3090
+ var path16 = aPath;
3091
3091
  var url = urlParse(aPath);
3092
3092
  if (url) {
3093
3093
  if (!url.path) {
3094
3094
  return aPath;
3095
3095
  }
3096
- path14 = url.path;
3096
+ path16 = url.path;
3097
3097
  }
3098
- var isAbsolute = exports2.isAbsolute(path14);
3099
- var parts = path14.split(/\/+/);
3098
+ var isAbsolute = exports2.isAbsolute(path16);
3099
+ var parts = path16.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
- path14 = parts.join("/");
3117
- if (path14 === "") {
3118
- path14 = isAbsolute ? "/" : ".";
3116
+ path16 = parts.join("/");
3117
+ if (path16 === "") {
3118
+ path16 = isAbsolute ? "/" : ".";
3119
3119
  }
3120
3120
  if (url) {
3121
- url.path = path14;
3121
+ url.path = path16;
3122
3122
  return urlGenerate(url);
3123
3123
  }
3124
- return path14;
3124
+ return path16;
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 path14 = id.parts.join("/");
5928
- return (id.data ? "@" : "") + "PATH:" + path14;
5927
+ var path16 = id.parts.join("/");
5928
+ return (id.data ? "@" : "") + "PATH:" + path16;
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 fs14 = require("fs");
5970
- var templateString = fs14.readFileSync(filename, "utf8");
5969
+ var fs16 = require("fs");
5970
+ var templateString = fs16.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 fs14 = require("fs");
5999
+ var fs16 = 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
- fs14.accessSync(file);
6017
+ fs16.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 = fs14.statSync(file);
6028
+ var stat = fs16.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 path14 = require("path");
6048
+ var path16 = 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 = path14.join(prefix, suffix);
6062
+ const chromePath = path16.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 path14 = require("path");
6080
+ var path16 = 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 = path14.join(inst.trim(), suffix);
6090
+ const execPath = path16.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 path14 = require("path");
6120
- var fs14 = require("fs");
6119
+ var path16 = require("path");
6120
+ var fs16 = 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
- path14.join(require("os").homedir(), ".local/share/applications/"),
6137
+ path16.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 (fs14.existsSync(chromePathToTest) && canAccess(chromePathToTest) && isExecutable(chromePathToTest)) {
6163
+ if (fs16.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: path14, errorMaps, issueData } = params;
6753
- const fullPath = [...path14, ...issueData.path || []];
6752
+ const { data, path: path16, errorMaps, issueData } = params;
6753
+ const fullPath = [...path16, ...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, path14, key) {
6873
+ constructor(parent, value, path16, key) {
6874
6874
  this._cachedPath = [];
6875
6875
  this.parent = parent;
6876
6876
  this.data = value;
6877
- this._path = path14;
6877
+ this._path = path16;
6878
6878
  this._key = key;
6879
6879
  }
6880
6880
  get path() {
@@ -10481,9 +10481,17 @@ var failureAnalysisConfigSchema = external_exports.object({
10481
10481
  // Qualitative floor (not a numeric % — consistent with the match-strength design).
10482
10482
  minStrength: external_exports.enum(["weak", "moderate", "strong"]).default("weak"),
10483
10483
  maxFailuresToAnalyse: external_exports.number().int().min(1).default(500),
10484
- // Consumed by the Plan 3 feedback collector; inert until then.
10485
10484
  collectUnclassified: external_exports.boolean().default(true),
10486
- autoUpdateModel: external_exports.boolean().default(true)
10485
+ // What the feedback collector stores (while collectUnclassified is on):
10486
+ // 'blind-spots' = unknown/low-confidence only; 'all' = every model-scored
10487
+ // failure, so wrong-but-confident verdicts can be corrected by labeling.
10488
+ collectScope: external_exports.enum(["blind-spots", "all"]).default("blind-spots"),
10489
+ autoUpdateModel: external_exports.boolean().default(true),
10490
+ // Use the on-device trained model (when present and its evaluation gate passed).
10491
+ localModel: external_exports.boolean().default(true),
10492
+ // Override the local model file location (default ~/.reportforge/model-local.json).
10493
+ // Point at a repo-committed file to share a team-trained model with ephemeral CI.
10494
+ localModelPath: external_exports.string().optional()
10487
10495
  }).default({});
10488
10496
  var captureConfigSchema = external_exports.object({
10489
10497
  steps: external_exports.boolean().default(false),
@@ -10851,6 +10859,47 @@ var import_node_crypto = require("crypto");
10851
10859
  init_cjs_shims();
10852
10860
  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 };
10853
10861
 
10862
+ // src/analysis/NaiveBayesClassifier.ts
10863
+ init_cjs_shims();
10864
+
10865
+ // src/analysis/tokenize.ts
10866
+ init_cjs_shims();
10867
+ function tokenize(text) {
10868
+ 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);
10869
+ }
10870
+
10871
+ // src/analysis/NaiveBayesClassifier.ts
10872
+ function classify(model, text) {
10873
+ return classifyTokens(model, tokenize(text));
10874
+ }
10875
+ function classifyTokens(model, tokens) {
10876
+ const vocabTokens = tokens.filter(
10877
+ (tok) => tok.length > 0 && model.labels.some((l) => model.logLikelihoods[l]?.[tok] !== void 0)
10878
+ );
10879
+ if (vocabTokens.length === 0) {
10880
+ return { label: "unknown", margin: 0 };
10881
+ }
10882
+ const scores = {};
10883
+ for (const label of model.labels) {
10884
+ let s = model.priors[label];
10885
+ const oov = Math.log(model.alpha / (model.totals[label] + model.alpha * model.vocabSize));
10886
+ const ll = model.logLikelihoods[label];
10887
+ for (const tok of vocabTokens) {
10888
+ s += ll[tok] ?? oov;
10889
+ }
10890
+ scores[label] = s;
10891
+ }
10892
+ const ranked = Object.entries(scores).sort((a, b) => b[1] - a[1]);
10893
+ const topLog = ranked[0][1];
10894
+ const secondLog = ranked[1]?.[1] ?? topLog;
10895
+ const margin = (topLog - secondLog) / vocabTokens.length;
10896
+ return { label: ranked[0][0], margin };
10897
+ }
10898
+ function isValidNaiveBayesModel(m) {
10899
+ const x = m;
10900
+ 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";
10901
+ }
10902
+
10854
10903
  // src/analysis/ModelStore.ts
10855
10904
  var MODEL_SIG_DOMAIN = "reportforge-model-sig:v1\n";
10856
10905
  var testDeps = null;
@@ -10874,10 +10923,7 @@ function verifyModelSignature(modelJson, signatureB64url) {
10874
10923
  return false;
10875
10924
  }
10876
10925
  }
10877
- function schemaValid(m) {
10878
- const x = m;
10879
- 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";
10880
- }
10926
+ var schemaValid = isValidNaiveBayesModel;
10881
10927
  function readCache2() {
10882
10928
  try {
10883
10929
  const raw = import_node_fs.default.readFileSync(cacheFile(), "utf8");
@@ -11194,42 +11240,61 @@ function maybePrintUpdateNotice(licenseInfo, currentVersion) {
11194
11240
  // src/analysis/index.ts
11195
11241
  init_cjs_shims();
11196
11242
 
11197
- // src/analysis/FailureClusterer.ts
11243
+ // src/analysis/LocalModelStore.ts
11198
11244
  init_cjs_shims();
11245
+ var import_node_fs2 = __toESM(require("fs"));
11246
+ var import_node_os2 = __toESM(require("os"));
11247
+ var import_node_path2 = __toESM(require("path"));
11248
+ var testDeps2 = null;
11249
+ function defaultLocalModelPath() {
11250
+ return testDeps2?.file ?? import_node_path2.default.join(import_node_os2.default.homedir(), ".reportforge", "model-local.json");
11251
+ }
11252
+ function loadLocalModel(overridePath) {
11253
+ const file = overridePath ?? defaultLocalModelPath();
11254
+ const explicit = overridePath !== void 0;
11255
+ const reject = (why) => {
11256
+ const msg = `local model at ${file} not used: ${why}`;
11257
+ if (explicit) logger.warn(msg);
11258
+ else logger.debug(msg);
11259
+ return null;
11260
+ };
11261
+ let parsed;
11262
+ try {
11263
+ parsed = JSON.parse(import_node_fs2.default.readFileSync(file, "utf8"));
11264
+ } catch (e) {
11265
+ return explicit ? reject(`unreadable (${e.message})`) : null;
11266
+ }
11267
+ if (parsed.formatVersion !== 1) return reject(`unsupported formatVersion ${String(parsed.formatVersion)}`);
11268
+ if (!isValidNaiveBayesModel(parsed.model)) return reject("embedded model failed schema validation");
11269
+ const meta = parsed.meta;
11270
+ if (!meta || typeof meta.gatePassed !== "boolean") return reject("missing meta");
11271
+ if (!meta.gatePassed) return reject("its evaluation gate did not pass - run train-model after labeling more feedback");
11272
+ if (typeof meta.acceptThreshold !== "number") return reject("gate passed but no calibrated threshold (corrupt meta)");
11273
+ return {
11274
+ model: parsed.model,
11275
+ acceptThreshold: meta.acceptThreshold,
11276
+ strongThreshold: typeof meta.strongThreshold === "number" ? meta.strongThreshold : null,
11277
+ meta
11278
+ };
11279
+ }
11199
11280
 
11200
- // src/analysis/NaiveBayesClassifier.ts
11281
+ // src/analysis/FailureClusterer.ts
11201
11282
  init_cjs_shims();
11202
11283
 
11203
- // src/analysis/tokenize.ts
11284
+ // src/analysis/redact.ts
11204
11285
  init_cjs_shims();
11205
- function tokenize(text) {
11206
- 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);
11286
+ var URI_RE = /[a-z][\w+.-]*:\/\/\S+/gi;
11287
+ var EMAIL_RE = /\b[\w.+-]+@[\w.-]+\.\w+\b/g;
11288
+ var SECRET_RE = /\b(?=[A-Za-z0-9_+/=-]*[0-9])(?=[A-Za-z0-9_+/=-]*[A-Za-z])[A-Za-z0-9_+/=-]{16,}\b/g;
11289
+ var IP_RE = /\b\d{1,3}(?:\.\d{1,3}){3}\b/g;
11290
+ var WIN_PATH_RE = /[A-Za-z]:\\[^\s'"]+/g;
11291
+ var UNIX_PATH_RE = /(?:\/[\w.-]+){2,}/g;
11292
+ function redactForStorage(raw) {
11293
+ 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 ");
11294
+ return tokenize(redacted).join(" ");
11207
11295
  }
11208
-
11209
- // src/analysis/NaiveBayesClassifier.ts
11210
- function classify(model, text) {
11211
- const tokens = tokenize(text);
11212
- const vocabTokens = tokens.filter(
11213
- (tok) => model.labels.some((l) => model.logLikelihoods[l]?.[tok] !== void 0)
11214
- );
11215
- if (vocabTokens.length === 0) {
11216
- return { label: "unknown", margin: 0 };
11217
- }
11218
- const scores = {};
11219
- for (const label of model.labels) {
11220
- let s = model.priors[label];
11221
- const oov = Math.log(model.alpha / (model.totals[label] + model.alpha * model.vocabSize));
11222
- const ll = model.logLikelihoods[label];
11223
- for (const tok of vocabTokens) {
11224
- s += ll[tok] ?? oov;
11225
- }
11226
- scores[label] = s;
11227
- }
11228
- const ranked = Object.entries(scores).sort((a, b) => b[1] - a[1]);
11229
- const topLog = ranked[0][1];
11230
- const secondLog = ranked[1]?.[1] ?? topLog;
11231
- const margin = (topLog - secondLog) / vocabTokens.length;
11232
- return { label: ranked[0][0], margin };
11296
+ function localInferenceTokens(message) {
11297
+ return redactForStorage(message).split(" ").filter(Boolean);
11233
11298
  }
11234
11299
 
11235
11300
  // src/analysis/constants.ts
@@ -11258,7 +11323,7 @@ function errorHeader(message) {
11258
11323
  return beforeCallLog.split("\n").filter((line) => !/^\s*[-+]?\s*(?:Expected|Received)\b/i.test(line)).join("\n");
11259
11324
  }
11260
11325
  function isAssertionTimeout(message) {
11261
- return /Expect "[^"]+" with timeout/.test(message) && /resolved to [1-9]\d* element/.test(message);
11326
+ return /(?:Expect "[^"]+"|expect\.\w+) with timeout/.test(message) && /resolved to [1-9]\d* element/.test(message);
11262
11327
  }
11263
11328
  function isNetwork(message) {
11264
11329
  return /net::ERR_(CONNECTION|NAME_NOT_RESOLVED|INTERNET_DISCONNECTED|SSL|TIMED_OUT|ADDRESS|NETWORK_CHANGED|EMPTY_RESPONSE)|\bECONN(?:REFUSED|RESET)\b|\bENOTFOUND\b|\bEAI_AGAIN\b|getaddrinfo|socket hang up|fetch failed/.test(errorHeader(message));
@@ -11295,26 +11360,37 @@ function topFrame(stack) {
11295
11360
  function representativeMessage(message) {
11296
11361
  return clampWithEllipsis(message.split("\n")[0], MAX_MESSAGE_CHARS);
11297
11362
  }
11298
- function classifyOne(f, model) {
11363
+ function classifyOne(f, chain) {
11299
11364
  const frame = topFrame(f.error.stack);
11300
11365
  if (f.retryHistory.some((r) => r.status === "passed")) {
11301
- return { category: "flaky-by-retry", margin: OVERRIDE_MARGIN, failure: f, frame };
11366
+ return { category: "flaky-by-retry", margin: OVERRIDE_MARGIN, strength: "strong", source: "retry", failure: f, frame };
11302
11367
  }
11303
11368
  for (const [match, category] of MESSAGE_RULES) {
11304
- if (match(f.error.message)) return { category, margin: OVERRIDE_MARGIN, failure: f, frame };
11369
+ if (match(f.error.message)) {
11370
+ return { category, margin: OVERRIDE_MARGIN, strength: "strong", source: "rule", failure: f, frame };
11371
+ }
11372
+ }
11373
+ if (chain.local) {
11374
+ const { model, acceptThreshold, strongThreshold } = chain.local;
11375
+ const r = classifyTokens(model, localInferenceTokens(f.error.message));
11376
+ if (r.label !== "unknown" && r.margin >= acceptThreshold) {
11377
+ const strength = strongThreshold !== null && r.margin >= strongThreshold ? "strong" : "moderate";
11378
+ return { category: r.label, margin: r.margin, strength, source: "local", failure: f, frame };
11379
+ }
11305
11380
  }
11306
- const { label, margin } = classify(model, classifyInput(f));
11307
- return { category: label, margin, failure: f, frame };
11381
+ const { label, margin } = classify(chain.base, classifyInput(f));
11382
+ return { category: label, margin, strength: strengthOf(margin), source: "base", failure: f, frame };
11308
11383
  }
11309
- function classifyAll(failures, model) {
11310
- return failures.map((f) => classifyOne(f, model));
11384
+ function classifyAll(failures, chain) {
11385
+ return failures.map((f) => classifyOne(f, chain));
11311
11386
  }
11312
11387
  function perTestClassifications(classified) {
11313
11388
  return classified.map((c) => ({
11314
11389
  testId: c.failure.testId,
11315
11390
  category: c.category,
11316
11391
  message: representativeMessage(c.failure.error.message),
11317
- strength: strengthOf(c.margin)
11392
+ strength: c.strength
11393
+ // resolved at classification time, per producing source
11318
11394
  }));
11319
11395
  }
11320
11396
  function clusterClassified(classified) {
@@ -11325,13 +11401,16 @@ function clusterClassified(classified) {
11325
11401
  if (bucket) bucket.push(c);
11326
11402
  else groups.set(key, [c]);
11327
11403
  }
11404
+ const rank = Object.fromEntries(
11405
+ STRENGTH_LEVELS.map((level, i) => [level, i])
11406
+ );
11328
11407
  const clusters = [];
11329
11408
  for (const members of groups.values()) {
11330
- const margin = Math.max(...members.map((m) => m.margin));
11409
+ const strongest = members.reduce((a, b) => rank[b.strength] > rank[a.strength] ? b : a);
11331
11410
  clusters.push({
11332
11411
  category: members[0].category,
11333
- strength: strengthOf(margin),
11334
- margin,
11412
+ strength: strongest.strength,
11413
+ margin: Math.max(...members.map((m) => m.margin)),
11335
11414
  count: members.length,
11336
11415
  tests: members.slice(0, MAX_TESTS_PER_CLUSTER).map((m) => m.failure.testTitle),
11337
11416
  testsTotal: members.length,
@@ -11407,82 +11486,133 @@ function buildBriefSentence(stats, analysis, trend) {
11407
11486
 
11408
11487
  // src/analysis/UnclassifiedCollector.ts
11409
11488
  init_cjs_shims();
11410
- var import_node_fs2 = __toESM(require("fs"));
11411
- var import_node_os2 = __toESM(require("os"));
11412
- var import_node_path2 = __toESM(require("path"));
11489
+ var import_node_fs4 = __toESM(require("fs"));
11490
+ var import_node_os4 = __toESM(require("os"));
11491
+ var import_node_path4 = __toESM(require("path"));
11413
11492
 
11414
11493
  // src/analysis/feedback-csv.ts
11415
11494
  init_cjs_shims();
11416
- var FEEDBACK_HEADER = "category,margin,text";
11495
+ var FEEDBACK_HEADER = "category,margin,label,text";
11496
+ var LEGACY_FEEDBACK_HEADER = "category,margin,text";
11417
11497
  function parseFeedbackRows(content) {
11418
- return content.trim().split("\n").slice(1).filter(Boolean).map((line) => {
11498
+ const lines = content.trim().split("\n").filter(Boolean);
11499
+ if (lines.length === 0) return [];
11500
+ const header = lines[0].trim();
11501
+ const isV2 = header === FEEDBACK_HEADER;
11502
+ if (!isV2 && header !== LEGACY_FEEDBACK_HEADER) return [];
11503
+ const minParts = isV2 ? 4 : 3;
11504
+ const rows = [];
11505
+ for (const line of lines.slice(1)) {
11419
11506
  const parts = line.split(",");
11420
- return { category: parts[0], margin: parts[1], text: parts.slice(2).join(","), raw: line };
11421
- });
11507
+ if (parts.length < minParts) continue;
11508
+ const row = isV2 ? { category: parts[0], margin: parts[1], label: parts[2], text: parts.slice(3).join(",") } : { category: parts[0], margin: parts[1], label: "", text: parts.slice(2).join(",") };
11509
+ if (row.text.trim().length === 0) continue;
11510
+ rows.push(row);
11511
+ }
11512
+ return rows;
11513
+ }
11514
+ function serializeFeedbackRow(r) {
11515
+ const label = r.label.replace(/[,\r\n]/g, "").trim();
11516
+ return `${r.category},${r.margin},${label},${r.text}`;
11422
11517
  }
11423
11518
  function feedbackIdentity(category, text) {
11424
11519
  return `${category} ${text}`;
11425
11520
  }
11426
11521
 
11522
+ // src/analysis/FeedbackStore.ts
11523
+ init_cjs_shims();
11524
+ var import_node_fs3 = __toESM(require("fs"));
11525
+ var import_node_os3 = __toESM(require("os"));
11526
+ var import_node_path3 = __toESM(require("path"));
11527
+ var MARKER_FILENAME = "project.json";
11528
+ function writeProjectMarker(projectDir, marker) {
11529
+ try {
11530
+ atomicWrite(import_node_path3.default.join(projectDir, MARKER_FILENAME), JSON.stringify(marker));
11531
+ } catch {
11532
+ }
11533
+ }
11534
+ function rewriteFeedbackFile(file, rows) {
11535
+ const body = rows.map(serializeFeedbackRow).join("\n");
11536
+ atomicWrite(file, `${FEEDBACK_HEADER}
11537
+ ${body}${body ? "\n" : ""}`);
11538
+ }
11539
+ function atomicWrite(file, content) {
11540
+ import_node_fs3.default.mkdirSync(import_node_path3.default.dirname(file), { recursive: true });
11541
+ const tmp = `${file}.${process.pid}.tmp`;
11542
+ import_node_fs3.default.writeFileSync(tmp, content, "utf8");
11543
+ import_node_fs3.default.renameSync(tmp, file);
11544
+ }
11545
+
11427
11546
  // src/analysis/UnclassifiedCollector.ts
11428
11547
  var MAX_ROWS = 500;
11429
11548
  var FILENAME = "unclassified.csv";
11430
- var URI_RE = /[a-z][\w+.-]*:\/\/\S+/gi;
11431
- var EMAIL_RE = /\b[\w.+-]+@[\w.-]+\.\w+\b/g;
11432
- var SECRET_RE = /\b(?=[A-Za-z0-9_+/=-]*[0-9])(?=[A-Za-z0-9_+/=-]*[A-Za-z])[A-Za-z0-9_+/=-]{16,}\b/g;
11433
- var IP_RE = /\b\d{1,3}(?:\.\d{1,3}){3}\b/g;
11434
- var WIN_PATH_RE = /[A-Za-z]:\\[^\s'"]+/g;
11435
- var UNIX_PATH_RE = /(?:\/[\w.-]+){2,}/g;
11436
- function redactForStorage(raw) {
11437
- 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 ");
11438
- return tokenize(redacted).join(" ");
11439
- }
11440
- function shouldCollect(c) {
11441
- return c.category === "unknown" || c.margin <= STRENGTH.collectMaxMargin;
11549
+ function shouldCollect(c, scope) {
11550
+ if (c.source !== "base") return false;
11551
+ if (c.category === "unknown" || c.margin <= STRENGTH.collectMaxMargin) return true;
11552
+ return scope === "all";
11442
11553
  }
11443
11554
  function tildeify(p) {
11444
- const home = import_node_os2.default.homedir();
11555
+ const home = import_node_os4.default.homedir();
11445
11556
  return p.startsWith(home) ? `~${p.slice(home.length)}` : p;
11446
11557
  }
11447
- function collectUnclassified(classified, projectDir) {
11448
- 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);
11558
+ function collectUnclassified(classified, projectDir, opts = {}) {
11559
+ const scope = opts.scope ?? "blind-spots";
11560
+ const rows = classified.filter((c) => shouldCollect(c, scope)).map((c) => ({ text: redactForStorage(c.failure.error.message), category: c.category, margin: c.margin })).filter((r) => r.text.length > 0);
11449
11561
  if (rows.length === 0) return;
11450
- const file = import_node_path2.default.join(projectDir, FILENAME);
11451
- const firstTime = !import_node_fs2.default.existsSync(file);
11562
+ const file = import_node_path4.default.join(projectDir, FILENAME);
11563
+ const firstTime = !import_node_fs4.default.existsSync(file);
11452
11564
  const byIdentity = /* @__PURE__ */ new Map();
11453
11565
  if (!firstTime) {
11454
- for (const r of parseFeedbackRows(import_node_fs2.default.readFileSync(file, "utf8"))) {
11455
- byIdentity.set(feedbackIdentity(r.category, r.text), r.raw);
11566
+ for (const r of parseFeedbackRows(import_node_fs4.default.readFileSync(file, "utf8"))) {
11567
+ byIdentity.set(feedbackIdentity(r.category, r.text), r);
11456
11568
  }
11457
11569
  }
11458
- let added = false;
11459
11570
  for (const r of rows) {
11460
11571
  const id = feedbackIdentity(r.category, r.text);
11572
+ const label = byIdentity.get(id)?.label ?? "";
11461
11573
  byIdentity.delete(id);
11462
- byIdentity.set(id, `${r.category},${r.margin},${r.text}`);
11463
- added = true;
11464
- }
11465
- if (!added) return;
11466
- const all = [...byIdentity.values()].slice(-MAX_ROWS);
11467
- import_node_fs2.default.mkdirSync(projectDir, { recursive: true });
11468
- import_node_fs2.default.writeFileSync(file, `${FEEDBACK_HEADER}
11469
- ${all.join("\n")}
11470
- `, "utf8");
11574
+ byIdentity.set(id, { category: r.category, margin: String(r.margin), label, text: r.text });
11575
+ }
11576
+ const all = evictOldestUnlabeledFirst([...byIdentity.values()], MAX_ROWS);
11577
+ import_node_fs4.default.mkdirSync(projectDir, { recursive: true });
11578
+ rewriteFeedbackFile(file, all);
11579
+ if (opts.project) {
11580
+ writeProjectMarker(projectDir, { cwd: opts.project.cwd, outputFile: opts.project.outputFile, updatedAt: Date.now() });
11581
+ }
11471
11582
  if (firstTime) {
11472
11583
  logger.info(
11473
11584
  `Unrecognized failures collected locally at ${tildeify(file)} to improve offline classification. Local only - never sent anywhere. Disable: failureAnalysis.collectUnclassified=false`
11474
11585
  );
11475
11586
  }
11476
11587
  }
11588
+ function evictOldestUnlabeledFirst(rows, max) {
11589
+ const excess = rows.length - max;
11590
+ if (excess <= 0) return rows;
11591
+ let unlabeledToDrop = Math.min(excess, rows.filter((r) => r.label === "").length);
11592
+ let labeledToDrop = excess - unlabeledToDrop;
11593
+ const out = [];
11594
+ for (const r of rows) {
11595
+ if (r.label === "" && unlabeledToDrop > 0) {
11596
+ unlabeledToDrop--;
11597
+ continue;
11598
+ }
11599
+ if (r.label !== "" && labeledToDrop > 0) {
11600
+ labeledToDrop--;
11601
+ continue;
11602
+ }
11603
+ out.push(r);
11604
+ }
11605
+ return out;
11606
+ }
11477
11607
 
11478
11608
  // src/utils/paths.ts
11479
11609
  init_cjs_shims();
11480
- var import_node_os3 = __toESM(require("os"));
11481
- var import_node_path3 = __toESM(require("path"));
11610
+ var import_node_os5 = __toESM(require("os"));
11611
+ var import_node_path5 = __toESM(require("path"));
11482
11612
  var import_node_crypto2 = __toESM(require("crypto"));
11483
11613
  function resolveProjectDir(options, cwd) {
11484
11614
  const projectKey = import_node_crypto2.default.createHash("sha256").update(cwd + (options.outputFile ?? "")).digest("hex").slice(0, 8);
11485
- return import_node_path3.default.join(import_node_os3.default.homedir(), ".reportforge", projectKey);
11615
+ return import_node_path5.default.join(import_node_os5.default.homedir(), ".reportforge", projectKey);
11486
11616
  }
11487
11617
 
11488
11618
  // src/analysis/index.ts
@@ -11517,16 +11647,36 @@ function runFailureAnalysis(reportData, options, cwd) {
11517
11647
  if (reportData.stats.failed === 0 || !fa.enabled) return;
11518
11648
  const cap = fa.maxFailuresToAnalyse;
11519
11649
  const analysed = reportData.failures.slice(0, cap);
11520
- const model = loadModel(fa.autoUpdateModel);
11521
- const classified = classifyAll(analysed, model);
11650
+ const base = loadModel(fa.autoUpdateModel);
11651
+ const local = fa.localModel ? loadLocalModel(fa.localModelPath) ?? void 0 : void 0;
11652
+ if (local) {
11653
+ logger.debug(
11654
+ `local model active (trained ${local.meta.trainedAt}, ${local.meta.labeledCount} labeled rows, accept margin >= ${local.acceptThreshold})`
11655
+ );
11656
+ if (local.meta.baseVersionAtEval !== base.version) {
11657
+ logger.debug(
11658
+ `local model was gate-checked against base v${local.meta.baseVersionAtEval} but base v${base.version} is active - re-run train-model to re-validate against the current base`
11659
+ );
11660
+ }
11661
+ }
11662
+ const classified = classifyAll(analysed, { base, local });
11663
+ logger.debug(`failure analysis: ${classified.length} classified - ${sourceTally(classified)}`);
11522
11664
  reportData.classifications = perTestClassifications(classified);
11523
11665
  if (templatesConsumeAnalysis(options)) {
11524
11666
  reportData.analysis = analyseClassified(classified, reportData.failures.length, fa);
11525
11667
  }
11526
11668
  if (fa.collectUnclassified) {
11527
- collectUnclassified(classified, resolveProjectDir(options, cwd));
11669
+ collectUnclassified(classified, resolveProjectDir(options, cwd), {
11670
+ scope: fa.collectScope,
11671
+ project: { cwd, outputFile: options.outputFile }
11672
+ });
11528
11673
  }
11529
11674
  }
11675
+ function sourceTally(classified) {
11676
+ const counts = /* @__PURE__ */ new Map();
11677
+ for (const c of classified) counts.set(c.source, (counts.get(c.source) ?? 0) + 1);
11678
+ return ["rule", "retry", "local", "base"].filter((s) => counts.has(s)).map((s) => `${counts.get(s)} ${s}`).join(" / ");
11679
+ }
11530
11680
 
11531
11681
  // src/collector/DataCollector.ts
11532
11682
  init_cjs_shims();
@@ -11802,8 +11952,8 @@ function buildSuiteResults(projects, limit) {
11802
11952
  const top = projects.flatMap((p) => p.suites);
11803
11953
  if (top.length <= 1 && top[0] && top[0].suites.length > 0) {
11804
11954
  const file = top[0];
11805
- const fs14 = statsFromTests(file.tests);
11806
- const rootRow = file.tests.length > 0 ? [{ label: file.title, passed: fs14.passed, failed: fs14.failed, timedOut: fs14.timedOut, skipped: fs14.skipped }] : [];
11955
+ const fs16 = statsFromTests(file.tests);
11956
+ const rootRow = file.tests.length > 0 ? [{ label: file.title, passed: fs16.passed, failed: fs16.failed, timedOut: fs16.timedOut, skipped: fs16.skipped }] : [];
11807
11957
  return capRows([...rootRow, ...file.suites.map(toRow)], limit);
11808
11958
  }
11809
11959
  return capRows(top.map(toRow), limit);
@@ -12863,21 +13013,26 @@ var ScreenshotEmbedder = class {
12863
13013
  init_cjs_shims();
12864
13014
  var import_child_process2 = require("child_process");
12865
13015
  var import_fs6 = __toESM(require("fs"));
13016
+ var LAUNCH_ARGS = [
13017
+ "--no-sandbox",
13018
+ "--disable-setuid-sandbox",
13019
+ "--disable-dev-shm-usage",
13020
+ "--disable-gpu",
13021
+ // Chrome's new headless mode (--headless=new, the only mode left in
13022
+ // Chrome 132+) briefly uncloaks a blank window on some Windows/GPU
13023
+ // combinations. Parking the window far off-screen makes that transient
13024
+ // invisible; harmless everywhere else and does not affect PDF output.
13025
+ "--window-position=-32000,-32000"
13026
+ ];
12866
13027
  var BrowserManager = class {
12867
13028
  async launch(executablePath) {
12868
13029
  const puppeteer = require("puppeteer-core");
12869
13030
  const chromePath = this.resolveChromePath(executablePath, puppeteer);
12870
13031
  logger.info(`Launching browser: ${chromePath ?? "puppeteer default"}`);
12871
- const launchArgs = [
12872
- "--no-sandbox",
12873
- "--disable-setuid-sandbox",
12874
- "--disable-dev-shm-usage",
12875
- "--disable-gpu"
12876
- ];
12877
13032
  this.browser = await puppeteer.launch({
12878
13033
  executablePath: chromePath,
12879
13034
  headless: true,
12880
- args: launchArgs
13035
+ args: LAUNCH_ARGS
12881
13036
  });
12882
13037
  const page = await this.browser.newPage();
12883
13038
  return { browser: this.browser, page };
@@ -14199,7 +14354,7 @@ var PdfReporter = class {
14199
14354
  this.options = parseOptions(rawOptions);
14200
14355
  setLogLevel(this.options.logLevel);
14201
14356
  this.dataCollector = new DataCollector(this.options.capture);
14202
- this.version = true ? "0.19.0" : "0.x";
14357
+ this.version = true ? "0.20.0" : "0.x";
14203
14358
  logger.info(`@reportforge/playwright-pdf v${this.version} initialised`);
14204
14359
  this.licenseClient = new LicenseClient({
14205
14360
  licenseKey: this.options.licenseKey,