@qaguardian/playwright-score 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 QA Guardian / playwright-score contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/METHODOLOGY.md ADDED
@@ -0,0 +1,178 @@
1
+ # Playwright Spec Score (SQS) Methodology
2
+
3
+ **Score version:** `sqs-v1`
4
+ **Package:** `playwright-score`
5
+ **Landing page:** https://qaguardian.com/open-source/playwright-score
6
+ **Maintainer:** [QA Guardian](https://qaguardian.com)
7
+ **Deterministic:** same inputs → same score
8
+ **AI-free:** no LLM calls in the scoring path
9
+
10
+ Any change to formulas, weights, SLOC definition, caps, or constants requires a new version (`sqs-v2`, …).
11
+
12
+ ---
13
+
14
+ ## Profiles
15
+
16
+ | Profile | Purpose |
17
+ |---|---|
18
+ | `standard` | Community Playwright best practices (`eslint-plugin-playwright`) + suite metrics |
19
+ | `guardian` | `standard` + QA Guardian house conventions (optional; clearly labeled) |
20
+
21
+ House rules are **not** claimed to be official Playwright doctrine.
22
+
23
+ ---
24
+
25
+ ## Output
26
+
27
+ - **score:** integer 0–100
28
+ - **grade:** A (90–100), B (80–89), C (70–79), D (60–69), F (0–59)
29
+ - **pass:** `score >= threshold`
30
+ - **dimensions:** per-dimension 0–100
31
+ - **findings:** full list (uncapped) for humans/agents
32
+
33
+ ---
34
+
35
+ ## Dimension weights
36
+
37
+ ### `standard`
38
+
39
+ | Dimension | Weight |
40
+ |---|---|
41
+ | playwrightHygiene | 40 |
42
+ | assertions | 25 |
43
+ | locators | 20 |
44
+ | structure | 15 |
45
+
46
+ ### `guardian`
47
+
48
+ | Dimension | Weight |
49
+ |---|---|
50
+ | playwrightHygiene | 30 |
51
+ | assertions | 15 |
52
+ | locators | 15 |
53
+ | structure | 15 |
54
+ | guardianConventions | 25 |
55
+
56
+ ---
57
+
58
+ ## Locators dimension (ratio)
59
+
60
+ Independent of finding-penalty math:
61
+
62
+ ```
63
+ native = count(getByRole|getByLabel|getByTestId|getByText|getByPlaceholder)
64
+ raw = count(page.locator(|frame.locator()
65
+ total = native + raw
66
+
67
+ locatorsScore = total === 0 ? 100 : round(100 * native / total)
68
+ ```
69
+
70
+ ESLint `no-raw-locators` / `prefer-native-locators` still appear in `findings[]` for repair guidance.
71
+ They are **not** double-counted into the locators dimension via exponential penalties.
72
+ For sqs-v1 they map to **report-only** for scoring (severity still shown in findings; units = 0 for penalty dimensions).
73
+
74
+ ---
75
+
76
+ ## SLOC (source lines of code)
77
+
78
+ ```
79
+ sloc = lines that are NOT:
80
+ - blank / whitespace-only
81
+ - pure comment lines (// or block-comment-only lines)
82
+ sloc = max(sloc, 1)
83
+ ```
84
+
85
+ Used for penalty density. JSDoc / decorative comments do not inflate slots.
86
+
87
+ ---
88
+
89
+ ## Penalty dimensions
90
+
91
+ Applies to: `playwrightHygiene`, `assertions`, `structure`, `guardianConventions`.
92
+
93
+ ### Constants (frozen sqs-v1)
94
+
95
+ | Constant | Value |
96
+ |---|---|
97
+ | `ERROR_UNIT` | 1.0 |
98
+ | `WARNING_UNIT` | 0.4 |
99
+ | `INFO_UNIT` | 0.0 |
100
+ | `MAX_FINDINGS_PER_RULE_PER_FILE` | 3 (for penalty math only) |
101
+ | `SLOT_DIVISOR` | 25 |
102
+ | `MIN_SLOTS` | 4 |
103
+ | `K` | 0.4 |
104
+
105
+ ### Steps
106
+
107
+ 1. **Cap** findings per `(file, ruleId)` to 3 for penalty math (report all in `findings[]`).
108
+ 2. **Units:** `rawUnits = E * 1.0 + W * 0.4`
109
+ 3. **Slots:** `slots = max(sloc / 25, 4)`
110
+ 4. **Load:** `load = rawUnits / slots`
111
+ 5. **Score:** `dimensionScore = clamp(0, 100, round(100 * exp(-0.4 * load)))`
112
+
113
+ ### Final score
114
+
115
+ ```
116
+ score = round(Σ (weight_d / 100) * dimensionScore_d)
117
+ pass = score >= threshold
118
+ ```
119
+
120
+ ---
121
+
122
+ ## Worked examples (Phase 0 validation)
123
+
124
+ Assumptions: single-file runs; weights as above.
125
+
126
+ ### 1. Tiny clean (~15 SLOC, 0 findings, all native locators)
127
+
128
+ - All penalty dims = 100
129
+ - Locators = 100
130
+ - **standard score ≈ 100**
131
+
132
+ ### 2. Tiny file, 1 hygiene error (~20 SLOC)
133
+
134
+ - slots = max(20/25, 4) = 4
135
+ - rawUnits = 1.0 → load = 0.25 → hygiene = round(100 * e^(-0.1)) ≈ **90**
136
+ - Other dims 100
137
+ - standard ≈ 0.4*90 + 0.25*100 + 0.2*100 + 0.15*100 = **96**
138
+
139
+ ### 3. Medium file, 5 distinct hygiene errors (~100 SLOC)
140
+
141
+ - slots = max(100/25, 4) = 4
142
+ - rawUnits = 5 → load = 1.25 → hygiene = round(100 * e^(-0.5)) ≈ **61**
143
+ - standard ≈ 0.4*61 + 60 = **84** if other dims perfect
144
+
145
+ ### 4. Large file, 20× same warning (capped to 3)
146
+
147
+ - 20 warnings same rule → cap 3 → rawUnits = 3 * 0.4 = 1.2
148
+ - sloc = 500 → slots = 20 → load = 0.06 → score ≈ **98** on that dim
149
+ - Cap prevents auto-zero
150
+
151
+ ### 5. Locator ratio 2 native / 10 total
152
+
153
+ - locatorsScore = round(100 * 2/10) = **20**
154
+ - Independent of caps
155
+
156
+ These examples feel fair: tiny single error stays high; multi-error drops materialy; repeated same-rule does not nuke large files; locator mix is a pure ratio.
157
+
158
+ ---
159
+
160
+ ## Default thresholds
161
+
162
+ | Context | Profile | Threshold |
163
+ |---|---|---|
164
+ | CLI default | standard | 80 |
165
+ | QAG dogfood (warn) | guardian | 75 → 80 |
166
+ | QAG gate (later) | guardian | 80 |
167
+
168
+ ---
169
+
170
+ ## Rule → dimension map (summary)
171
+
172
+ See package rule docs / source `profiles.ts`. High level:
173
+
174
+ - **playwrightHygiene:** waits, force, networkidle, missing await, handles, conditionals
175
+ - **assertions:** expect-expect, empty tests, web-first preference
176
+ - **locators:** ratio metric only (for score); raw-locator ESLint in findings
177
+ - **structure:** focused/skipped, oversized file, describe shape
178
+ - **guardianConventions:** load-state ban, Date.now ban, secrets, test.step, one-describe-one-test, etc.
package/README.md ADDED
@@ -0,0 +1,109 @@
1
+ # @qaguardian/playwright-score
2
+
3
+ **Deterministic, AI-free quality score for Playwright specs** (`sqs-v1`).
4
+
5
+ **Website:** [qaguardian.com/open-source/playwright-score](https://qaguardian.com/open-source/playwright-score)
6
+ **Built by:** [QA Guardian](https://qaguardian.com) — managed Playwright E2E (AI drafts, engineers verify, you own the code)
7
+
8
+ Lint + score Playwright tests with:
9
+
10
+ - **`standard`** — community best practices via [`eslint-plugin-playwright`](https://github.com/playwright-community/eslint-plugin-playwright) + suite metrics
11
+ - **`guardian`** — optional house conventions (QA Guardian codegen gate); not official Playwright doctrine
12
+
13
+ The score never calls an LLM. AI may *generate* or *repair* code using findings; **rules grade code**.
14
+
15
+ See [METHODOLOGY.md](./METHODOLOGY.md) for the frozen formula, or the full product write-up on the [landing page](https://qaguardian.com/open-source/playwright-score).
16
+
17
+ ## Install
18
+
19
+ ```bash
20
+ npm install -D @qaguardian/playwright-score
21
+ # or run from this package
22
+ npm install && npm run build
23
+ ```
24
+
25
+ ## CLI
26
+
27
+ ```bash
28
+ npx @qaguardian/playwright-score ./tests --profile standard --threshold 80
29
+ npx @qaguardian/playwright-score ./flow.spec.ts --profile guardian --format json --out report.json
30
+ ```
31
+
32
+ | Flag | Description |
33
+ |---|---|
34
+ | `--profile standard\|guardian` | Scoring profile (default `standard`) |
35
+ | `--threshold <n>` | Pass bar 0–100 |
36
+ | `--format text\|json\|markdown\|sarif` | Output format |
37
+ | `--out <file>` | Write report to file |
38
+
39
+ **Exit codes:** `0` pass · `1` below threshold · `2` tool error
40
+
41
+ ## Library
42
+
43
+ ```ts
44
+ import { scorePaths } from '@qaguardian/playwright-score';
45
+
46
+ const result = await scorePaths({
47
+ paths: ['tests/login.spec.ts'],
48
+ profile: 'standard',
49
+ threshold: 80,
50
+ });
51
+
52
+ console.log(result.score, result.grade, result.pass, result.findings);
53
+ ```
54
+
55
+ ## CI example
56
+
57
+ ```yaml
58
+ - name: Playwright Spec Score
59
+ run: npx playwright-score ./tests --profile standard --threshold 80 --format text
60
+ ```
61
+
62
+ ## QA Guardian integration
63
+
64
+ When used from `playwright_runner`, set:
65
+
66
+ | Env | Values | Default |
67
+ |---|---|---|
68
+ | `SPEC_SCORE_MODE` | `off` \| `warn` \| `gate` | `warn` |
69
+ | `SPEC_SCORE_PROFILE` | `standard` \| `guardian` | `guardian` |
70
+ | `SPEC_SCORE_THRESHOLD` | `0`–`100` | `75` (guardian) / `80` (standard) |
71
+
72
+ - **`warn`**: log score; never fail the run; findings still inject into heal/generate repair prompts
73
+ - **`gate`**: fail the run when score &lt; threshold
74
+ - After AI **generate**, specs below threshold get one automatic **heal** pass with score findings
75
+ - **Heal** prompts always include score findings when the scorer reports issues
76
+
77
+ ```bash
78
+ # Local from monorepo
79
+ cd playwright-score && npm run build
80
+ cd ../playwright_runner && npm install
81
+ SPEC_SCORE_MODE=warn SPEC_SCORE_PROFILE=guardian node ...
82
+ ```
83
+
84
+ ## Profiles
85
+
86
+ | Profile | Use when |
87
+ |---|---|
88
+ | `standard` | Any Playwright repo — best practices + locator mix + empty-expect detection |
89
+ | `guardian` | QA Guardian (or similar) codegen quality gate |
90
+
91
+ Disable a guardian secrets false positive:
92
+
93
+ ```ts
94
+ // spec-score-disable-next-line guardian/no-hardcoded-secrets
95
+ const password = 'intentional-fixture-value-xyz';
96
+ ```
97
+
98
+ ## Development
99
+
100
+ ```bash
101
+ npm install
102
+ npm run build
103
+ npm test
104
+ npm run score -- ./fixtures/good-standard.spec.ts
105
+ ```
106
+
107
+ ## License
108
+
109
+ MIT
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import '../dist/cli.js';
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,113 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * playwright-score CLI
4
+ */
5
+ import fs from 'node:fs';
6
+ import path from 'node:path';
7
+ import { scorePaths } from './index.js';
8
+ import { formatJson } from './formatters/json.js';
9
+ import { formatText } from './formatters/text.js';
10
+ import { formatMarkdown } from './formatters/markdown.js';
11
+ import { formatSarif } from './formatters/sarif.js';
12
+ function printHelp() {
13
+ console.log(`playwright-score — deterministic Playwright spec quality score (sqs-v1)
14
+
15
+ Usage:
16
+ playwright-score <paths...> [options]
17
+
18
+ Options:
19
+ --profile <standard|guardian> Scoring profile (default: standard)
20
+ --threshold <n> Pass threshold 0-100 (default: 80 standard / 75 guardian)
21
+ --format <text|json|markdown|sarif> Output format (default: text)
22
+ --out <file> Write report to file
23
+ --help Show help
24
+
25
+ Exit codes:
26
+ 0 score >= threshold
27
+ 1 score < threshold
28
+ 2 tool error
29
+
30
+ See METHODOLOGY.md for sqs-v1 formula.
31
+ `);
32
+ }
33
+ async function main() {
34
+ const args = process.argv.slice(2);
35
+ if (args.length === 0 || args.includes('--help') || args.includes('-h')) {
36
+ printHelp();
37
+ process.exit(args.length === 0 ? 2 : 0);
38
+ }
39
+ let profile = 'standard';
40
+ let threshold;
41
+ let format = 'text';
42
+ let out;
43
+ const paths = [];
44
+ for (let i = 0; i < args.length; i++) {
45
+ const a = args[i];
46
+ if (a === '--profile') {
47
+ profile = args[++i];
48
+ if (profile !== 'standard' && profile !== 'guardian') {
49
+ console.error(`Invalid profile: ${profile}`);
50
+ process.exit(2);
51
+ }
52
+ }
53
+ else if (a === '--threshold') {
54
+ threshold = Number(args[++i]);
55
+ if (Number.isNaN(threshold) || threshold < 0 || threshold > 100) {
56
+ console.error('threshold must be 0-100');
57
+ process.exit(2);
58
+ }
59
+ }
60
+ else if (a === '--format') {
61
+ const f = args[++i];
62
+ if (f !== 'text' && f !== 'json' && f !== 'markdown' && f !== 'sarif') {
63
+ console.error(`Invalid format: ${f}`);
64
+ process.exit(2);
65
+ }
66
+ format = f;
67
+ }
68
+ else if (a === '--out') {
69
+ out = args[++i];
70
+ }
71
+ else if (a.startsWith('-')) {
72
+ console.error(`Unknown option: ${a}`);
73
+ process.exit(2);
74
+ }
75
+ else {
76
+ paths.push(a);
77
+ }
78
+ }
79
+ if (paths.length === 0) {
80
+ console.error('At least one path is required');
81
+ process.exit(2);
82
+ }
83
+ try {
84
+ const result = await scorePaths({ paths, profile, threshold });
85
+ let body;
86
+ switch (format) {
87
+ case 'json':
88
+ body = formatJson(result);
89
+ break;
90
+ case 'markdown':
91
+ body = formatMarkdown(result);
92
+ break;
93
+ case 'sarif':
94
+ body = formatSarif(result);
95
+ break;
96
+ default:
97
+ body = formatText(result);
98
+ }
99
+ if (out) {
100
+ fs.mkdirSync(path.dirname(path.resolve(out)), { recursive: true });
101
+ fs.writeFileSync(out, body + '\n', 'utf8');
102
+ }
103
+ else {
104
+ console.log(body);
105
+ }
106
+ process.exit(result.pass ? 0 : 1);
107
+ }
108
+ catch (err) {
109
+ console.error(err instanceof Error ? err.message : err);
110
+ process.exit(2);
111
+ }
112
+ }
113
+ main();
@@ -0,0 +1,10 @@
1
+ import type { Finding, ProfileName } from './types.js';
2
+ /**
3
+ * Run ESLint once per scorePaths invocation with embedded config only.
4
+ * Does not load host-repo eslint.config.js.
5
+ */
6
+ export declare function runEslint(options: {
7
+ files: string[];
8
+ profile: ProfileName;
9
+ cwd?: string;
10
+ }): Promise<Finding[]>;
@@ -0,0 +1,120 @@
1
+ import path from 'node:path';
2
+ import { ESLint } from 'eslint';
3
+ import playwright from 'eslint-plugin-playwright';
4
+ import { guardianPlugin, guardianRuleConfigs, } from './rules/guardian-plugin.js';
5
+ import { mapRule } from './profiles.js';
6
+ function buildConfig(profile) {
7
+ const playwrightRecommended = playwright.configs?.['flat/recommended'] ??
8
+ playwright.configs?.recommended;
9
+ const base = [
10
+ {
11
+ files: ['**/*.{ts,tsx,js,jsx,mjs,cjs}'],
12
+ languageOptions: {
13
+ ecmaVersion: 2022,
14
+ sourceType: 'module',
15
+ parser: undefined, // set below if available
16
+ },
17
+ plugins: {
18
+ playwright,
19
+ ...(profile === 'guardian' ? { guardian: guardianPlugin } : {}),
20
+ },
21
+ rules: {
22
+ // Ensure key hygiene rules even if recommended set differs by version
23
+ 'playwright/no-wait-for-timeout': 'error',
24
+ 'playwright/no-networkidle': 'error',
25
+ 'playwright/no-force-option': 'error',
26
+ 'playwright/no-wait-for-selector': 'error',
27
+ 'playwright/no-wait-for-navigation': 'error',
28
+ 'playwright/missing-playwright-await': 'error',
29
+ 'playwright/expect-expect': 'error',
30
+ 'playwright/no-focused-test': 'error',
31
+ 'playwright/no-skipped-test': 'warn',
32
+ 'playwright/no-raw-locators': 'warn',
33
+ 'playwright/prefer-web-first-assertions': 'warn',
34
+ ...(profile === 'guardian' ? { ...guardianRuleConfigs } : {}),
35
+ },
36
+ },
37
+ ];
38
+ // Prefer spreading official recommended flat config when available
39
+ if (Array.isArray(playwrightRecommended)) {
40
+ const configs = playwrightRecommended;
41
+ if (profile === 'guardian') {
42
+ return [
43
+ ...configs,
44
+ {
45
+ plugins: { guardian: guardianPlugin },
46
+ rules: { ...guardianRuleConfigs },
47
+ },
48
+ // our severity tweaks
49
+ base[0],
50
+ ];
51
+ }
52
+ return [...configs, base[0]];
53
+ }
54
+ return base;
55
+ }
56
+ function toSeverity(eslintSeverity, override) {
57
+ if (override)
58
+ return override;
59
+ if (eslintSeverity === 2)
60
+ return 'error';
61
+ if (eslintSeverity === 1)
62
+ return 'warning';
63
+ return 'info';
64
+ }
65
+ /**
66
+ * Run ESLint once per scorePaths invocation with embedded config only.
67
+ * Does not load host-repo eslint.config.js.
68
+ */
69
+ export async function runEslint(options) {
70
+ const cwd = options.cwd ?? process.cwd();
71
+ const overrideConfig = buildConfig(options.profile);
72
+ // Try to attach typescript-eslint parser for .ts files
73
+ try {
74
+ const tsParser = await import('@typescript-eslint/parser');
75
+ for (const cfg of overrideConfig) {
76
+ if (cfg.files) {
77
+ cfg.languageOptions = {
78
+ ...cfg.languageOptions,
79
+ parser: tsParser,
80
+ parserOptions: {
81
+ ecmaVersion: 2022,
82
+ sourceType: 'module',
83
+ },
84
+ };
85
+ }
86
+ }
87
+ }
88
+ catch {
89
+ // parser optional for pure JS
90
+ }
91
+ const eslint = new ESLint({
92
+ cwd,
93
+ // Prevent loading ambient project ESLint configs (ESLint 9 flat API)
94
+ overrideConfigFile: true,
95
+ overrideConfig: overrideConfig,
96
+ // Ignore host ignore files that might exclude generated specs
97
+ ignore: false,
98
+ });
99
+ const results = await eslint.lintFiles(options.files);
100
+ const findings = [];
101
+ for (const result of results) {
102
+ const file = path.resolve(result.filePath);
103
+ for (const msg of result.messages) {
104
+ if (!msg.ruleId)
105
+ continue;
106
+ const mapping = mapRule(msg.ruleId);
107
+ findings.push({
108
+ rule: msg.ruleId,
109
+ severity: toSeverity(msg.severity, mapping.severityOverride),
110
+ message: msg.message,
111
+ file,
112
+ line: msg.line,
113
+ column: msg.column,
114
+ dimension: mapping.dimension,
115
+ reportOnly: mapping.reportOnly,
116
+ });
117
+ }
118
+ }
119
+ return findings;
120
+ }
@@ -0,0 +1,2 @@
1
+ import type { ScoreResult } from '../types.js';
2
+ export declare function formatJson(result: ScoreResult): string;
@@ -0,0 +1,3 @@
1
+ export function formatJson(result) {
2
+ return JSON.stringify(result, null, 2);
3
+ }
@@ -0,0 +1,2 @@
1
+ import type { ScoreResult } from '../types.js';
2
+ export declare function formatMarkdown(result: ScoreResult): string;
@@ -0,0 +1,26 @@
1
+ export function formatMarkdown(result) {
2
+ const lines = [];
3
+ lines.push(`# Playwright Spec Score (\`${result.scoreVersion}\`)`);
4
+ lines.push('');
5
+ lines.push(`**${result.score}/100 (${result.grade})** — ${result.pass ? 'PASS' : 'FAIL'} · profile \`${result.profile}\` · threshold ${result.threshold}`);
6
+ lines.push('');
7
+ lines.push('| Dimension | Score |');
8
+ lines.push('|---|---|');
9
+ for (const [k, v] of Object.entries(result.dimensions)) {
10
+ if (v === undefined)
11
+ continue;
12
+ lines.push(`| ${k} | ${v} |`);
13
+ }
14
+ lines.push('');
15
+ lines.push(`Files: ${result.summary.files} · Tests: ${result.summary.tests} · SLOC: ${result.summary.sloc} · Findings: ${result.summary.findings}`);
16
+ if (result.findings.length) {
17
+ lines.push('');
18
+ lines.push('## Findings');
19
+ lines.push('');
20
+ for (const f of result.findings.slice(0, 40)) {
21
+ const loc = f.line ? `:${f.line}` : '';
22
+ lines.push(`- **${f.severity}** \`${f.rule}\` \`${f.file}${loc}\` — ${f.message}`);
23
+ }
24
+ }
25
+ return lines.join('\n');
26
+ }
@@ -0,0 +1,3 @@
1
+ import type { ScoreResult } from '../types.js';
2
+ /** Minimal SARIF 2.1.0 for CI integrations */
3
+ export declare function formatSarif(result: ScoreResult): string;
@@ -0,0 +1,55 @@
1
+ /** Minimal SARIF 2.1.0 for CI integrations */
2
+ export function formatSarif(result) {
3
+ const rulesMap = new Map();
4
+ for (const f of result.findings) {
5
+ if (!rulesMap.has(f.rule)) {
6
+ rulesMap.set(f.rule, {
7
+ id: f.rule,
8
+ shortDescription: { text: f.rule },
9
+ });
10
+ }
11
+ }
12
+ const sarif = {
13
+ $schema: 'https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json',
14
+ version: '2.1.0',
15
+ runs: [
16
+ {
17
+ tool: {
18
+ driver: {
19
+ name: 'playwright-score',
20
+ informationUri: 'https://github.com/qaguardian/playwright-score',
21
+ version: result.scoreVersion,
22
+ rules: [...rulesMap.values()],
23
+ },
24
+ },
25
+ results: result.findings.map((f) => ({
26
+ ruleId: f.rule,
27
+ level: f.severity === 'error'
28
+ ? 'error'
29
+ : f.severity === 'warning'
30
+ ? 'warning'
31
+ : 'note',
32
+ message: { text: f.message },
33
+ locations: [
34
+ {
35
+ physicalLocation: {
36
+ artifactLocation: { uri: f.file },
37
+ region: f.line
38
+ ? { startLine: f.line, startColumn: f.column ?? 1 }
39
+ : undefined,
40
+ },
41
+ },
42
+ ],
43
+ })),
44
+ properties: {
45
+ score: result.score,
46
+ grade: result.grade,
47
+ pass: result.pass,
48
+ profile: result.profile,
49
+ threshold: result.threshold,
50
+ },
51
+ },
52
+ ],
53
+ };
54
+ return JSON.stringify(sarif, null, 2);
55
+ }
@@ -0,0 +1,2 @@
1
+ import type { ScoreResult } from '../types.js';
2
+ export declare function formatText(result: ScoreResult): string;