@qaguardian/playwright-score 1.0.0 → 2.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/CHANGELOG.md ADDED
@@ -0,0 +1,817 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented here. Any change that
4
+ alters what a given suite scores *by changing a rule's weight or
5
+ severity* is a new scoring-model version (see
6
+ [METHODOLOGY.md](./METHODOLOGY.md)), not a quiet patch. A release that
7
+ leaves every rule's weight and severity untouched but changes which files
8
+ or which test names get recognized in the first place — discovery and
9
+ parsing accuracy, not grading policy — can still move a suite's score
10
+ without itself being a new model version; 2.1.0 below is exactly that
11
+ case, and says so explicitly.
12
+
13
+ ## 2.1.0 — 2026-09-24
14
+
15
+ Scoring model stays v4 — nothing here changes a rule's weight or
16
+ severity, only whether a suite is *recognized* correctly in the first
17
+ place (see the note at the top of this file reconciling that with "any
18
+ score-changing release is a new model version"). Every fix below made at
19
+ least one real corpus repo's score more accurate — not uniformly higher,
20
+ and not less strict or more lenient by policy; see "Behaviour change"
21
+ below and VALIDATION.md's "Bugs found this way, and fixed" for the full
22
+ story (corpus repo, exact before/after) behind each one.
23
+
24
+ ### Fixed
25
+ - **Custom `test.extend()` fixture names.** A suite that always declares
26
+ its tests via a custom-named fixture (`const loggedTest =
27
+ test.extend(...)`, imported into every spec file — sometimes through a
28
+ re-export barrel) had every `expect()` inside a completely normal test
29
+ flagged by upstream `eslint-plugin-playwright`'s `no-standalone-expect`
30
+ and sibling rules (which key off the literal identifier `test`, and
31
+ can only dereference a same-file extend chain), and undercounted by
32
+ this package's own test-span/`countTests` logic too.
33
+ `src/test-aliases.ts` resolves these aliases — same-file and
34
+ cross-file, through imports and re-export barrels — and feeds both
35
+ `eslint-plugin-playwright`'s `globalAliases.test` setting and this
36
+ package's own AST rules. Scores rise for affected suites:
37
+ `argos-ci/argos` 57/F → 95/A, `apache/superset` 70/C → 77/C.
38
+ - **Discovery now honours the target's own `playwright.config.*`
39
+ `testDir`/`testMatch`/`testIgnore`.** Spec discovery previously matched
40
+ by filename suffix only, so a directory holding a different test
41
+ runner's files under a similar naming convention got swept in
42
+ alongside the real suite. `src/playwright-config.ts` statically parses
43
+ (never executes — a scored repo's config is untrusted input) the
44
+ nearest `playwright.config.{ts,js,mjs,cjs}` at or above the scanned
45
+ directory and scopes discovery to what the target itself declares.
46
+ When no config is found at all, this falls back to the previous
47
+ filename-based behavior silently, exactly as before this release; when
48
+ a config *is* found but can't be read statically (a dynamic value or
49
+ an unsupported shape, e.g. a `RegExp` `testMatch`), it falls back the
50
+ same way but now also adds an entry to the new, additive, optional
51
+ `ScoreResult.configWarnings` field so the caller can see why. Includes
52
+ three fixes found making this work against real configs: matching
53
+ Playwright's own `createFileMatcher` auto-`**/`-prefix behavior for a
54
+ `testMatch`/`testIgnore` pattern with no leading `**/`; globbing from
55
+ the config's `testDir` rather than the caller's own (possibly narrower
56
+ or differently-rooted) scanned directory; and failing *open* (keeping a
57
+ file) rather than treating an unresolvable relative import as proof a
58
+ file isn't a real spec, since a sparse/partial checkout can legitimately
59
+ be missing a fixtures module the real project has. This moves a
60
+ suite's score in *either* direction depending on whether the old
61
+ filename-only discovery had been over- or under-matching relative to
62
+ what the config itself declares — see "Behaviour change" below for a
63
+ concrete case where it lowers a score.
64
+ - **Discovery no longer misses a plain CommonJS suite.** A follow-on to
65
+ the config-scoped discovery fix above: the safety net that confirms a
66
+ config-matched file transitively imports `@playwright/test` only
67
+ walked ES `import`/`export ... from` syntax, so a suite written
68
+ entirely in CommonJS (`require()`, no `import` anywhere) read as
69
+ *definitive negative evidence* of not being a Playwright spec — the
70
+ opposite of the truth — and hard-failed to 0 files the moment
71
+ config-scoped discovery activated for it. Fixed by also recognizing a
72
+ top-level `require('x')` call the same way as an ES import.
73
+ - **Config discovery and a config's `testDir` can no longer escape the
74
+ scanned repo.** Hardening fixes for a public tool that reads an
75
+ untrusted repo's own config. `findPlaywrightConfig` stops walking
76
+ upward at the first ancestor directory containing `.git` (a directory
77
+ for a normal clone, a file for a worktree/submodule), so a coincidental
78
+ `playwright.config.*` sitting above the actual repo root — a shared CI
79
+ runner, a monorepo checkout nested inside another one — is never picked
80
+ up (this guarantee needs a `.git` somewhere in the ancestor chain).
81
+ The bug this closes: when no `.git` existed anywhere in the ancestor
82
+ chain, the boundary used to clamp a config's `testDir` fell back to the
83
+ filesystem root (or 20 levels up), which is effectively no clamp at
84
+ all, so a `testDir: '/'` could make the glob walk the whole filesystem
85
+ (in the worst case, a hang). For the `testDir` clamp the fallback is
86
+ now the directory actually passed to the scorer, or the config file's
87
+ own directory when that's the wider of the two, never the filesystem
88
+ root. The config search itself (a bounded series of existence checks)
89
+ keeps its wider fallback so monorepo configs above the scanned
90
+ directory are still found.
91
+ Symlinks got the same treatment: a `testDir` is now checked *after
92
+ resolving symlinks* on both sides of the ancestor check, not just
93
+ lexically, so a `testDir` that's a symlink to somewhere outside the
94
+ repo can't launder an escape the plain path comparison would have
95
+ missed (the glob's own `follow` option, already `false` by default for
96
+ `**` patterns, is also now pinned explicitly). An escaping `testDir` —
97
+ absolute (`'/'`), climbing out (`'../../..'`), or via a symlink — gets a
98
+ `configWarnings` entry and falls back to filename-based discovery
99
+ rather than globbing the clamped repo root with testMatch/testIgnore
100
+ patterns written for a different directory. To be clear about the
101
+ actual exposure this closes: a scored *result* was never able to
102
+ include a file whose path lies outside the scan root either way (a
103
+ symlinked file inside the repo is a separate, tracked follow-up) — index.ts always
104
+ filters expanded matches back down to the caller's own requested
105
+ directory — the risk was the walk/glob itself reading (or hanging on)
106
+ parts of the filesystem well outside the repo, not a leaked finding in
107
+ the output. Neither case is exercised by any corpus repo (a legitimate
108
+ config never does this); found and fixed during this release's own
109
+ review, not from a corpus regression.
110
+
111
+ ### Behaviour change
112
+ - **Config-scoped discovery can lower a suite's score, not just raise
113
+ it.** Scoping to what a target's own `playwright.config.*` actually
114
+ declares is strictly more accurate than the old filename-suffix match,
115
+ but "more accurate" isn't the same as "friendlier": `mattermost/
116
+ mattermost` moves **84/B → 79/C (a pass→fail flip)** because its
117
+ top-level config's `testDir` doesn't cover the real, correctly-written
118
+ `upgrade-specs/` suite it runs only through per-project `testDir`
119
+ overrides — `projects[].testDir` isn't read yet (see VALIDATION.md's
120
+ "Known limitations"). A future config parser that reads
121
+ `projects[].testDir` would recover this repo's full suite and its
122
+ higher score.
123
+
124
+ ### Corpus
125
+ Grown from 50 (2.0.0) to 100 public suites: 34 added in an interim,
126
+ unpublished pass (84 total — see VALIDATION.md for the full accounting of
127
+ that pass), then 16 more here for brand
128
+ recognition (Nextcloud, Ghost, Pinterest, Microsoft, The Guardian,
129
+ Shopify ×2, Adobe, Automattic, BBC, Google, Datadog, Twilio) plus three
130
+ named competitors (Checkly, Currents, LambdaTest), scored under the
131
+ exact same rules as everyone else. Every one of the 100 entries in
132
+ VALIDATION.md now carries the exact commit SHA scored and the date it
133
+ was tested, not just an aggregate "last run" date.
134
+
135
+ ## 2.0.0 — 2026-09-23
136
+
137
+ **Scoring model v4 — `standard` scores change.** Owner direction: public
138
+ scores should reflect quality, full stop, not be softened to avoid moving
139
+ anyone's number ("I don't care who fails"). This supersedes the 1.1.0
140
+ plan of shipping these same rules as report-only under `standard`; 1.1.0
141
+ was never published. See METHODOLOGY.md's "Why v4" for the full
142
+ reasoning and VALIDATION.md for the complete before/after table across
143
+ all 50 corpus repos, including every pass→fail flip.
144
+
145
+ Corpus expanded from 17 to 50 real public repos (see
146
+ `scripts/validate-corpus.sh`) — every rule below was checked against real
147
+ findings across all 50 before landing, not just the fixtures.
148
+
149
+ ### Scores changed
150
+ `standard` scores are **not** unchanged in this release (that was 1.1.0's
151
+ promise; it's void under v4). Against the last actually-published version
152
+ (1.0.0, model v3, none of these rules): 20/50 corpus repos move. Net
153
+ **+22** across the corpus, but that's almost entirely Flagsmith's `.pw.ts`
154
+ discovery fix alone (+62, previously hard-failed at 0 — its 20-spec suite
155
+ wasn't found at all). Excluding Flagsmith, the other 19 movers are **net
156
+ −40** (mean −2.1/repo) — real, quality-driven drops. Biggest: tldraw −9,
157
+ AFFiNE −5, activepieces −5, microsoft/vscode −4, sencho −2, cal.com −2,
158
+ then thirteen repos at −1. **Three pass→fail flips** at the default
159
+ threshold (80): `payloadcms/payload` (80→79), `microsoft/vscode`
160
+ (81→77), `tldraw/tldraw` (81→72). Pass rate: 37/50 (74%) → 34/50 (68%).
161
+ See VALIDATION.md for the full per-repo table and the "Known
162
+ limitations" this release documents rather than papers over. Two
163
+ acceptance bars now guard against these rules over-correcting: Playwright's
164
+ own official examples must still score >= 90/A (see
165
+ `tests/official-examples.test.ts`, vendored from microsoft/playwright),
166
+ and a hand-written "gold" fixture must still reach exactly 100.
167
+
168
+ ### Added — four rules narrowing the gameability gap (scored under `standard`)
169
+ Narrowing, not closing: each rule matches specific known patterns (see the
170
+ respellings each one closed in this same release), not the general class
171
+ of "code that fakes stability or coverage" — a determined author can
172
+ likely still find an unmatched spelling. Real signal on real code, not a
173
+ guarantee nothing slips through.
174
+ - **`pwscore/no-timer-sleep`** (error) — flags a hard-coded sleep
175
+ disguised past `playwright/no-wait-for-timeout`: `await new
176
+ Promise((resolve) => setTimeout(resolve, ms))`, its `globalThis.
177
+ setTimeout` and wrapped-callback (`setTimeout(() => resolve(), ms)`)
178
+ respellings, and the unrelated but equally invisible `node:timers/
179
+ promises` sleep (`await setTimeout(ms)`).
180
+ - **`pwscore/no-coordinate-click`** (**warning**, not error — see below)
181
+ — flags coordinate-based mouse interaction (`page.mouse.click/dblclick/
182
+ move/down/up(x, y)`, including a destructured `const { mouse } = page`
183
+ binding) in place of a locator. **Known false positive, mitigated, not
184
+ eliminated**: this is also the *correct* way to interact with
185
+ canvas-based apps (whiteboard/drawing-tool suites with no addressable
186
+ DOM element) — two infinite-canvas editors in the corpus (tldraw,
187
+ AFFiNE) account for 428/459 of this rule's hits. Demoted to a
188
+ warning-level (0.4, not 1.0) demerit rather than skipped for canvas
189
+ apps by heuristic, which would itself become a new gameability surface
190
+ — see METHODOLOGY.md's "Why v4" and VALIDATION.md's "Known
191
+ limitations".
192
+ - **`pwscore/no-trivial-assertion`** (error) — flags an assertion whose
193
+ subject and matcher combination can only ever pass regardless of
194
+ anything the app under test does (`expect(true).toBe(true)`,
195
+ `expect([]).toEqual([])`, `` expect(`x`).toBe(`x`) ``, `expect(1 +
196
+ 1).toBe(2)`). Always passes, satisfies `playwright/expect-expect`,
197
+ verifies nothing about the app. Never flags a deliberate force-fail
198
+ assertion that always *fails* (`expect(true).toBe(false)`,
199
+ `expect(true, msg).toBeFalsy()`).
200
+ - **`pwscore/no-soft-assertion-only-test`** (warning) — flags a test
201
+ whose every assertion is `expect.soft(...)`, so a failed check never
202
+ fails the run. `expect.poll(...)` counts as a hard assertion (a failed
203
+ poll still throws). Soft assertions are a legitimate, deliberate choice
204
+ (gather every problem in one pass) in many suites, but a warning-level
205
+ (not error) demerit as the default now that it's scored, not report-
206
+ only.
207
+
208
+ ### Added — inline `eslint-disable` comments are now a finding, not invisible
209
+ `pwscore/eslint-disable-comment` (warning) flags every ESLint suppression-
210
+ directive comment (`eslint-disable`, `eslint-disable-line`,
211
+ `eslint-disable-next-line`, block or line form) in a scored file — a real
212
+ demerit, same as everything else in this release. **Known limitation**:
213
+ it reports the presence of the comment, not which specific rule or
214
+ finding it silences — see VALIDATION.md.
215
+
216
+ ### Fixed — an inline `eslint-disable` comment could still silence every finding it named
217
+ Found in review, before this ever shipped: adding the finding above was
218
+ not enough on its own — every rule this package enables is an ordinary
219
+ ESLint rule, and ESLint honors an inline disable comment by default, so
220
+ `/* eslint-disable */` on line 1 still silenced everything it named for
221
+ scoring purposes, same as it always had. A 10-test file (a hard wait plus
222
+ a tautological assertion in every test) scored 35/F; the same file with
223
+ that one comment on line 1 scored **99/A** — the new
224
+ `pwscore/eslint-disable-comment` finding was reported, but the four real
225
+ findings it was covering for were simply gone. ESLint 9's flat-config
226
+ `linterOptions.noInlineConfig` (see `eslint-runner.ts`) now makes every
227
+ inline directive comment inert for this package's rule set: the comment
228
+ itself is still scored via `pwscore/eslint-disable-comment`, but it can
229
+ no longer also make the finding it targets disappear. The "Known
230
+ limitation" above is narrower as a result — a disable comment can no
231
+ longer silence anything this package's own rules find, it just isn't
232
+ attributed to which specific rule it named.
233
+
234
+ ### Removed — `strict` profile
235
+ 1.1.0 (never published) planned a `strict` profile to gate the four rules
236
+ above and the `eslint-disable-comment` finding behind an opt-in, leaving
237
+ `standard` unchanged. With all of them scored under `standard` directly
238
+ as of v4, `strict` would be byte-for-byte identical to `standard` —
239
+ nothing left to gate. `--profile strict` on the CLI now errors with a
240
+ migration message; the library API degrades a legacy `'strict'` string
241
+ to `standard` weights, same fallback as the `guardian` profile's removal
242
+ in 1.0.0.
243
+
244
+ ### Fixed — `.pw.ts` spec discovery
245
+ Directory/glob discovery now recognizes `*.pw.ts` (joining `.spec.`,
246
+ `.test.`, `.e2e.`, `.e2e-spec.`, `.e2e-test.`) — Flagsmith's entire
247
+ 20-spec `frontend/e2e` suite (`billing-test.pw.ts`, `flag-tests.pw.ts`,
248
+ ...) previously hard-failed as "no files matched" despite being a fully
249
+ healthy, populated suite, the same failure mode `.e2e.ts`/`.e2e-spec.ts`
250
+ were fixed for in 1.0.0 and its predecessors.
251
+
252
+ ### Added — acceptance bars: official examples score high, 100 is reachable
253
+ `tests/official-examples.test.ts` vendors unmodified (attribution header
254
+ only) specs from microsoft/playwright (Apache-2.0; see
255
+ `fixtures/playwright-official/NOTICE.md`) and asserts each scores >= 90
256
+ and grades A under `standard` — three `examples/todomvc` specs (100/A
257
+ each) and `examples/github-api/tests/test-api.spec.ts` (97/A). Checked
258
+ that the 97, not 100, isn't an unfair penalty: the file's only finding is
259
+ `pwscore/eslint-disable-comment` on its own `/* eslint-disable
260
+ notice/notice */` licence-header lint escape (Playwright's monorepo
261
+ convention, unrelated to Playwright/test quality — see NOTICE.md), −3
262
+ points (structure dimension 100→80, weighted). That's correct, not a
263
+ bug: this project scores every suppression-directive comment as a real,
264
+ unconditional demerit regardless of what it's aimed at (see the "Added —
265
+ inline eslint-disable comments" entry above and VALIDATION.md's Known
266
+ limitations) — a licence-header opt-out is still, mechanically, an
267
+ `eslint-disable` comment. 97/A comfortably clears the >= 90 bar either
268
+ way.
269
+ `examples/svgomg/tests/example.spec.ts` was investigated and excluded
270
+ (79/C, genuine non-idiomatic CSS/text-selector usage predating current
271
+ Playwright locator guidance, not a scorer bug — documented in NOTICE.md).
272
+ A new hand-written `fixtures/gold.spec.ts` scores exactly 100 with zero
273
+ findings, proving 100 is reachable, not just a theoretical ceiling.
274
+
275
+ ## 1.0.0 — 2026-08-28
276
+
277
+ First stable release. The scoring model (v3), CLI contract, library API,
278
+ and GitHub Action inputs/outputs are now considered stable — breaking
279
+ changes to any of them mean a major version bump from here on.
280
+
281
+ ### Fixed — locator ratio
282
+ - **`getByAltText` and `getByTitle` now count as native locators.** They
283
+ are part of Playwright's own `getBy*` family but were missing from the
284
+ native set, so an image-heavy suite using alt/title locators plus a
285
+ single `.locator()` fallback scored 0/100 on the locators dimension for
286
+ perfectly idiomatic code. Scores can only improve or stay the same under
287
+ this fix (the native count grows; raw counting is unchanged). The
288
+ 17-suite validation corpus was re-run — see VALIDATION.md.
289
+
290
+ ### Added — public API for composing layers
291
+ - `getTestSpans` and `attributeFindingScopes` are now exported. A caller
292
+ layering its own rules on top of `standard` (an extra private dimension,
293
+ say) can attribute its findings to test/hook/module scope with the exact
294
+ logic the package uses internally, then score the dimension with the
295
+ already-exported `ratioDimensionScore` — reused, not reimplemented, so
296
+ the math can't drift.
297
+
298
+ ## 0.5.0 — 2026-08-27
299
+
300
+ ### Changed — scoring model v3: "the weighted share of your tests that are clean"
301
+ - **The density model is gone.** v1/v2 scored findings per 25 lines of
302
+ code through an exponential decay with a minimum-slots floor. That was
303
+ unexplainable ("0.5 penalty units per slot"), structurally lenient on
304
+ small suites, and gameable — padding a suite with clean lines diluted
305
+ the penalty. All of it (K, slot divisor, minimum slots, per-rule caps,
306
+ `e^-x`) is removed.
307
+ - **Model v3**: every finding is attributed to the test it sits in via AST
308
+ test spans. A test accumulates demerits per dimension (error 1.0,
309
+ warning 0.4, capped at 1 — a test is at worst fully flawed); findings in
310
+ before/after hooks demerit every test in the file; module-level findings
311
+ count once per file. Each dimension is
312
+ `100 × (1 − demerits/tests)`; locators stays the native/raw usage
313
+ ratio; final score is the weighted sum (40/25/20/15), unchanged grades.
314
+ - The motivating example (3 tests: two hard waits, two assertion-free, one
315
+ skipped) scores **53/F** — it was 78/C under v1 density math and 64/D
316
+ under v2.
317
+ - **In-house acronyms removed**: the score version is now plainly `v3`
318
+ (JSON `scoreVersion: "v3"`); "sqs-*" no longer appears anywhere in
319
+ output, docs, or code.
320
+ - `summary.cleanTests` added (tests with no demerit in any dimension);
321
+ text output now prints `clean tests=N/M`.
322
+ - **Removed exports**: `SQS_V1`, `SQS_V2`, `penaltyDimensionScore`,
323
+ `applyPerRuleCap` (the density machinery). `MODEL` and
324
+ `ratioDimensionScore` replace them. Callers composing house rules on
325
+ top (QA Guardian's `playwright_runner`) should migrate when they bump.
326
+
327
+ ## 0.4.0 — 2026-08-27
328
+
329
+ ### Changed — score version `sqs-v2`
330
+ - **Assertions dimension is now a per-test coverage ratio** instead of a
331
+ finding-density penalty. sqs-v1's density math misread assertion
332
+ coverage badly on small suites: 2 of 3 tests asserting nothing scored
333
+ 82/100 on the dimension (and the overall example suite 78/C) because two
334
+ findings in a tiny file barely register per-SLOC. Whether a test asserts
335
+ anything is a fraction of tests, not a smell rate — so, like the
336
+ locators dimension, the primary signal is now
337
+ `coverage = assertedTests / tests` (an **uncapped** census of
338
+ `playwright/expect-expect` findings, taken after all delegation
339
+ resolution), decayed multiplicatively by the density load of the
340
+ remaining assertion-quality rules (`valid-expect`,
341
+ `no-standalone-expect`, `prefer-web-first-assertions`). The motivating
342
+ example now scores the dimension 33 and the suite 64/D.
343
+ - Everything else — weights, hygiene/structure density math, locator
344
+ ratio, constants, grades — is unchanged. Per METHODOLOGY's change
345
+ policy this is a new score version: results now report `sqs-v2`.
346
+ - `summary.unassertedTests` added to the JSON output; the text formatter
347
+ prints `tests asserting=N/M`.
348
+ - `SQS_V1` export is deprecated and aliases `SQS_V2` (identical
349
+ constants); it will be removed in a future release.
350
+
351
+ ## 0.3.0 — 2026-08-27
352
+
353
+ ### Fixed
354
+ - **Nine documented sqs-v1 rules never ran.** `buildConfig` spread
355
+ eslint-plugin-playwright's `flat/recommended` config only when it was an
356
+ array, but the plugin exports a single config object — so the whole
357
+ recommended set was silently dropped, and every rule that relied on it
358
+ was inert. Concretely: `no-conditional-in-test`, `no-conditional-expect`,
359
+ `no-element-handle`, `no-eval`, `no-page-pause`, `no-useless-await`,
360
+ `valid-expect`, `no-standalone-expect`, and `max-nested-describe` were
361
+ all mapped to dimensions in `profiles.ts` (and METHODOLOGY.md's hygiene
362
+ dimension explicitly lists "handles, conditionals") yet could never
363
+ produce a finding. A spec whose tests were built entirely out of
364
+ `if (await locator.isVisible())` branches scored a clean 100.
365
+ - The fix enables the intended rule set **explicitly and frozen** in
366
+ `eslint-runner.ts` rather than re-spreading upstream `recommended`:
367
+ eslint-plugin-playwright is a `^` range dependency, and inheriting its
368
+ recommended set at install time would let identical code score
369
+ differently across installs. Severities mirror upstream flat/recommended
370
+ (2.x). `require-top-level-describe` remains deliberately disabled
371
+ (stylistic; not in upstream recommended; would penalize idiomatic
372
+ describe-less suites).
373
+
374
+ ### Scoring impact
375
+ - The formula, weights, and constants are unchanged — this restores the
376
+ *documented* sqs-v1 rule inputs, so the score version stays `sqs-v1`.
377
+ Suites that use conditional test logic, element handles, `page.pause()`,
378
+ or malformed/standalone `expect` calls will score **lower** than under
379
+ 0.2.0 (which could not see those findings at all). Clean suites are
380
+ unaffected — all `good-*` fixtures score identically.
381
+
382
+ ## 0.2.0 — 2026-08-19
383
+
384
+ ### Removed
385
+ - **The `guardian` profile.** It was QA Guardian's own internal codegen
386
+ house-rules layer — arbitrary product-specific conventions (e.g.
387
+ "timeouts must be exactly 2000 or 20000ms") never claimed as official
388
+ Playwright doctrine, and not part of this package's actual differentiated
389
+ value (the aggregate score, locator-ratio tracing, assertion-delegation
390
+ resolution — all in `standard`). Shipping one company's house rules
391
+ inside a general-purpose public tool undermined the "this is a neutral
392
+ community linter" positioning the rest of this project has been built
393
+ around. `ProfileName` is now `'standard'` only; `--profile guardian` on
394
+ the CLI now exits 2 with an explicit message pointing here instead of
395
+ silently doing something unexpected. Passing an unrecognized profile
396
+ string programmatically (e.g. a caller still pinned to the old contract)
397
+ falls back to `standard`'s weights rather than throwing — a defensive
398
+ guard, not an invitation to keep using it.
399
+ QA Guardian's own `playwright_runner` now runs its private house-rules
400
+ layer separately, composed with this package's public `standard`
401
+ profile — verified byte-for-byte identical output to the old built-in
402
+ `guardian` profile across every fixture and 15 real production specs
403
+ before cutover. `penaltyDimensionScore`/`applyPerRuleCap` are now
404
+ exported (they always existed internally) specifically so that
405
+ composition — or anyone else building a similar house-rules layer on
406
+ top of `standard` — doesn't have to reimplement the scoring math.
407
+ - `scripts/dogfood.sh` — it reached into a relative sibling-repo path that
408
+ only exists inside this project's private internal monorepo, and
409
+ defaulted to the now-removed `guardian` profile. Internal-only tooling
410
+ that never belonged in a public repo; `scripts/validate-corpus.sh`
411
+ (real public repos, no internal paths) is the tool for this now.
412
+
413
+ ## 0.1.14 — 2026-08-19
414
+
415
+ ### Fixed
416
+ - **CLI stdout truncation on large output.** `console.log(body)` immediately
417
+ followed by `process.exit()` raced Node's async pipe write against
418
+ process termination — for output over ~64KB (a real suite's 1,215-finding
419
+ JSON report, ~370KB), the write was silently cut at exactly the pipe
420
+ buffer boundary when stdout wasn't a TTY (piped to `jq`, a CI log
421
+ processor, anything). No error, just truncated, invalid output. Found
422
+ while building `scripts/validate-corpus.sh` — reproduced with `--out
423
+ file.json` (full 376,920 bytes) vs. piped (exactly 65,536 bytes) against
424
+ the same input. Fixed by using `process.exitCode` instead of
425
+ `process.exit()` everywhere in the CLI, which lets Node exit naturally
426
+ once pending I/O actually drains — the standard fix for this class of
427
+ bug. 3 new tests spawn the real CLI binary as a child process (the only
428
+ way to reproduce a stdout-pipe bug — the existing suite only ever called
429
+ `scorePaths()` in-process).
430
+ - **`.e2e-spec.`/`.e2e-test.` naming conventions weren't discovered either**
431
+ — the same gap as 0.1.13's `.e2e.` fix, one suffix short. Verified
432
+ against Immich: 13 real Playwright specs named `*.e2e-spec.ts`,
433
+ previously invisible, sitting alongside 31 *other* files using the
434
+ identical suffix for `vitest`/`supertest` backend tests — correctly
435
+ excluded by the existing `looksLikeNonPlaywrightTest` safety net once
436
+ discovery found them. `SPEC_GLOBS` now also matches
437
+ `*.e2e-spec.*`/`*.e2e-test.*`.
438
+
439
+ ### Added
440
+ - **`scripts/validate-corpus.sh`** — re-runnable real-world validation:
441
+ shallow/sparse-clones 17 real public repos fresh and scores each with the
442
+ local build. See `VALIDATION.md` for the current results and the story
443
+ behind every fix that came from running it.
444
+
445
+ ## 0.1.13 — 2026-08-19
446
+
447
+ ### Fixed
448
+ - **Directory/glob discovery only matched `*.spec.*`/`*.test.*` — a repo
449
+ using `.e2e.` to disambiguate end-to-end specs from unit tests scored a
450
+ hard 0/F ("no files matched") no matter how healthy its suite actually
451
+ was.** Verified against a real, large, well-known repo (cal.com:
452
+ `apps/web/playwright/` — 53 real Playwright specs, all named `*.e2e.ts`,
453
+ zero `*.spec.ts`/`*.test.ts`). `SPEC_GLOBS` now also matches
454
+ `*.e2e.{ts,tsx,js,jsx}`. Re-scored: 0/F, no files matched → 85/B, PASS,
455
+ 53 files / 278 tests found — and the findings are real (642
456
+ `no-raw-locators`, spot-checked against the actual source), not noise
457
+ introduced by the wider glob.
458
+ `.e2e.` is also Cypress's own scaffolding convention, so
459
+ `looksLikeNonPlaywrightTest` (the existing safety net for a broad glob
460
+ sweeping in a different framework's tests) gained an explicit Cypress
461
+ check — an import from `'cypress'`, or common `cy.*` command usage
462
+ (`cy.visit`, `cy.get`, `cy.contains`, ...) with no import at all, since
463
+ Cypress's globals are injected the same way React Testing Library's
464
+ `screen` often is. Matched on specific command names, not a bare `cy.`
465
+ prefix, so a Playwright spec with an unrelated local variable named `cy`
466
+ is never caught by mistake.
467
+ 9 new tests, full real-world corpus re-verified with zero regressions.
468
+
469
+ ## 0.1.12 — 2026-08-19
470
+
471
+ ### Fixed
472
+ - **Assertion-helper delegation was invisible for class-based Page Object
473
+ Models — `findLocalAssertionHelperNames` only recognized free functions
474
+ and `const`-bound arrows, never a class method.** Since almost every
475
+ real-world POM is written as a class, this meant the cross-file
476
+ assertion-delegation tracing shipped in 0.1.11 didn't actually reach the
477
+ common case. Now also collects `MethodDefinition` (`kind: 'method'`)
478
+ candidates.
479
+ - **`.waitFor({ state })` (state defaults to `'visible'`) is now recognized
480
+ as an assertion equivalent to `expect(...)`.** It exists only to verify
481
+ a state, unlike an action method that merely throws on failure as a
482
+ side effect — functionally the same as `expect(locator).toBeVisible()`.
483
+ Matched on the exact property name `waitFor`, never confused with
484
+ `waitForEvent`/`waitForURL`/`waitForResponse`/`waitForLoadState`/
485
+ `waitForTimeout`, none of which are assertions.
486
+ - **Delegation through more than one call is now resolved within a single
487
+ file** — e.g. a `waitForNotificationAndClose()` method calling
488
+ `this.waitForNotification()`, which itself calls `.waitFor()`, two
489
+ calls removed from anything expect-expect could previously see. A
490
+ bounded fixed-point pass: any candidate calling (by name, any receiver —
491
+ same matching eslint-plugin-playwright's own `assertFunctionNames`
492
+ option already uses) something already recognized is added too, until
493
+ no new names are found.
494
+ Verified against the real suite this was found in (n8n): its
495
+ `NotificationsPage.waitForNotificationAndClose` is exactly this shape,
496
+ two calls deep, defined in the same file as `waitForNotification`. Files
497
+ with an `expect-expect` finding dropped from ~15 to 5 across n8n's 256
498
+ specs; the remaining ones are a different, disclosed pattern (some tests
499
+ signal failure by throwing directly rather than asserting a state —
500
+ still open, not attempted here). Re-verified against the full real-world
501
+ corpus (Playwright, Supabase, Storybook, freeCodeCamp, OpenMRS,
502
+ patient-chart, 15 real Guardian specs) with zero regressions.
503
+
504
+ ## 0.1.11 — 2026-08-17
505
+
506
+ ### Fixed
507
+ - **Page Object Model suites read as 0/100 on the locators dimension even
508
+ when the suite uses native locators throughout.** A spec file that routes
509
+ all interaction through an imported class (`canvas.addNode(...)` instead
510
+ of `page.getByRole(...)` directly) has zero locator calls of its own —
511
+ the real `getByRole`/`getByTestId`/`.locator()` calls live in a separate
512
+ page-object file the scorer never looked at. Verified against a real,
513
+ large suite (n8n's own e2e tests: 256 spec files, 1,046 tests, every one
514
+ of them at 0 direct locator calls) — it scored 79/C and *failed* the
515
+ default threshold for this reason alone; the suite's actual page objects
516
+ use native locators almost exclusively.
517
+ `src/import-graph.ts` now follows a spec file's *relative* imports
518
+ (`./`, `../` only — never a bare package specifier, npm dependency, or
519
+ path alias, which would need a real module resolver to resolve safely)
520
+ to locate local dependency files within the scanned directory, and folds
521
+ their locator counts into the suite-level ratio. Dependency files are
522
+ never linted and never count toward SLOC/tests/findings — only their
523
+ locator calls are attributed to the suite. Bounded deliberately (depth
524
+ and file-count capped, confined to the scanned directory) so a large or
525
+ circular import graph can't blow up cost. Re-scoring n8n's suite with
526
+ this fix: 79/C (FAIL) → 95/A (PASS).
527
+ As a byproduct of the same infrastructure, assertion delegated to an
528
+ *imported* helper function (not just a same-file one, which already
529
+ worked — see 0.1.7) is now also recognized by `expect-expect`.
530
+ - Fixed a related bug found while building the above: the import-tracing
531
+ boundary was initially set to the scored spec files' own common
532
+ ancestor, which is *narrower* than the actual project directory whenever
533
+ specs live in a dedicated `tests/` folder sibling to `pages/`/
534
+ `fixtures/` (the normal shape for this pattern) — every import climbing
535
+ back out of `tests/` was rejected as "outside the boundary." Fixed by
536
+ widening the boundary to the directory the caller actually pointed the
537
+ scanner at. Does not apply when every input is an explicit file path
538
+ with no directory to widen to — see the new regression test documenting
539
+ that limitation.
540
+
541
+ ## 0.1.10 — 2026-08-15
542
+
543
+ ### Added
544
+ - **Reusable GitHub Action** (`action.yml` at repo root, usable as
545
+ `qa-guardian/playwright-score@v1`). Runs the scorer, writes a job summary
546
+ (always — no extra permissions needed), optionally posts/updates a sticky
547
+ PR comment with the full findings, and gates or warns on the threshold
548
+ via `mode: gate|warn` (mirrors the existing `SPEC_SCORE_MODE` convention
549
+ from the QA Guardian integration). Exposes `score`/`grade`/`pass` as step
550
+ outputs. Not yet published to the Marketplace or exercised in a live
551
+ workflow — validated locally by running the same commands/JSON parsing
552
+ the action performs against the built CLI, not by an actual GitHub
553
+ Actions run.
554
+ - **CI auto-publish** (`.github/workflows/publish.yml`): on push to `main`
555
+ touching `package.json`, builds, tests, and publishes to npm only if the
556
+ local version isn't already on the registry. Authenticates via npm's
557
+ **Trusted Publisher (OIDC)** flow (`id-token: write`, no stored token) —
558
+ chosen over a classic automation token specifically because npm is
559
+ restricting 2FA-bypass tokens for direct publishing starting January 2027;
560
+ OIDC isn't affected and has nothing to rotate or expire. Requires the
561
+ trusted publisher configured once on npmjs.com (package settings →
562
+ Trusted Publisher → GitHub Actions → repo `qa-guardian/playwright-score`,
563
+ workflow `publish.yml`) and npm CLI ≥11.5.1 / Node ≥22.14.0 in the runner
564
+ (both satisfied by `actions/setup-node@v4` with `node-version: '22'`).
565
+
566
+ ### Chore
567
+ - No functional change beyond the above. Version bump to verify the new
568
+ CI auto-publish pipeline (`.github/workflows/publish.yml`,
569
+ Trusted Publisher/OIDC) works end-to-end against the real npm registry.
570
+
571
+ ## 0.1.9 — 2026-08-15
572
+
573
+ ### Fixed
574
+ - **`playwright/prefer-locator` and `playwright/prefer-native-locators` were
575
+ mapped in `profiles.ts` but never actually enabled — completely inert.**
576
+ `prefer-locator` specifically catches Playwright's older direct-action API
577
+ (`page.click('#foo')` instead of a locator or native locator), arguably
578
+ the single most common raw-selector anti-pattern in naive/AI-generated
579
+ code. Also extended `countLocators`'s raw/native ratio to recognize this
580
+ pattern (`click`/`dblclick`/`hover`/`check`/`uncheck`/`tap`/`focus` called
581
+ with a string selector as the first argument — methods a real `Locator`
582
+ never accepts a string for, so this is unambiguous regardless of receiver
583
+ name; `fill`/`type`/`press`/`selectOption` are deliberately excluded since
584
+ their first argument is legitimately a string on a `Locator` too).
585
+ Verified manually: a realistic "naive AI-generated" test sample using
586
+ nothing but `page.click(selector)`/`page.type(selector, ...)` went from a
587
+ misleading 85/B (PASS) to a correct 65/D (FAIL). Re-validated against the
588
+ full real-world corpus (~130 files across freeCodeCamp, OpenMRS, patient
589
+ chart, and Storybook e2e suites, plus 15 real Guardian specs) — no new
590
+ false positives.
591
+
592
+ ## 0.1.8 — 2026-08-15
593
+
594
+ ### Fixed
595
+ - **Directory/glob scanning did not exclude `node_modules` (or `dist`,
596
+ `build`, `.next`, `coverage`, and similar vendored/generated
597
+ directories).** Verified as a real bug: running the CLI against a
598
+ project root that has any dependency shipping its own `*.spec.ts`/
599
+ `*.test.ts` files silently swept those in alongside real specs. Every
600
+ comparable tool in this space (ESLint, Prettier, Jest) default-excludes
601
+ these directories for the same reason. Re-verified against a real
602
+ 823MB `node_modules` containing 385 spec-like files: now correctly
603
+ excluded, and fast (glob itself skips traversing into the ignored
604
+ directories, not just filtering results afterward). An explicit file
605
+ path always bypasses this, same as the non-Playwright-test filter.
606
+
607
+ ## 0.1.7 — 2026-08-15
608
+
609
+ Deeper real-world validation: ~10 more single-file pulls (API-only specs,
610
+ mobile device emulation, deeply-chained fixtures, Japanese-language
611
+ content) plus a full clone of freeCodeCamp's e2e suite (89 spec files)
612
+ scored as one directory. No crashes; UTF-8/multi-byte content (Japanese,
613
+ Chinese, emoji seen across this and the previous round) parses and scores
614
+ correctly throughout.
615
+
616
+ ### Fixed
617
+ - **The `assertFunctionPatterns` name-guessing from 0.1.6 was still
618
+ fundamentally unbounded.** A fourth real production file used yet
619
+ another naming convention (`alertToBeVisible`, imported from another
620
+ module) for the exact same assertion-delegation shape, and a fifth
621
+ (`checkFlashMessageVisibility`) didn't match any reasonable prefix/suffix
622
+ pattern at all — real codebases name these helpers however they want.
623
+ Replaced pure name-guessing with `findLocalAssertionHelperNames`: a
624
+ same-file call-graph pass that discovers any function (declared or
625
+ const-bound to an arrow/function expression) whose own body actually
626
+ contains an `expect(...)`-shaped call, regardless of what it's named,
627
+ and feeds the result into `playwright/expect-expect`'s own
628
+ `assertFunctionNames` option per run. This is fully name-agnostic for
629
+ the common (same-file helper) case; the name-pattern list from 0.1.6
630
+ remains only as a fallback for helpers imported from another file, which
631
+ can't be resolved without following module specifiers — a narrower,
632
+ clearly-scoped residual limitation.
633
+
634
+ ## 0.1.6 — 2026-08-15
635
+
636
+ Further real-world validation: ~45 more spec files across single-file
637
+ pulls (visual regression, accessibility, file upload, iframes, auth
638
+ setup, Storybook) and three fully-cloned real e2e suites (openmrs-esm-core,
639
+ openmrs-esm-patient-chart, storybook) scored as whole directories. No
640
+ crashes across ~100+ real files total (this round plus 0.1.5's); all
641
+ formatters (text/json/markdown/sarif) and `--out` file writing checked at
642
+ that scale too.
643
+
644
+ ### Fixed
645
+ - **`playwright/expect-expect` can only see `expect(...)` calls written
646
+ directly in a test body — it has no way to trace an assertion made
647
+ inside a helper function the test calls.** Delegating assertions to a
648
+ shared helper (e.g. an `audit(page, path)` a11y check reused across many
649
+ near-identical tests, or an `expectXToBeVisible(page)` helper) is a very
650
+ common way to dedupe similar specs, and was flagged as "no assertions"
651
+ at error severity — verified against two real production files (one
652
+ losing 2 tests' worth of real signal to this). Configured
653
+ `assertFunctionPatterns` (eslint-plugin-playwright's own documented
654
+ escape hatch for this) with a conservative, camelCase/exact-anchored
655
+ pattern set (`assert*`, `verify*`, `validate*`, `audit*`, `expect*`,
656
+ `checkA11y`, `checkAccessibility`) — anchored so e.g. `checkoutFlow`
657
+ doesn't collide with a bare `check*` prefix.
658
+
659
+ ## 0.1.5 — 2026-08-15
660
+
661
+ Validated against ~16 real-world Playwright spec files pulled from public
662
+ GitHub repos (react-joyride, openmrs, rancher-desktop, stylelint-demo,
663
+ Microsoft's own Playwright test suite, and others) plus the sample suites
664
+ already dogfooded internally, in addition to unit/integration coverage.
665
+ Found and fixed three more real false positives this surfaced:
666
+
667
+ ### Fixed
668
+ - **`expect.poll()`/`expect.soft()` assertions weren't recognized as
669
+ assertions at all.** A source-text-regex check (`metrics/no-empty-test`)
670
+ only matched a bare `expect(` call, so any file whose only assertion
671
+ used the poll/soft chained form was flagged at **error** severity as
672
+ having no assertions — on a real file that unambiguously polls until a
673
+ condition is true. The AST-based community `playwright/expect-expect`
674
+ rule (already active) handles this correctly on its own, so the
675
+ redundant, buggy regex check has been removed rather than patched
676
+ further.
677
+ - **Playwright's own documented conditional `test.skip(condition, reason)`
678
+ was flagged identically to an always-skipped test declaration.**
679
+ `eslint-plugin-playwright`'s `no-skipped-test` doesn't distinguish
680
+ `test.skip('name', fn)` (a test that never runs — worth flagging) from
681
+ `test.skip(browserName === 'webkit', 'reason')` (Playwright's documented
682
+ API for conditionally skipping at runtime — playwright.dev/docs/test-annotations#conditionally-skip-a-test).
683
+ Verified against a real cross-browser test file where this generated
684
+ five warnings on entirely idiomatic code. Replaced with
685
+ `pwscore/no-skipped-test-declaration`, which tells the two apart by
686
+ whether the trailing argument is a function (a declaration always
687
+ supplies one; a conditional skip's second argument, if any, is the
688
+ reason string) — same severity for the real anti-pattern, none for the
689
+ documented one.
690
+ - **`// eslint-disable-next-line @typescript-eslint/no-unused-vars` (and
691
+ any other disable comment referencing a rule we don't bundle) produced a
692
+ confusing non-finding.** We only embed `@typescript-eslint`'s *parser*,
693
+ not its rule set, so ESLint reports "Definition for rule '...' was not
694
+ found" for any disable directive targeting one of its rules — a
695
+ diagnostic about our own rule coverage, not the spec's quality. This
696
+ pattern is extremely common in real TypeScript code (any repo that also
697
+ lints with `@typescript-eslint`, react-hooks, import, etc.). These
698
+ messages are now dropped entirely instead of appearing as a report-only
699
+ finding.
700
+
701
+ ## 0.1.4 — 2026-08-15
702
+
703
+ Comprehensive correctness/hardening pass ahead of public launch.
704
+
705
+ ### Fixed
706
+ - **A spec file with a syntax error scored a perfect 100/A.** ESLint
707
+ reports parse errors as a message with `fatal:true` and `ruleId:null`;
708
+ those were silently dropped by the same filter that skips harmless
709
+ "file ignored" notices. A file that isn't even valid JS/TS is now a hard
710
+ fail (score 0 / grade F) with an explicit `playwright-score/parse-error`
711
+ finding, instead of the worst possible outcome for a tool whose job is
712
+ catching bad — including AI-generated — code.
713
+ - **Directory/glob scanning could sweep in unrelated Jest/Vitest/RTL unit
714
+ tests** sitting next to real Playwright specs (a very common repo
715
+ layout, e.g. `src/Button.test.tsx` next to `e2e/login.spec.ts`, both
716
+ matching a broad `*.test.ts`-style glob), silently polluting the
717
+ aggregate score. Files matched via directory/glob expansion are now
718
+ skipped when they show positive evidence of a non-Playwright framework
719
+ (an import from vitest/jest/mocha/jasmine/@testing-library, or RTL's
720
+ `screen.*`/`toBeInTheDocument()`); reported explicitly via the new
721
+ `skippedFiles` result field, not silently. Paths passed explicitly are
722
+ always scored regardless. An earlier version of this check tried to
723
+ *positively* require Playwright evidence instead and was reverted after
724
+ it silently skipped 18 of 19 genuine specs in a real production suite —
725
+ custom Playwright fixtures (`test/expect` re-exported from a project
726
+ fixtures module) can destructure arbitrarily-named fixtures, not just
727
+ `page`/`context`/`browser`, so there's no reliable positive signal.
728
+ - **The new AST-based locator counter (added in the previous release)
729
+ silently returned zero for any `.tsx`/`.jsx` file containing real JSX**
730
+ (e.g. Playwright component tests), because `@typescript-eslint/parser`
731
+ doesn't enable JSX parsing by default and the failure was swallowed by
732
+ the same try/catch that handles genuinely unparseable source. JSX is now
733
+ enabled based on the file extension.
734
+ - `test.fix(...)` isn't a real Playwright API — the actual modifier is
735
+ `test.fixme(...)`. Several guardian rules (`one-describe-one-test`,
736
+ `require-test-step`) and metric heuristics (`countTests`,
737
+ `findEmptyExpectTests`) checked for the nonexistent `.fix` spelling and
738
+ so never recognized real `test.fixme(...)` declarations.
739
+ - `guardian/require-expect` only recognized bare `test(...)` calls, so
740
+ `test.only/skip/fixme/slow(...)` declarations with zero assertions went
741
+ unflagged by this rule specifically (the community `expect-expect` rule
742
+ still caught them as a redundant safety net, so real-world impact was
743
+ partial).
744
+ - `guardian/one-describe-one-test` didn't recognize
745
+ `test.describe.serial/parallel/only/fixme(...)` as a describe form at
746
+ all (only bare `test.describe(...)` matched), and flagged
747
+ `test.describe.serial(...)`'s multiple ordered `test()` calls as
748
+ "sprawl" — but `describe.serial` is Playwright's own documented
749
+ mechanism for exactly that one-journey-multiple-ordered-tests pattern.
750
+ - `guardian/no-hardcoded-secrets` matched "auth" as a bare substring, so
751
+ `authorName`, `isAuthenticated`, and similar common identifiers were
752
+ flagged as possible secrets. Matching is now whole-word (after
753
+ camelCase/snake_case splitting), so `authorName` no longer matches while
754
+ `authToken`/`apiKey`/`Authorization` still do.
755
+ - `guardian/no-generic-long-timeout` flagged *any* object property named
756
+ `timeout` anywhere in the file — including unrelated business/fixture
757
+ data like `{ timeout: 1800 }` describing a session-length field — not
758
+ just `timeout` options passed to a Playwright call. Now scoped to
759
+ `timeout` properties passed inline as a call argument.
760
+ - `prefer-web-first-assertions` was downgraded to `warn` with no stated
761
+ reason, softer than eslint-plugin-playwright's own recommended `error`
762
+ severity for a rule it treats as important. Aligned to match.
763
+
764
+ ### Added
765
+ - `ScoreResult.skippedFiles`, and a summary of skipped files in the text
766
+ and markdown formatters.
767
+
768
+ ## 0.1.3 — 2026-08-14
769
+
770
+ ### Fixed
771
+ - The `locators` dimension counted native/raw locator calls with a regex
772
+ anchored to literal `page`/`frame` receivers, undercounting raw locators
773
+ on any other receiver (e.g. `this.container.locator(...)` in a Page
774
+ Object Model, or a second `.locator()` chained off a prior locator call).
775
+ Counting now walks the same AST ESLint already parses, so it can't
776
+ silently disagree with `no-raw-locators` findings.
777
+ - SARIF output pointed `informationUri` at a URL missing a hyphen
778
+ (`github.com/qaguardian/...` instead of `qa-guardian`), and
779
+ `artifactLocation.uri` used absolute filesystem paths, which GitHub code
780
+ scanning cannot map back to files in a PR diff. `Finding.file` (and so
781
+ every formatter, not just SARIF) is now relative to the files' common
782
+ ancestor directory instead of an absolute path.
783
+ - Removed `guardian/prefer-uuid` and `guardian/fixtures-import` from the
784
+ rule→dimension map: they were referenced but never implemented or
785
+ enabled, so they could never fire.
786
+
787
+ ### Added
788
+ - Fixture and integration test coverage for the 5 guardian rules that
789
+ previously had none: `no-hardcoded-secrets`, `no-generic-long-timeout`,
790
+ `one-describe-one-test`, `require-test-step`, `no-wait-for-load-state`.
791
+
792
+ ## 0.1.2 — 2026-08-14
793
+
794
+ ### Fixed
795
+ - ESLint 9 flat config resolves a `basePath` from the `cwd` passed to it and
796
+ silently drops (no error, no `ruleId`) any file outside that path.
797
+ `scorePaths`/`runEslint` now derive the basePath from the common ancestor
798
+ of the files actually being linted instead of trusting the caller's `cwd`,
799
+ which is not guaranteed to contain them. Previously, any caller running
800
+ from an unrelated working directory got zero ESLint-based findings on
801
+ every file regardless of content.
802
+
803
+ ## 0.1.1 — 2026-08-03
804
+
805
+ ### Fixed
806
+ - Empty file matches now hard-fail with score 0 / grade F instead of
807
+ reporting a misleadingly healthy ~99 pass.
808
+
809
+ ### Docs
810
+ - Scoped-package CLI invocation docs (`npx -p @qaguardian/playwright-score`).
811
+ - `--version` flag.
812
+
813
+ ## 0.1.0 — 2026-07-30
814
+
815
+ Initial release. `sqs-v1` scoring: `standard` (community `eslint-plugin-playwright`
816
+ + suite metrics) and `guardian` (house conventions) profiles. CLI with text,
817
+ json, markdown, and sarif output. Library export via `scorePaths`.