@reportforge/playwright-pdf 0.3.1 → 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/README.md +3 -0
- package/dist/cli/export-feedback.js +87 -0
- package/dist/demo/cli.js +413 -59
- package/dist/index.d.mts +26 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.js +444 -64
- package/dist/index.mjs +521 -141
- package/dist/templates/layouts/detailed.hbs +2 -0
- package/dist/templates/layouts/executive.hbs +2 -0
- package/dist/templates/partials/analysis-oneliner.hbs +3 -0
- package/dist/templates/partials/failure-analysis.hbs +42 -0
- package/dist/templates/styles/base.css +3 -0
- package/dist/templates/styles/detailed.css +21 -0
- package/package.json +8 -3
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(
|
|
1233
|
-
return /^\.|this\b/.test(
|
|
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(
|
|
1238
|
-
return
|
|
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(
|
|
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:
|
|
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),
|
|
2637
|
+
var params = this.setupFullMustacheParams(decorator, program, void 0), path15 = decorator.path;
|
|
2638
2638
|
this.useDecorators = true;
|
|
2639
|
-
this.opcode("registerDecorator", params.length,
|
|
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
|
|
2704
|
-
this.opcode("getContext",
|
|
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
|
-
|
|
2708
|
-
this.accept(
|
|
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
|
|
2713
|
-
|
|
2714
|
-
this.accept(
|
|
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),
|
|
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
|
-
|
|
2725
|
-
|
|
2726
|
-
this.accept(
|
|
2727
|
-
this.opcode("invokeHelper", params.length,
|
|
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(
|
|
2731
|
-
this.addDepth(
|
|
2732
|
-
this.opcode("getContext",
|
|
2733
|
-
var 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,
|
|
2735
|
+
this.opcode("lookupBlockParam", blockParamId, path15.parts);
|
|
2736
2736
|
} else if (!name) {
|
|
2737
2737
|
this.opcode("pushContext");
|
|
2738
|
-
} else if (
|
|
2738
|
+
} else if (path15.data) {
|
|
2739
2739
|
this.options.data = true;
|
|
2740
|
-
this.opcode("lookupData",
|
|
2740
|
+
this.opcode("lookupData", path15.depth, path15.parts, path15.strict);
|
|
2741
2741
|
} else {
|
|
2742
|
-
this.opcode("lookupOnContext",
|
|
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
|
|
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
|
-
|
|
3104
|
+
path15 = url.path;
|
|
3105
3105
|
}
|
|
3106
|
-
var isAbsolute = exports.isAbsolute(
|
|
3107
|
-
var parts =
|
|
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
|
-
|
|
3125
|
-
if (
|
|
3126
|
-
|
|
3124
|
+
path15 = parts.join("/");
|
|
3125
|
+
if (path15 === "") {
|
|
3126
|
+
path15 = isAbsolute ? "/" : ".";
|
|
3127
3127
|
}
|
|
3128
3128
|
if (url) {
|
|
3129
|
-
url.path =
|
|
3129
|
+
url.path = path15;
|
|
3130
3130
|
return urlGenerate(url);
|
|
3131
3131
|
}
|
|
3132
|
-
return
|
|
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
|
|
5936
|
-
return (id.data ? "@" : "") + "PATH:" +
|
|
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
|
|
5978
|
-
var templateString =
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
6128
|
-
var
|
|
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
|
-
|
|
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 (
|
|
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:
|
|
6754
|
-
const fullPath = [...
|
|
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,
|
|
6874
|
+
constructor(parent, value, path15, key) {
|
|
6875
6875
|
this._cachedPath = [];
|
|
6876
6876
|
this.parent = parent;
|
|
6877
6877
|
this.data = value;
|
|
6878
|
-
this._path =
|
|
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,6 +10838,9 @@ 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",
|
|
@@ -10773,13 +10877,15 @@ var LicenseClient = class {
|
|
|
10773
10877
|
return this.post("/api/license/activate", {
|
|
10774
10878
|
licenseKey,
|
|
10775
10879
|
fingerprint: this.fingerprint.hash,
|
|
10776
|
-
label: this.fingerprint.label
|
|
10880
|
+
label: this.fingerprint.label,
|
|
10881
|
+
modelVersion: this.modelVersionForServer
|
|
10777
10882
|
});
|
|
10778
10883
|
}
|
|
10779
10884
|
async refresh(jwt) {
|
|
10780
10885
|
return this.post("/api/license/refresh", {
|
|
10781
10886
|
jwt,
|
|
10782
|
-
label: this.fingerprint.label
|
|
10887
|
+
label: this.fingerprint.label,
|
|
10888
|
+
modelVersion: this.modelVersionForServer
|
|
10783
10889
|
});
|
|
10784
10890
|
}
|
|
10785
10891
|
async post(route, body) {
|
|
@@ -10837,6 +10943,276 @@ function normalizeUrl(url) {
|
|
|
10837
10943
|
return url.endsWith("/") ? url.slice(0, -1) : url;
|
|
10838
10944
|
}
|
|
10839
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
|
+
|
|
10840
11216
|
// src/collector/DataCollector.ts
|
|
10841
11217
|
init_esm_shims();
|
|
10842
11218
|
|
|
@@ -11459,14 +11835,14 @@ var ShardMerger = class {
|
|
|
11459
11835
|
|
|
11460
11836
|
// src/pdf/PdfGenerator.ts
|
|
11461
11837
|
init_esm_shims();
|
|
11462
|
-
import
|
|
11463
|
-
import
|
|
11838
|
+
import fs12 from "fs";
|
|
11839
|
+
import path12 from "path";
|
|
11464
11840
|
|
|
11465
11841
|
// src/templates/engine.ts
|
|
11466
11842
|
init_esm_shims();
|
|
11467
11843
|
var import_handlebars = __toESM(require_lib());
|
|
11468
|
-
import
|
|
11469
|
-
import
|
|
11844
|
+
import fs5 from "fs";
|
|
11845
|
+
import path7 from "path";
|
|
11470
11846
|
|
|
11471
11847
|
// src/utils/duration.ts
|
|
11472
11848
|
init_esm_shims();
|
|
@@ -11486,9 +11862,9 @@ var TemplateEngine = class {
|
|
|
11486
11862
|
this.fontsBundleCss = "";
|
|
11487
11863
|
this.customTemplateCache = /* @__PURE__ */ new Map();
|
|
11488
11864
|
this.handlebars = import_handlebars.default.create();
|
|
11489
|
-
this.templatesDir =
|
|
11490
|
-
if (!
|
|
11491
|
-
this.templatesDir =
|
|
11865
|
+
this.templatesDir = path7.resolve(__dirname, "templates");
|
|
11866
|
+
if (!fs5.existsSync(this.templatesDir)) {
|
|
11867
|
+
this.templatesDir = path7.resolve(__dirname, "../templates");
|
|
11492
11868
|
}
|
|
11493
11869
|
this.registerHelpers();
|
|
11494
11870
|
this.registerPartials();
|
|
@@ -11505,21 +11881,21 @@ var TemplateEngine = class {
|
|
|
11505
11881
|
this.chartjsInline = inline;
|
|
11506
11882
|
}
|
|
11507
11883
|
render(data, template) {
|
|
11508
|
-
const layoutPath =
|
|
11509
|
-
if (!
|
|
11884
|
+
const layoutPath = path7.join(this.templatesDir, "layouts", `${template}.hbs`);
|
|
11885
|
+
if (!fs5.existsSync(layoutPath)) {
|
|
11510
11886
|
throw new Error(`[reportforge] Template layout not found: ${layoutPath}`);
|
|
11511
11887
|
}
|
|
11512
|
-
const layoutSource =
|
|
11888
|
+
const layoutSource = fs5.readFileSync(layoutPath, "utf8");
|
|
11513
11889
|
const compiledLayout = this.handlebars.compile(layoutSource);
|
|
11514
11890
|
const ctx = this.buildContext(data, template);
|
|
11515
11891
|
return compiledLayout(ctx);
|
|
11516
11892
|
}
|
|
11517
11893
|
renderFromPath(data, filePath) {
|
|
11518
11894
|
if (!this.customTemplateCache.has(filePath)) {
|
|
11519
|
-
if (!
|
|
11895
|
+
if (!fs5.existsSync(filePath)) {
|
|
11520
11896
|
throw new Error(`[reportforge] Custom template not found: ${filePath}`);
|
|
11521
11897
|
}
|
|
11522
|
-
const source =
|
|
11898
|
+
const source = fs5.readFileSync(filePath, "utf8");
|
|
11523
11899
|
this.customTemplateCache.set(filePath, this.handlebars.compile(source));
|
|
11524
11900
|
}
|
|
11525
11901
|
const compiledFn = this.customTemplateCache.get(filePath);
|
|
@@ -11632,17 +12008,17 @@ var TemplateEngine = class {
|
|
|
11632
12008
|
return tests.sort((a, b) => b.durationMs - a.durationMs).slice(0, 10);
|
|
11633
12009
|
}
|
|
11634
12010
|
readStyle(filename) {
|
|
11635
|
-
const stylePath =
|
|
11636
|
-
if (!
|
|
11637
|
-
return
|
|
12011
|
+
const stylePath = path7.join(this.templatesDir, "styles", filename);
|
|
12012
|
+
if (!fs5.existsSync(stylePath)) return "";
|
|
12013
|
+
return fs5.readFileSync(stylePath, "utf8");
|
|
11638
12014
|
}
|
|
11639
12015
|
registerPartials() {
|
|
11640
|
-
const partialsDir =
|
|
11641
|
-
if (!
|
|
11642
|
-
for (const file of
|
|
12016
|
+
const partialsDir = path7.join(this.templatesDir, "partials");
|
|
12017
|
+
if (!fs5.existsSync(partialsDir)) return;
|
|
12018
|
+
for (const file of fs5.readdirSync(partialsDir)) {
|
|
11643
12019
|
if (!file.endsWith(".hbs")) continue;
|
|
11644
|
-
const name =
|
|
11645
|
-
const src =
|
|
12020
|
+
const name = path7.basename(file, ".hbs");
|
|
12021
|
+
const src = fs5.readFileSync(path7.join(partialsDir, file), "utf8");
|
|
11646
12022
|
this.handlebars.registerPartial(name, src);
|
|
11647
12023
|
}
|
|
11648
12024
|
}
|
|
@@ -11705,6 +12081,10 @@ var TemplateEngine = class {
|
|
|
11705
12081
|
);
|
|
11706
12082
|
this.handlebars.registerHelper("lt", (a, b) => a < b);
|
|
11707
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));
|
|
11708
12088
|
this.handlebars.registerHelper("statusBadge", (status) => {
|
|
11709
12089
|
const map = {
|
|
11710
12090
|
passed: "badge--passed",
|
|
@@ -11726,7 +12106,7 @@ var TemplateEngine = class {
|
|
|
11726
12106
|
|
|
11727
12107
|
// src/screenshots/ScreenshotEmbedder.ts
|
|
11728
12108
|
init_esm_shims();
|
|
11729
|
-
import
|
|
12109
|
+
import fs6 from "fs";
|
|
11730
12110
|
import sharp from "sharp";
|
|
11731
12111
|
var ScreenshotEmbedder = class {
|
|
11732
12112
|
async embedAll(failures, opts) {
|
|
@@ -11735,7 +12115,7 @@ var ScreenshotEmbedder = class {
|
|
|
11735
12115
|
async embed(failure, opts) {
|
|
11736
12116
|
if (!failure.screenshotPath && !failure.screenshotBuffer) return;
|
|
11737
12117
|
try {
|
|
11738
|
-
const raw = failure.screenshotBuffer ?? await
|
|
12118
|
+
const raw = failure.screenshotBuffer ?? await fs6.promises.readFile(failure.screenshotPath);
|
|
11739
12119
|
const { buffer, mime } = opts.reencode ? await this.recompress(raw, opts) : { buffer: raw, mime: this.detectMimeType(raw) };
|
|
11740
12120
|
failure.screenshotBase64 = `data:${mime};base64,${buffer.toString("base64")}`;
|
|
11741
12121
|
} catch (e) {
|
|
@@ -11747,7 +12127,7 @@ var ScreenshotEmbedder = class {
|
|
|
11747
12127
|
}
|
|
11748
12128
|
async embedLogoFile(logoPath) {
|
|
11749
12129
|
try {
|
|
11750
|
-
const buffer = await
|
|
12130
|
+
const buffer = await fs6.promises.readFile(logoPath);
|
|
11751
12131
|
const ext = logoPath.split(".").pop()?.toLowerCase();
|
|
11752
12132
|
let mime = "image/png";
|
|
11753
12133
|
if (ext === "jpg" || ext === "jpeg") mime = "image/jpeg";
|
|
@@ -11792,7 +12172,7 @@ var ScreenshotEmbedder = class {
|
|
|
11792
12172
|
// src/pdf/BrowserManager.ts
|
|
11793
12173
|
init_esm_shims();
|
|
11794
12174
|
import { execSync as execSync2 } from "child_process";
|
|
11795
|
-
import
|
|
12175
|
+
import fs7 from "fs";
|
|
11796
12176
|
var BrowserManager = class {
|
|
11797
12177
|
async launch(executablePath) {
|
|
11798
12178
|
const puppeteer = __require("puppeteer-core");
|
|
@@ -11820,13 +12200,13 @@ var BrowserManager = class {
|
|
|
11820
12200
|
}
|
|
11821
12201
|
}
|
|
11822
12202
|
resolveChromePath(explicit, puppeteer) {
|
|
11823
|
-
if (explicit &&
|
|
12203
|
+
if (explicit && fs7.existsSync(explicit)) return explicit;
|
|
11824
12204
|
const envPath = process.env.PUPPETEER_EXECUTABLE_PATH;
|
|
11825
|
-
if (envPath &&
|
|
12205
|
+
if (envPath && fs7.existsSync(envPath)) return envPath;
|
|
11826
12206
|
try {
|
|
11827
12207
|
const chromeFinder = require_lib2();
|
|
11828
12208
|
const found = chromeFinder();
|
|
11829
|
-
if (found &&
|
|
12209
|
+
if (found && fs7.existsSync(found)) return found;
|
|
11830
12210
|
} catch {
|
|
11831
12211
|
}
|
|
11832
12212
|
const linuxPaths = [
|
|
@@ -11837,19 +12217,19 @@ var BrowserManager = class {
|
|
|
11837
12217
|
"/snap/bin/chromium"
|
|
11838
12218
|
];
|
|
11839
12219
|
for (const p of linuxPaths) {
|
|
11840
|
-
if (
|
|
12220
|
+
if (fs7.existsSync(p)) return p;
|
|
11841
12221
|
}
|
|
11842
12222
|
try {
|
|
11843
12223
|
const found = execSync2("which google-chrome chromium chromium-browser 2>/dev/null", {
|
|
11844
12224
|
stdio: "pipe"
|
|
11845
12225
|
}).toString().trim().split("\n")[0];
|
|
11846
|
-
if (found &&
|
|
12226
|
+
if (found && fs7.existsSync(found)) return found;
|
|
11847
12227
|
} catch {
|
|
11848
12228
|
}
|
|
11849
12229
|
if (puppeteer.executablePath) {
|
|
11850
12230
|
try {
|
|
11851
12231
|
const bundled = puppeteer.executablePath();
|
|
11852
|
-
if (bundled &&
|
|
12232
|
+
if (bundled && fs7.existsSync(bundled)) return bundled;
|
|
11853
12233
|
} catch {
|
|
11854
12234
|
}
|
|
11855
12235
|
}
|
|
@@ -11862,21 +12242,21 @@ var BrowserManager = class {
|
|
|
11862
12242
|
|
|
11863
12243
|
// src/pdf/HtmlWriter.ts
|
|
11864
12244
|
init_esm_shims();
|
|
11865
|
-
import
|
|
11866
|
-
import
|
|
11867
|
-
import
|
|
11868
|
-
import
|
|
12245
|
+
import fs8 from "fs";
|
|
12246
|
+
import os7 from "os";
|
|
12247
|
+
import path8 from "path";
|
|
12248
|
+
import crypto2 from "crypto";
|
|
11869
12249
|
var HtmlWriter = class {
|
|
11870
12250
|
async write(html) {
|
|
11871
|
-
const tmpDir =
|
|
11872
|
-
const filename = `rf-report-${
|
|
11873
|
-
this.tempPath =
|
|
11874
|
-
await
|
|
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");
|
|
11875
12255
|
return this.tempPath;
|
|
11876
12256
|
}
|
|
11877
12257
|
async cleanup() {
|
|
11878
12258
|
if (this.tempPath) {
|
|
11879
|
-
await
|
|
12259
|
+
await fs8.promises.unlink(this.tempPath).catch(() => {
|
|
11880
12260
|
});
|
|
11881
12261
|
this.tempPath = void 0;
|
|
11882
12262
|
}
|
|
@@ -11910,10 +12290,10 @@ var ChartRenderer = class {
|
|
|
11910
12290
|
// src/pdf/PdfEncryptor.ts
|
|
11911
12291
|
init_esm_shims();
|
|
11912
12292
|
import { execFileSync } from "child_process";
|
|
11913
|
-
import
|
|
11914
|
-
import
|
|
11915
|
-
import
|
|
11916
|
-
import
|
|
12293
|
+
import fs9 from "fs";
|
|
12294
|
+
import path9 from "path";
|
|
12295
|
+
import os8 from "os";
|
|
12296
|
+
import crypto3 from "crypto";
|
|
11917
12297
|
var PdfEncryptor = class {
|
|
11918
12298
|
async encrypt(pdfPath, password) {
|
|
11919
12299
|
if (!password) return;
|
|
@@ -11923,10 +12303,10 @@ var PdfEncryptor = class {
|
|
|
11923
12303
|
);
|
|
11924
12304
|
return;
|
|
11925
12305
|
}
|
|
11926
|
-
const tmpPath =
|
|
11927
|
-
const pwFile =
|
|
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")}`);
|
|
11928
12308
|
try {
|
|
11929
|
-
|
|
12309
|
+
fs9.writeFileSync(pwFile, `${password}
|
|
11930
12310
|
${password}`, { mode: 384 });
|
|
11931
12311
|
execFileSync("qpdf", [
|
|
11932
12312
|
`--password-file=${pwFile}`,
|
|
@@ -11936,13 +12316,13 @@ ${password}`, { mode: 384 });
|
|
|
11936
12316
|
pdfPath,
|
|
11937
12317
|
tmpPath
|
|
11938
12318
|
], { stdio: "pipe" });
|
|
11939
|
-
|
|
12319
|
+
fs9.renameSync(tmpPath, pdfPath);
|
|
11940
12320
|
logger.info(`PDF encrypted with password.`);
|
|
11941
12321
|
} catch (e) {
|
|
11942
12322
|
logger.warn(`qpdf encryption failed: ${e.message}`);
|
|
11943
12323
|
} finally {
|
|
11944
|
-
if (
|
|
11945
|
-
|
|
12324
|
+
if (fs9.existsSync(tmpPath)) fs9.unlinkSync(tmpPath);
|
|
12325
|
+
fs9.rmSync(pwFile, { force: true });
|
|
11946
12326
|
}
|
|
11947
12327
|
}
|
|
11948
12328
|
isQpdfAvailable() {
|
|
@@ -11957,8 +12337,8 @@ ${password}`, { mode: 384 });
|
|
|
11957
12337
|
|
|
11958
12338
|
// src/filename/FilenameResolver.ts
|
|
11959
12339
|
init_esm_shims();
|
|
11960
|
-
import
|
|
11961
|
-
import
|
|
12340
|
+
import path10 from "path";
|
|
12341
|
+
import fs10 from "fs";
|
|
11962
12342
|
var MAX_BRANCH_LENGTH = 50;
|
|
11963
12343
|
var FilenameResolver = class {
|
|
11964
12344
|
/**
|
|
@@ -11975,8 +12355,8 @@ var FilenameResolver = class {
|
|
|
11975
12355
|
const branch = this.sanitiseBranch(ctx.branch);
|
|
11976
12356
|
const status = ctx.status;
|
|
11977
12357
|
const resolved = template.replace(/\{date\}/g, date).replace(/\{branch\}/g, branch).replace(/\{status\}/g, status);
|
|
11978
|
-
const absolute =
|
|
11979
|
-
|
|
12358
|
+
const absolute = path10.isAbsolute(resolved) ? resolved : path10.resolve(process.cwd(), resolved);
|
|
12359
|
+
fs10.mkdirSync(path10.dirname(absolute), { recursive: true });
|
|
11980
12360
|
return absolute;
|
|
11981
12361
|
}
|
|
11982
12362
|
/**
|
|
@@ -11988,7 +12368,7 @@ var FilenameResolver = class {
|
|
|
11988
12368
|
* // → '/out/2026-report-executive.pdf'
|
|
11989
12369
|
*/
|
|
11990
12370
|
injectTemplateSuffix(outputPath, suffix) {
|
|
11991
|
-
const ext =
|
|
12371
|
+
const ext = path10.extname(outputPath);
|
|
11992
12372
|
const base = outputPath.slice(0, outputPath.length - ext.length);
|
|
11993
12373
|
return `${base}-${suffix}${ext}`;
|
|
11994
12374
|
}
|
|
@@ -12046,8 +12426,8 @@ var PRESETS = {
|
|
|
12046
12426
|
|
|
12047
12427
|
// src/pdf/FailurePaginator.ts
|
|
12048
12428
|
init_esm_shims();
|
|
12049
|
-
import
|
|
12050
|
-
import
|
|
12429
|
+
import fs11 from "fs";
|
|
12430
|
+
import path11 from "path";
|
|
12051
12431
|
var FailurePaginator = class {
|
|
12052
12432
|
async paginate(failures, maxInline, pdfOutputPath) {
|
|
12053
12433
|
if (failures.length <= maxInline) {
|
|
@@ -12057,13 +12437,13 @@ var FailurePaginator = class {
|
|
|
12057
12437
|
const overflow = failures.slice(maxInline);
|
|
12058
12438
|
const sidecarPath = this.sidecarPathFor(pdfOutputPath);
|
|
12059
12439
|
try {
|
|
12060
|
-
await
|
|
12440
|
+
await fs11.promises.writeFile(
|
|
12061
12441
|
sidecarPath,
|
|
12062
12442
|
JSON.stringify(this.serialize(overflow), null, 2),
|
|
12063
12443
|
"utf8"
|
|
12064
12444
|
);
|
|
12065
12445
|
logger.info(
|
|
12066
|
-
`Emitted ${overflow.length} overflow failures \u2192 ${
|
|
12446
|
+
`Emitted ${overflow.length} overflow failures \u2192 ${path11.basename(sidecarPath)}`
|
|
12067
12447
|
);
|
|
12068
12448
|
} catch (e) {
|
|
12069
12449
|
logger.warn(`Could not write failure sidecar: ${e.message}`);
|
|
@@ -12072,9 +12452,9 @@ var FailurePaginator = class {
|
|
|
12072
12452
|
return { inline, overflowCount: overflow.length, sidecarPath };
|
|
12073
12453
|
}
|
|
12074
12454
|
sidecarPathFor(pdfPath) {
|
|
12075
|
-
const dir =
|
|
12076
|
-
const base =
|
|
12077
|
-
return
|
|
12455
|
+
const dir = path11.dirname(pdfPath);
|
|
12456
|
+
const base = path11.basename(pdfPath, path11.extname(pdfPath));
|
|
12457
|
+
return path11.join(dir, `${base}-failures.json`);
|
|
12078
12458
|
}
|
|
12079
12459
|
/**
|
|
12080
12460
|
* Drops the inlined image bytes — a 150-entry overflow would otherwise
|
|
@@ -12152,7 +12532,7 @@ var PdfGenerator = class {
|
|
|
12152
12532
|
const sources = this.resolveTemplateSources(options);
|
|
12153
12533
|
const isMulti = sources.length > 1;
|
|
12154
12534
|
for (const source of sources) {
|
|
12155
|
-
if (source.kind === "custom" && !
|
|
12535
|
+
if (source.kind === "custom" && !fs12.existsSync(source.absolutePath)) {
|
|
12156
12536
|
throw new Error(
|
|
12157
12537
|
`[reportforge] Custom template not found: ${source.absolutePath}`
|
|
12158
12538
|
);
|
|
@@ -12183,14 +12563,14 @@ var PdfGenerator = class {
|
|
|
12183
12563
|
await this.renderPdf(templateData, templateOptions, source, compression, outputPath, page);
|
|
12184
12564
|
const capBytes = (options.maxFileSizeMb ?? 0) * 1024 * 1024;
|
|
12185
12565
|
if (capBytes > 0) {
|
|
12186
|
-
const sizeBytes = (await
|
|
12566
|
+
const sizeBytes = (await fs12.promises.stat(outputPath)).size;
|
|
12187
12567
|
if (sizeBytes > capBytes) {
|
|
12188
12568
|
const tighter = this.tightenForCap(compression, sizeBytes, capBytes);
|
|
12189
12569
|
logger.warn(
|
|
12190
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}).`
|
|
12191
12571
|
);
|
|
12192
12572
|
await this.renderPdf(templateData, templateOptions, source, tighter, outputPath, page);
|
|
12193
|
-
const finalSize = (await
|
|
12573
|
+
const finalSize = (await fs12.promises.stat(outputPath)).size;
|
|
12194
12574
|
if (finalSize > capBytes) {
|
|
12195
12575
|
logger.warn(
|
|
12196
12576
|
`PDF still ${fmtMb(finalSize)} after retune. Consider lowering maxInlineFailures or splitting the report.`
|
|
@@ -12203,7 +12583,7 @@ var PdfGenerator = class {
|
|
|
12203
12583
|
if (options.pdfPassword) {
|
|
12204
12584
|
await this.pdfEncryptor.encrypt(outputPath, options.pdfPassword);
|
|
12205
12585
|
}
|
|
12206
|
-
const finalSizeMb = fmtMb((await
|
|
12586
|
+
const finalSizeMb = fmtMb((await fs12.promises.stat(outputPath)).size);
|
|
12207
12587
|
logger.info(`PDF report generated: ${outputPath} (${finalSizeMb})`);
|
|
12208
12588
|
outputPaths.push(outputPath);
|
|
12209
12589
|
}
|
|
@@ -12216,8 +12596,8 @@ var PdfGenerator = class {
|
|
|
12216
12596
|
if (options.templatePath && options.templatePath.length > 0) {
|
|
12217
12597
|
return [...new Set(options.templatePath)].map((p) => ({
|
|
12218
12598
|
kind: "custom",
|
|
12219
|
-
absolutePath:
|
|
12220
|
-
label:
|
|
12599
|
+
absolutePath: path12.isAbsolute(p) ? p : path12.resolve(process.cwd(), p),
|
|
12600
|
+
label: path12.basename(p, ".hbs")
|
|
12221
12601
|
}));
|
|
12222
12602
|
}
|
|
12223
12603
|
const rawIds = Array.isArray(options.template) ? options.template : [options.template];
|
|
@@ -12267,7 +12647,7 @@ var PdfGenerator = class {
|
|
|
12267
12647
|
...data,
|
|
12268
12648
|
failures: paginated.inline,
|
|
12269
12649
|
overflowFailureCount: paginated.overflowCount,
|
|
12270
|
-
overflowSidecarName: paginated.sidecarPath ?
|
|
12650
|
+
overflowSidecarName: paginated.sidecarPath ? path12.basename(paginated.sidecarPath) : void 0
|
|
12271
12651
|
};
|
|
12272
12652
|
for (const f of renderData.failures) f.screenshotBase64 = void 0;
|
|
12273
12653
|
if (options.includeScreenshots !== false) {
|
|
@@ -12287,7 +12667,7 @@ var PdfGenerator = class {
|
|
|
12287
12667
|
await page.goto(fileUrl, { waitUntil: "domcontentloaded", timeout: 6e4 });
|
|
12288
12668
|
const showCharts = source.kind === "builtin" ? source.id !== "minimal" : true;
|
|
12289
12669
|
await this.chartRenderer.waitForCharts(page, showCharts);
|
|
12290
|
-
logger.info(`Generating PDF \u2192 ${
|
|
12670
|
+
logger.info(`Generating PDF \u2192 ${path12.relative(process.cwd(), outputPath)}`);
|
|
12291
12671
|
await page.pdf({
|
|
12292
12672
|
path: outputPath,
|
|
12293
12673
|
format: "A4",
|
|
@@ -12304,21 +12684,19 @@ function fmtMb(bytes) {
|
|
|
12304
12684
|
}
|
|
12305
12685
|
|
|
12306
12686
|
// src/reporter.ts
|
|
12307
|
-
import
|
|
12308
|
-
import crypto3 from "crypto";
|
|
12309
|
-
import os6 from "os";
|
|
12687
|
+
import path14 from "path";
|
|
12310
12688
|
|
|
12311
12689
|
// src/history/HistoryManager.ts
|
|
12312
12690
|
init_esm_shims();
|
|
12313
|
-
import
|
|
12314
|
-
import
|
|
12691
|
+
import fs13 from "fs";
|
|
12692
|
+
import path13 from "path";
|
|
12315
12693
|
var HistoryManager = class {
|
|
12316
12694
|
constructor(filePath) {
|
|
12317
12695
|
this.filePath = filePath;
|
|
12318
12696
|
}
|
|
12319
12697
|
read() {
|
|
12320
12698
|
try {
|
|
12321
|
-
const raw =
|
|
12699
|
+
const raw = fs13.readFileSync(this.filePath, "utf8");
|
|
12322
12700
|
const parsed = JSON.parse(raw);
|
|
12323
12701
|
if (parsed.version !== 1) {
|
|
12324
12702
|
logger.warn(`history.version-mismatch: path=${this.filePath} version=${String(parsed.version)}`);
|
|
@@ -12332,12 +12710,12 @@ var HistoryManager = class {
|
|
|
12332
12710
|
}
|
|
12333
12711
|
append(entry, maxSize) {
|
|
12334
12712
|
try {
|
|
12335
|
-
|
|
12713
|
+
fs13.mkdirSync(path13.dirname(this.filePath), { recursive: true });
|
|
12336
12714
|
const next = {
|
|
12337
12715
|
version: 1,
|
|
12338
12716
|
entries: [entry, ...this.read()].slice(0, maxSize)
|
|
12339
12717
|
};
|
|
12340
|
-
|
|
12718
|
+
fs13.writeFileSync(this.filePath, JSON.stringify(next, null, 2), { mode: 420 });
|
|
12341
12719
|
return next.entries;
|
|
12342
12720
|
} catch (err) {
|
|
12343
12721
|
logger.warn(`history.write-failed: path=${this.filePath} error=${err.message}`);
|
|
@@ -12678,11 +13056,13 @@ var PdfReporter = class {
|
|
|
12678
13056
|
this.dataCollector = new DataCollector();
|
|
12679
13057
|
this.pdfGenerator = new PdfGenerator();
|
|
12680
13058
|
this.options = parseOptions(rawOptions);
|
|
12681
|
-
const version = true ? "0.
|
|
13059
|
+
const version = true ? "0.4.0" : "0.x";
|
|
12682
13060
|
logger.info(`@reportforge/playwright-pdf v${version} initialised`);
|
|
12683
13061
|
this.licenseClient = new LicenseClient({
|
|
12684
13062
|
licenseKey: this.options.licenseKey,
|
|
12685
|
-
serverUrl: this.options.serverUrl
|
|
13063
|
+
serverUrl: this.options.serverUrl,
|
|
13064
|
+
autoUpdateModel: this.options.failureAnalysis.autoUpdateModel,
|
|
13065
|
+
currentModelVersion: MODEL.version
|
|
12686
13066
|
});
|
|
12687
13067
|
}
|
|
12688
13068
|
onBegin(config, suite) {
|
|
@@ -12704,6 +13084,7 @@ var PdfReporter = class {
|
|
|
12704
13084
|
}
|
|
12705
13085
|
}
|
|
12706
13086
|
const reportData = this._buildReportData(collected, licenseInfo);
|
|
13087
|
+
runFailureAnalysis(reportData, this.options, this.cwd);
|
|
12707
13088
|
let pdfPaths = [];
|
|
12708
13089
|
try {
|
|
12709
13090
|
pdfPaths = await this.pdfGenerator.generateAll(reportData, this.options, licenseInfo);
|
|
@@ -12810,7 +13191,7 @@ var PdfReporter = class {
|
|
|
12810
13191
|
}
|
|
12811
13192
|
resolveProjectName() {
|
|
12812
13193
|
try {
|
|
12813
|
-
const pkgPath =
|
|
13194
|
+
const pkgPath = path14.resolve(process.cwd(), "package.json");
|
|
12814
13195
|
const pkg = __require(pkgPath);
|
|
12815
13196
|
return pkg.name ?? "Playwright Tests";
|
|
12816
13197
|
} catch {
|
|
@@ -12860,10 +13241,9 @@ function deriveVerdict(stats) {
|
|
|
12860
13241
|
}
|
|
12861
13242
|
function resolveHistoryPath(options, cwd) {
|
|
12862
13243
|
if (options.historyFile) {
|
|
12863
|
-
return
|
|
13244
|
+
return path14.isAbsolute(options.historyFile) ? options.historyFile : path14.resolve(cwd, options.historyFile);
|
|
12864
13245
|
}
|
|
12865
|
-
|
|
12866
|
-
return path11.join(os6.homedir(), ".reportforge", projectKey, "history.json");
|
|
13246
|
+
return path14.join(resolveProjectDir(options, cwd), "history.json");
|
|
12867
13247
|
}
|
|
12868
13248
|
function flattenSuiteNodes(suites) {
|
|
12869
13249
|
return suites.flatMap((s) => [s, ...flattenSuiteNodes(s.suites)]);
|