@scantrix/cli 1.0.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.
@@ -0,0 +1,206 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.severityScore = severityScore;
4
+ exports.computeFinalSeverity = computeFinalSeverity;
5
+ exports.applyEscalation = applyEscalation;
6
+ exports.isTestScoped = isTestScoped;
7
+ exports.riskPointsToHealthScore = riskPointsToHealthScore;
8
+ exports.gradeFromHealthScore = gradeFromHealthScore;
9
+ exports.calculateRiskScore = calculateRiskScore;
10
+ function severityScore(sev) {
11
+ return sev === "high" ? 3 : sev === "medium" ? 2 : 1;
12
+ }
13
+ /**
14
+ * Determine whether a finding's severity should be escalated based on
15
+ * how widespread the pattern is across the test suite.
16
+ *
17
+ * Pure function — no side-effects.
18
+ */
19
+ function computeFinalSeverity(baseSeverity, occurrences, totalTestFiles) {
20
+ if (baseSeverity === "high") {
21
+ return { finalSeverity: "high", prevalence: totalTestFiles > 0 ? occurrences / totalTestFiles : null, reason: "already high" };
22
+ }
23
+ if (totalTestFiles <= 0) {
24
+ return { finalSeverity: baseSeverity, prevalence: null, reason: "unknown prevalence" };
25
+ }
26
+ const prevalence = occurrences / totalTestFiles;
27
+ if (baseSeverity === "medium") {
28
+ if (prevalence >= 0.25) {
29
+ return { finalSeverity: "high", prevalence, reason: `prevalence ${(prevalence * 100).toFixed(1)}% >= 25% threshold` };
30
+ }
31
+ if (occurrences >= 200) {
32
+ return { finalSeverity: "high", prevalence, reason: `${occurrences} occurrences >= 200 absolute threshold` };
33
+ }
34
+ if (prevalence >= 0.10 && occurrences >= 75) {
35
+ return { finalSeverity: "high", prevalence, reason: `prevalence ${(prevalence * 100).toFixed(1)}% >= 10% and ${occurrences} occurrences >= 75 (compound threshold)` };
36
+ }
37
+ return { finalSeverity: "medium", prevalence, reason: "below escalation thresholds" };
38
+ }
39
+ // baseSeverity === "low"
40
+ if (prevalence >= 0.25) {
41
+ return { finalSeverity: "medium", prevalence, reason: `prevalence ${(prevalence * 100).toFixed(1)}% >= 25% threshold` };
42
+ }
43
+ if (occurrences >= 200) {
44
+ return { finalSeverity: "medium", prevalence, reason: `${occurrences} occurrences >= 200 absolute threshold` };
45
+ }
46
+ return { finalSeverity: "low", prevalence, reason: "below escalation thresholds" };
47
+ }
48
+ /**
49
+ * Post-scan escalation pass: promote finding severity when a pattern is
50
+ * widespread across the test suite ("blast radius").
51
+ *
52
+ * Returns a new array. Non-escalated findings are returned by reference
53
+ * (no copy). Escalated findings get `baseSeverity` and `escalationReason` set.
54
+ * The original array is never mutated.
55
+ */
56
+ function applyEscalation(findings, effectiveTestFiles) {
57
+ return findings.map((f) => {
58
+ const occurrences = f.totalOccurrences ?? f.evidence.length;
59
+ const result = computeFinalSeverity(f.severity, occurrences, effectiveTestFiles);
60
+ if (result.finalSeverity === f.severity) {
61
+ return f; // no escalation — return by reference
62
+ }
63
+ return {
64
+ ...f,
65
+ baseSeverity: f.severity,
66
+ severity: result.finalSeverity,
67
+ escalationReason: result.reason,
68
+ };
69
+ });
70
+ }
71
+ /**
72
+ * Finding prefixes that are tied to test files.
73
+ * Used by reports and downstream consumers to classify findings.
74
+ */
75
+ const TEST_SCOPED_PREFIXES = [
76
+ "PW-FLAKE-",
77
+ "PW-STABILITY-",
78
+ "PW-LOC-",
79
+ "PW-PERF-",
80
+ "PW-DEPREC-",
81
+ "CY-",
82
+ "SE-",
83
+ "DEP-",
84
+ ];
85
+ function isTestScoped(findingId) {
86
+ return TEST_SCOPED_PREFIXES.some((p) => findingId.startsWith(p));
87
+ }
88
+ /**
89
+ * Convert raw risk points (unbounded, higher = worse) to a 0–100 health score
90
+ * (higher = better) using exponential decay.
91
+ *
92
+ * Examples:
93
+ * 0 risk points → 100 (perfect)
94
+ * 50 risk points → 78
95
+ * 100 risk points → 61
96
+ * 200 risk points → 37
97
+ * 300 risk points → 22
98
+ * 400 risk points → 14
99
+ */
100
+ function riskPointsToHealthScore(riskPoints) {
101
+ return Math.round(100 * Math.exp(-riskPoints / 200));
102
+ }
103
+ /**
104
+ * Map a 0–100 health score to a letter grade and human-readable level.
105
+ * Higher score = better grade.
106
+ *
107
+ * Default thresholds: A (≥80), B (≥60), C (≥40), D (≥30), F (<30)
108
+ */
109
+ function gradeFromHealthScore(score, thresholds) {
110
+ const t = thresholds ?? { gradeA: 80, gradeB: 60, gradeC: 40 };
111
+ if (score >= t.gradeA)
112
+ return { grade: "A", level: "Excellent", description: "Well-structured framework with strong practices" };
113
+ if (score >= t.gradeB)
114
+ return { grade: "B", level: "Good", description: "Solid foundation with room for improvement" };
115
+ if (score >= t.gradeC)
116
+ return { grade: "C", level: "Fair", description: "Multiple areas need attention" };
117
+ if (score >= 30)
118
+ return { grade: "D", level: "Poor", description: "Significant issues affecting reliability" };
119
+ return { grade: "F", level: "Critical", description: "Widespread issues requiring immediate action" };
120
+ }
121
+ /**
122
+ * Calculate the risk score and grade from a set of findings.
123
+ *
124
+ * This is the **single source of truth** for scoring — used by the CLI report
125
+ * and all result output. Any change to the formula must happen here so all
126
+ * consumers stay in sync.
127
+ *
128
+ * ## Density normalization
129
+ *
130
+ * When `inventory` is provided, the decay constant (lambda) is scaled by suite
131
+ * size so that larger test suites are evaluated more leniently. The rationale:
132
+ * 24 findings scattered across 981 test files is a healthier codebase than 18
133
+ * findings concentrated in 105 test files.
134
+ *
135
+ * The density factor scales the decay constant (λ = 200 × factor), meaning
136
+ * the same raw risk points produce a higher health score for larger suites.
137
+ * Factor is clamped to [1.0, 4.0] — small suites see no penalty, and the
138
+ * maximum benefit caps at 4.0× to prevent extremely large suites from
139
+ * masking real problems.
140
+ *
141
+ * Returns a 0–100 health score (higher = better) in the `riskScore` field.
142
+ */
143
+ function calculateRiskScore(findings, inventory) {
144
+ // Weight findings by type and severity
145
+ const INSIGHT_MULTIPLIER = 1.5; // Correlated findings are higher signal
146
+ const CI_MULTIPLIER = 1.3; // CI issues affect entire pipeline
147
+ const ARCH_MULTIPLIER = 1.2; // Architecture issues compound over time
148
+ let riskPoints = 0;
149
+ for (const f of findings) {
150
+ const basePoints = severityScore(f.severity);
151
+ // Occurrence multiplier: N^0.6 power curve.
152
+ // Provides meaningful sensitivity across the full range — fixing 58 of 170
153
+ // occurrences produces a visible score improvement, unlike log₂ which
154
+ // compresses too aggressively at high counts.
155
+ // Uses totalOccurrences (true count) when available, since evidence arrays
156
+ // are capped for report brevity (typically 10-25 items).
157
+ //
158
+ // 1 occurrence → 1.00 25 occurrences → 6.90
159
+ // 5 occurrences → 2.63 50 occurrences → 10.46
160
+ // 10 occurrences → 3.98 100 occurrences → 15.85
161
+ const occurrences = f.totalOccurrences ?? f.evidence.length;
162
+ const evidenceMultiplier = Math.pow(Math.max(occurrences, 1), 0.6);
163
+ let multiplier = 1;
164
+ if (f.findingId.startsWith("PW-INSIGHT-"))
165
+ multiplier = INSIGHT_MULTIPLIER;
166
+ else if (f.findingId.startsWith("CI-"))
167
+ multiplier = CI_MULTIPLIER;
168
+ else if (f.findingId.startsWith("ARCH-"))
169
+ multiplier = ARCH_MULTIPLIER;
170
+ riskPoints += basePoints * evidenceMultiplier * multiplier;
171
+ }
172
+ // Density normalization: scale the decay constant by suite size.
173
+ // Larger suites get a higher lambda, so the same risk points yield a better score.
174
+ //
175
+ // densityFactor formula: 1.0 + 0.3 × log₂(effectiveTestFiles / 50)
176
+ // 50 files → 1.0 (neutral — no adjustment)
177
+ // 100 files → 1.3
178
+ // 200 files → 1.6
179
+ // 500 files → ~2.0
180
+ // 1000 files → ~2.3
181
+ // 5000 files → ~2.9
182
+ // < 50 files → 1.0 (floor — never penalize small suites)
183
+ // cap at 4.0 (reached only at ~51,000 files)
184
+ const BASE_LAMBDA = 200;
185
+ let lambda = BASE_LAMBDA;
186
+ let densityInfo;
187
+ if (inventory && findings.length > 0) {
188
+ const effectiveTestFiles = inventory.testFiles +
189
+ (inventory.cypressTestFiles ?? 0) +
190
+ (inventory.seleniumTestFiles ?? 0);
191
+ if (effectiveTestFiles > 0) {
192
+ const findingDensity = findings.length / effectiveTestFiles;
193
+ const rawFactor = 1.0 + 0.3 * Math.log2(Math.max(effectiveTestFiles / 50, 1));
194
+ const densityFactor = Math.min(4.0, Math.max(1.0, rawFactor));
195
+ lambda = BASE_LAMBDA * densityFactor;
196
+ densityInfo = {
197
+ effectiveTestFiles,
198
+ findingDensity: Math.round(findingDensity * 1000) / 1000,
199
+ densityFactor: Math.round(densityFactor * 100) / 100,
200
+ };
201
+ }
202
+ }
203
+ const healthScore = Math.round(100 * Math.exp(-riskPoints / lambda));
204
+ const gradeInfo = gradeFromHealthScore(healthScore);
205
+ return { ...gradeInfo, riskScore: healthScore, densityInfo };
206
+ }
@@ -0,0 +1,29 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.JsonSink = void 0;
4
+ exports.resolveSinkConfig = resolveSinkConfig;
5
+ exports.buildSinks = buildSinks;
6
+ const jsonSink_1 = require("./jsonSink");
7
+ /**
8
+ * Resolve sink configuration from environment variables, with optional
9
+ * CLI-flag overrides.
10
+ */
11
+ function resolveSinkConfig(overrides) {
12
+ return {
13
+ jsonPath: overrides?.jsonPath ?? process.env.SCANTRIX_JSON_PATH ?? undefined,
14
+ };
15
+ }
16
+ /**
17
+ * Build the ordered list of sinks from a validated {@link SinkConfig}.
18
+ *
19
+ * JSON sink is created when `jsonPath` is set.
20
+ */
21
+ function buildSinks(config) {
22
+ const sinks = [];
23
+ if (config.jsonPath) {
24
+ sinks.push(new jsonSink_1.JsonSink(config.jsonPath));
25
+ }
26
+ return sinks;
27
+ }
28
+ var jsonSink_2 = require("./jsonSink");
29
+ Object.defineProperty(exports, "JsonSink", { enumerable: true, get: function () { return jsonSink_2.JsonSink; } });
@@ -0,0 +1,28 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.JsonSink = void 0;
7
+ const promises_1 = __importDefault(require("fs/promises"));
8
+ const path_1 = __importDefault(require("path"));
9
+ /**
10
+ * Writes the full {@link ScanResult} as a pretty-printed JSON file.
11
+ *
12
+ * This sink is always safe to use and has no external dependencies
13
+ * beyond Node's built-in `fs` module. Recommended in every
14
+ * environment (local & CI) as a durable artifact / fallback.
15
+ */
16
+ class JsonSink {
17
+ filePath;
18
+ name = "json";
19
+ constructor(filePath) {
20
+ this.filePath = filePath;
21
+ }
22
+ async write(result) {
23
+ const abs = path_1.default.resolve(this.filePath);
24
+ await promises_1.default.mkdir(path_1.default.dirname(abs), { recursive: true });
25
+ await promises_1.default.writeFile(abs, JSON.stringify(result, null, 2), "utf8");
26
+ }
27
+ }
28
+ exports.JsonSink = JsonSink;
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,26 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <!-- Generator: Adobe Illustrator 30.2.1, SVG Export Plug-In . SVG Version: 9.03 Build 0) -->
3
+ <svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
4
+ width="512" height="512" viewBox="-1 -7.5 527 527" xml:space="preserve">
5
+ <g id="BoxInterior">
6
+ <g id="Lambda_00000018955945252053725280000009118050138801679498_">
7
+ <polyline id="LeftLeg" fill="#4F7BE3" stroke="#4F7BE3" stroke-width="0.3673" stroke-miterlimit="10" points="241.313,228.973
8
+ 62.719,454.627 144.574,454.599 276.345,284.973 "/>
9
+ <polygon fill="#4F7BE3" stroke="#4F7BE3" stroke-width="0.3673" stroke-miterlimit="10" points="200.662,202.76 228.653,253.062
10
+ 342.316,453.453 460.935,453.453 460.935,381.12 382.252,381.12 194.649,54.57 62.719,54.412 62.719,112.57 149.396,112.57 "/>
11
+
12
+ <polyline id="LeftLeg_00000170258041919781009300000016929244431908451753_" fill="#4F7BE3" stroke="#4F7BE3" stroke-width="0.3673" stroke-miterlimit="10" points="
13
+ 324.334,223.188 459.624,55.345 377.745,55.376 289.658,166.627 "/>
14
+ </g>
15
+ </g>
16
+ <g id="BoxEdges">
17
+ <path id="BottomRt" fill="#808080" d="M494.925,480.929V368.497h29.502l0.19,122.857c0.017,16.859,0.19,19.32-3.021,20.31
18
+ L376.932,512l0.03-29.695l84.485-0.073h33.478L494.925,480.929L494.925,480.929z"/>
19
+ <path id="BottomLeft" fill="#808080" d="M29.698,480.929V368.497H0.197l-0.19,122.857c-0.017,16.859-0.19,19.32,3.021,20.31
20
+ L147.691,512l-0.03-29.695l-84.485-0.073H29.698V480.929z"/>
21
+ <path id="TopRt" fill="#808080" d="M494.925,31.071v112.433h29.502l0.19-122.857c0.017-16.859,0.19-19.32-3.021-20.309L376.932,0
22
+ l0.03,29.695l84.485,0.073h33.478L494.925,31.071L494.925,31.071z"/>
23
+ <path id="TopLeft" fill="#808080" d="M29.698,31.071v112.433H0.197L0.007,20.646c-0.018-16.859-0.19-19.32,3.02-20.309L147.691,0
24
+ l-0.03,29.695l-84.485,0.073H29.698V31.071z"/>
25
+ </g>
26
+ </svg>
@@ -0,0 +1,64 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <svg id="Layer_1" xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 5942.2 1528">
3
+ <!-- Generator: Adobe Illustrator 30.2.1, SVG Export Plug-In . SVG Version: 2.1.1 Build 1) -->
4
+ <defs>
5
+ <style>
6
+ .st0, .st1 {
7
+ fill: #808080;
8
+ }
9
+
10
+ .st1, .st2, .st3, .st4 {
11
+ stroke: #4f7be3;
12
+ stroke-miterlimit: 10;
13
+ }
14
+
15
+ .st2 {
16
+ fill: #808080;
17
+ }
18
+
19
+ .st3 {
20
+ stroke-width: .57px;
21
+ }
22
+
23
+ .st3, .st5, .st4 {
24
+ fill: #4f7be3;
25
+ }
26
+
27
+ .st4 {
28
+ stroke-width: 5.57px;
29
+ }
30
+ </style>
31
+ </defs>
32
+ <g id="ScantrixText">
33
+ <g id="scantrix-text">
34
+ <path id="S" class="st1" d="M1723.07,1059.71v-86.2h384.81c37.73,0,55.33-18.94,55.33-53.52v-73.29c0-34.58-17.61-53.52-55.33-53.52h-260.16c-100.66,0-139.03-56.88-139.03-140.15v-33.8c0-83.26,38.38-140.15,139.03-140.15h386.81v85.21h-385.02c-39.77,0-57.39,19.1-57.39,53.98v35.71c0,34.88,17.62,53.98,55.39,53.98h260.23c100.56,0,138.91,56.84,138.91,140.04v71.67c0,83.2-38.34,140.04-138.91,140.04h-384.66Z"/>
35
+ <path id="C" class="st1" d="M2419.1,1059.72c-88.5,0-122.24-56.91-122.24-140.21v-300.21c0-83.3,33.74-140.21,122.24-140.21h313.62v85.77h-294.62c-33.19,0-48.68,18.97-48.68,53.61v301.86c0,34.64,15.49,53.61,48.68,53.61h294.62v85.77h-313.62Z"/>
36
+ <path id="A" class="st1" d="M3240.7,1059.72l-170.04-467.27-172.93,467.27h-101.24l225.67-580.17h97.52l223.22,580.17h-97.2s-5,0-5,0h0Z"/>
37
+ <path id="N" class="st1" d="M3847.31,1060.17l-353.93-452.32,1,452.32h-89.43l-3.08-581.08h109.07l318.62,396.68-1-396.68h90.34v581.08h-64.6s-7,0-7,0h0Z"/>
38
+ <path id="T" class="st1" d="M4201.58,1060.84v-495.39h-219.3v-86.37h535.36v86.37h-213.62v495.39h-102.44Z"/>
39
+ <path id="R" class="st1" d="M5011.92,1062.63l-101.71-240h-228.71v240h-81.76V476.63h368.47c98.59,0,136.17,57.43,136.17,141.51v42.19c0,75.64-30.21,142.67-107.26,152.64l102.33,249.66h-87.54ZM5022.63,616.92c0-35.08-17.27-54.29-54.28-54.29h-286.85v172h286.85c37.01,0,54.28-39.21,54.28-74.29v-43.43Z"/>
40
+ <path id="I" class="st1" d="M5171.62,1065.54V476.63h96.85l-1.08,588.91h-95.77Z"/>
41
+ <g id="x-symbol">
42
+ <g id="x-lambda">
43
+ <polyline id="x-left-leg" class="st4" points="5596.03 736.18 5329.86 1075.12 5448.78 1075.18 5647.7 827.16"/>
44
+ <polygon class="st3" points="5532.07 696.07 5572.71 772.04 5737.75 1074.7 5909.98 1074.7 5909.98 965.45 5795.74 965.45 5523.34 472.25 5331.77 472.01 5331.77 559.85 5457.63 559.85 5532.07 696.07"/>
45
+ </g>
46
+ <polygon id="x-accent" class="st2" points="5717 736.18 5912.49 471.07 5806.7 471.06 5672.99 657.44 5717 736.18"/>
47
+ </g>
48
+ </g>
49
+ <path id="I1" data-name="I" class="st0" d="M2991.44,806.51l157.55.44,28.07,77.38-213.92-1.36"/>
50
+ </g>
51
+ <g id="BoxInterior">
52
+ <g id="box-lambda">
53
+ <polyline id="box-left-leg" class="st1" points="779.61 686.51 248.16 1260.19 485.62 1260.3 882.79 840.51"/>
54
+ <polygon class="st1" points="651.9 618.63 733.05 747.22 1062.57 1259.48 1406.46 1259.48 1406.46 1074.58 1178.35 1074.58 634.47 239.81 251.99 239.4 251.99 388.08 503.28 388.08 651.9 618.63"/>
55
+ </g>
56
+ <polygon id="box-accent" class="st5" points="1000.53 702.76 1403.17 243.06 1203.13 243.15 910.13 564.66 1000.53 702.76"/>
57
+ </g>
58
+ <g id="BoxEdges">
59
+ <path id="BottomRt" class="st5" d="M1512.87,1380.16v-303.12h79.8l.51,331.22c.05,45.45.51,52.09-8.17,54.76l-391.29.91.08-80.06,228.52-.2h90.55v-3.51Z"/>
60
+ <path id="BottomLeft" class="st5" d="M151.88,1380.16v-303.12h-79.8l-.51,331.22c-.05,45.45-.51,52.09,8.17,54.76l391.29.91-.08-80.06-228.51-.2h-90.55v-3.51Z"/>
61
+ <path id="TopRt" class="st5" d="M1512.87,147.41v303.12h79.8l.51-331.23c.05-45.45.51-52.09-8.17-54.75l-391.29-.91.08,80.06,228.52.2h90.55v3.51Z"/>
62
+ <path id="TopLeft" class="st5" d="M152.88,147.41v303.12h-79.8l-.51-331.23c-.05-45.45-.51-52.09,8.17-54.75l391.29-.91-.08,80.06-228.51.2h-90.55v3.51Z"/>
63
+ </g>
64
+ </svg>
@@ -0,0 +1,64 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <svg id="Layer_1" xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 5942.2 1528">
3
+ <!-- Generator: Adobe Illustrator 30.2.1, SVG Export Plug-In . SVG Version: 2.1.1 Build 1) -->
4
+ <defs>
5
+ <style>
6
+ .st0, .st1 {
7
+ fill: #e2e8f0;
8
+ }
9
+
10
+ .st1, .st2, .st3, .st4 {
11
+ stroke: #4f7be3;
12
+ stroke-miterlimit: 10;
13
+ }
14
+
15
+ .st2 {
16
+ fill: #d9d9d9;
17
+ }
18
+
19
+ .st3 {
20
+ stroke-width: .57px;
21
+ }
22
+
23
+ .st3, .st5, .st4 {
24
+ fill: #4f7be3;
25
+ }
26
+
27
+ .st4 {
28
+ stroke-width: 5.57px;
29
+ }
30
+ </style>
31
+ </defs>
32
+ <g id="ScantrixText">
33
+ <g id="scantrix-text">
34
+ <path id="S" class="st1" d="M1723.07,1059.71v-86.2h384.81c37.73,0,55.33-18.94,55.33-53.52v-73.29c0-34.58-17.61-53.52-55.33-53.52h-260.16c-100.66,0-139.03-56.88-139.03-140.15v-33.8c0-83.26,38.38-140.15,139.03-140.15h386.81v85.21h-385.02c-39.77,0-57.39,19.1-57.39,53.98v35.71c0,34.88,17.62,53.98,55.39,53.98h260.23c100.56,0,138.91,56.84,138.91,140.04v71.67c0,83.2-38.34,140.04-138.91,140.04h-384.66Z"/>
35
+ <path id="C" class="st1" d="M2419.1,1059.72c-88.5,0-122.24-56.91-122.24-140.21v-300.21c0-83.3,33.74-140.21,122.24-140.21h313.62v85.77h-294.62c-33.19,0-48.68,18.97-48.68,53.61v301.86c0,34.64,15.49,53.61,48.68,53.61h294.62v85.77h-313.62Z"/>
36
+ <path id="A" class="st1" d="M3240.7,1059.72l-170.04-467.27-172.93,467.27h-101.24l225.67-580.17h97.52l223.22,580.17h-97.2s-5,0-5,0h0Z"/>
37
+ <path id="N" class="st1" d="M3847.31,1060.17l-353.93-452.32,1,452.32h-89.43l-3.08-581.08h109.07l318.62,396.68-1-396.68h90.34v581.08h-64.6s-7,0-7,0h0Z"/>
38
+ <path id="T" class="st1" d="M4201.58,1060.84v-495.39h-219.3v-86.37h535.36v86.37h-213.62v495.39h-102.44Z"/>
39
+ <path id="R" class="st1" d="M5011.92,1062.63l-101.71-240h-228.71v240h-81.76V476.63h368.47c98.59,0,136.17,57.43,136.17,141.51v42.19c0,75.64-30.21,142.67-107.26,152.64l102.33,249.66h-87.54ZM5022.63,616.92c0-35.08-17.27-54.29-54.28-54.29h-286.85v172h286.85c37.01,0,54.28-39.21,54.28-74.29v-43.43Z"/>
40
+ <path id="I" class="st1" d="M5171.62,1065.54V476.63h96.85l-1.08,588.91h-95.77Z"/>
41
+ <g id="x-symbol">
42
+ <g id="x-lambda">
43
+ <polyline id="x-left-leg" class="st4" points="5596.03 736.18 5329.86 1075.12 5448.78 1075.18 5647.7 827.16"/>
44
+ <polygon class="st3" points="5532.07 696.07 5572.71 772.04 5737.75 1074.7 5909.98 1074.7 5909.98 965.45 5795.74 965.45 5523.34 472.25 5331.77 472.01 5331.77 559.85 5457.63 559.85 5532.07 696.07"/>
45
+ </g>
46
+ <polygon id="x-accent" class="st2" points="5717 736.18 5912.49 471.07 5806.7 471.06 5672.99 657.44 5717 736.18"/>
47
+ </g>
48
+ </g>
49
+ <path id="I1" data-name="I" class="st0" d="M2991.44,806.51l157.55.44,28.07,77.38-213.92-1.36"/>
50
+ </g>
51
+ <g id="BoxInterior">
52
+ <g id="box-lambda">
53
+ <polyline id="box-left-leg" class="st1" points="779.61 686.51 248.16 1260.19 485.62 1260.3 882.79 840.51"/>
54
+ <polygon class="st1" points="651.9 618.63 733.05 747.22 1062.57 1259.48 1406.46 1259.48 1406.46 1074.58 1178.35 1074.58 634.47 239.81 251.99 239.4 251.99 388.08 503.28 388.08 651.9 618.63"/>
55
+ </g>
56
+ <polygon id="box-accent" class="st5" points="1000.53 702.76 1403.17 243.06 1203.13 243.15 910.13 564.66 1000.53 702.76"/>
57
+ </g>
58
+ <g id="BoxEdges">
59
+ <path id="BottomRt" class="st5" d="M1512.87,1380.16v-303.12h79.8l.51,331.22c.05,45.45.51,52.09-8.17,54.76l-391.29.91.08-80.06,228.52-.2h90.55v-3.51Z"/>
60
+ <path id="BottomLeft" class="st5" d="M151.88,1380.16v-303.12h-79.8l-.51,331.22c-.05,45.45-.51,52.09,8.17,54.76l391.29.91-.08-80.06-228.51-.2h-90.55v-3.51Z"/>
61
+ <path id="TopRt" class="st5" d="M1512.87,147.41v303.12h79.8l.51-331.23c.05-45.45.51-52.09-8.17-54.75l-391.29-.91.08,80.06,228.52.2h90.55v3.51Z"/>
62
+ <path id="TopLeft" class="st5" d="M152.88,147.41v303.12h-79.8l-.51-331.23c-.05-45.45-.51-52.09,8.17-54.75l391.29-.91-.08,80.06-228.51.2h-90.55v3.51Z"/>
63
+ </g>
64
+ </svg>
package/package.json ADDED
@@ -0,0 +1,55 @@
1
+ {
2
+ "name": "@scantrix/cli",
3
+ "version": "1.0.0",
4
+ "description": "Static analysis and configuration-aware auditing engine for automation repositories",
5
+ "main": "dist/cli.js",
6
+ "bin": {
7
+ "scantrix": "dist/cli.js"
8
+ },
9
+ "scripts": {
10
+ "build": "tsc",
11
+ "start": "node dist/cli.js",
12
+ "audit": "npm run build && node dist/cli.js",
13
+ "scan": "npm run audit",
14
+ "test": "vitest run",
15
+ "test:watch": "vitest",
16
+ "generate:favicons": "ts-node scripts/generate-favicons.ts"
17
+ },
18
+ "keywords": [
19
+ "playwright",
20
+ "cypress",
21
+ "selenium",
22
+ "test-automation",
23
+ "static-analysis",
24
+ "audit",
25
+ "ci",
26
+ "quality"
27
+ ],
28
+ "author": "Scantrix",
29
+ "license": "MIT",
30
+ "engines": {
31
+ "node": ">=18"
32
+ },
33
+ "files": [
34
+ "dist/",
35
+ "docs/*.svg",
36
+ "LICENSE",
37
+ "README.md"
38
+ ],
39
+ "type": "commonjs",
40
+ "dependencies": {
41
+ "fast-glob": "^3.3.3",
42
+ "markdown-it": "^14.1.0",
43
+ "minimist": "^1.2.8"
44
+ },
45
+ "devDependencies": {
46
+ "@types/markdown-it": "^14.1.2",
47
+ "@types/minimist": "^1.2.5",
48
+ "@types/node": "^25.1.0",
49
+ "@types/sharp": "^0.31.1",
50
+ "sharp": "^0.34.5",
51
+ "ts-node": "^10.9.2",
52
+ "typescript": "^5.9.3",
53
+ "vitest": "^3.0.0"
54
+ }
55
+ }