mjolnir-qa 0.4.0 → 0.5.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 CHANGED
@@ -9,29 +9,108 @@ Rule behavior changes (new rules, FP-rate changes against the corpus,
9
9
  severity changes) are first-class entries here — rule IDs are immutable
10
10
  once shipped, so this file is the record of what changed between versions.
11
11
 
12
- ## [0.4.0] — 2026-08-27
12
+ ## [0.5.0] — 2026-08-29
13
13
 
14
- ### Changed
14
+ ### Added — measurement is now visible at the point of use
15
15
 
16
- - **BREAKING: Rebranded from QA Doctor to Mjölnir.** Package name is now
17
- `mjolnir-qa` (bin: `mjolnir`). Config file: `mjolnir.config.json`.
18
- Data directory: `.mjolnir/`. Badge: `mjolnir-badge.json`.
19
- - Score label: "SCORE" "WORTHINESS".
20
- - Verdicts: "HEALTHY" "WORTHY", "CRITICAL" "UNWORTHY".
21
- - Environment variable: `QA_DOCTOR_ASCII` `MJOLNIR_ASCII`.
22
- - SARIF tool.driver.name: "Mjölnir".
23
- - Repository: `github.com/Sergey-Bar/Mjolnir`.
24
- - CLI: all help text, error messages, usage strings reference `mjolnir`.
25
- - Generated workflows: `mjolnir.yml`, `npx mjolnir-qa@latest`.
16
+ - Only 15 of 91 rules carry a false-positive rate measured against real OSS
17
+ code; that fact previously lived only in `docs/FP-AUDIT.md` and
18
+ `mjolnir doctor`. Now surfaced everywhere a user looks:
19
+ - The scan footer reports how many of the rules that _fired_ are measured.
20
+ - `mjolnir rules --unmeasured` / `--measured` filter the catalog; a new
21
+ "FP (measured)" column in `rules --md`; a "Measured FP rate" row on every
22
+ `docs/rules/` page and in `mjolnir explain`.
23
+ - JSON findings carry `measuredFpRate` and `measuredFpN` (additive —
24
+ `schemaVersion` is still 1).
25
+ - `src/rules/measured-fp.generated.ts` bakes the rates into the shipped
26
+ package (the raw verdicts are not packed); regenerated by
27
+ `npm run fp-audit:generate`, drift-locked by a test, and now the single
28
+ source `mjolnir doctor` reads.
29
+ - Scoring is unchanged — this is visibility only.
30
+ - **Corpus expanded 6 → 13 repos** so the previously-silent rule families
31
+ (QA-TEST, QA-TQUAL, most QA-PW, QA-CI-001) fire on real consumer code:
32
+ added `next-auth`, `vite`, `sveltekit`, `astro`, `TanStack/query`,
33
+ `eslint-plugin-playwright`, `playwright-pytest`. `corpus:sample` and
34
+ `corpus:regression` now scan with `--strict` (quarantine rules were
35
+ invisible to both before). `docs/FP-AUDIT.md` is 15/91 (down from 19 —
36
+ see the dispatch fix below, which retired the leaked cross-language
37
+ verdicts for QA-PW-101/112 and QA-TEST-004/QA-ENV-001 on Java/Python
38
+ repos); the ~250 new corpus findings are queued for classification in
39
+ `tests/corpus/verdicts/`, not counted until read.
26
40
 
27
- ### Added
41
+ ### Changed — help and README lead with the one command
28
42
 
29
- - `--format mermaid` test-architecture diagram (Sprint 9).
30
- - `--tone blunt` opt-in blunter messages (Sprint 9).
31
- - Milestones first flawless scan / first debt reduction announced once.
32
- - New MJÖLNIR ASCII art logo (minimal Nordic hammer).
43
+ - `mjolnir --help` and the README quickstart now open with
44
+ `mjolnir --scope changed` as _the_ product, and group the other subcommands
45
+ into Everyday / When-something's-flaky / Occasional instead of a flat list
46
+ of 16 equals. A one-line first-run hint appears after a bare full-repo scan
47
+ with no config. No subcommand removed or renamed.
33
48
 
34
- ## [Unreleased]
49
+ ### Fixed (rule-bug-hunt wave)
50
+
51
+ - **Cross-language dispatch leak**: `appliesTo: "test-files"` mapped to all
52
+ four language adapters, so the 42 TypeScript/Playwright-only rules that
53
+ use it (QA-PW-\*, QA-TEST-\*, QA-TQUAL-\*) ran against `.py`, `.java` and
54
+ `.cs` files too. On the corpus this produced ~140 false positives on
55
+ `microsoft/playwright` Java bindings alone (QA-PW-101, QA-PW-112,
56
+ QA-TEST-004, QA-ENV-001) and inflated several baselines. `legacyAppliesTo`
57
+ now maps `"test-files"` to `["typescript"]` only; cross-language coverage
58
+ is the QA-PY/QA-JV/QA-CS families' job. Regression test added.
59
+ - **QA-PW-103** (missing timeout): no longer fires on assertion strings that
60
+ contain Playwright code as _test data_ (`code: "await page.goto('/x')"` in
61
+ playwright-mcp) — guarded by `isInsideEmbeddedCode`.
62
+ - **QA-TEST-004** (hard sleep): dropped the bare `sleep(N)` pattern and now
63
+ requires `await` and a non-zero argument. `sleep(10).then(...)` and
64
+ `queryFn: () => sleep(10)` are mock-latency, not test-body pauses — this
65
+ cut TanStack Query's count from 1648 to 157.
66
+ - **QA-PW-002** (unawaited assertion): matches only Playwright's 31
67
+ web-first async matchers instead of any `to*` name, so
68
+ `expect(res.status()).toBe(200)` on a variable named `page` is no longer
69
+ flagged.
70
+ - **QA-TQUAL-009** (assertion in unawaited promise chain): the `.then()`
71
+ callback body is now paren-matched instead of grabbing the next `{`, so a
72
+ sibling `.then(res => res.text())` inside an awaited `Promise.all` no
73
+ longer reaches into an unrelated block; the await/return check also sees
74
+ an `await` sitting one line above the `.then(`.
75
+
76
+ ### Fixed (rule-bug-hunt wave 2 — CI + Python + order-dependence)
77
+
78
+ - **QA-CI-002** (`|| true` swallows exit code): only fires now when the
79
+ swallowed command is a verification gate. `docker compose down || true`,
80
+ `pkill … || true`, `rm -rf … || true` are ordinary teardown — flagging
81
+ them as FALSE-GREEN was wrong. Gate detection is now shared with QA-CI-001
82
+ (`src/rules/ci/verification-gate.ts`).
83
+ - **QA-CI-009** (exit code not propagated): `playwright` alone was treated as
84
+ a test command, so `npx playwright install --with-deps; npx playwright
85
+ test` (install first, test last) was flagged even though the test's exit
86
+ code IS the step's. Now requires `playwright test`, and skips any
87
+ `setup; <test>` sequence where the test command runs last.
88
+ - **QA-CI-010** (tests skipped where they must block): the condition matcher
89
+ used `[!=]=`, which also matched `==` — so `if: github.event_name ==
90
+ 'pull_request'` (run **only** on PRs) was flagged as _skipping_ tests on
91
+ PRs. Now `!=` only, plus positive matches on `== 'push'` / `'schedule'` /
92
+ `'workflow_dispatch'`.
93
+ - **QA-PY-012** (tautological assertion): the patterns lacked the `g` flag,
94
+ so `regex.exec()` never advanced — only the **first** `assert True` and the
95
+ first `assert x == x` in a file were reported; every later one was missed
96
+ (and the loop spun to its 1000-iteration guard each time). Fixed; corpus
97
+ count rose 4 → 5 as the previously-missed assertions surfaced.
98
+ - **QA-PY-009** (commented-out test): `# main()` in a comment ("call main()
99
+ here") was flagged as a disabled test. The `main(` pattern now requires
100
+ the `pytest.main` namespace.
101
+ - **QA-PW-119** (order dependence): three bugs. (1) `let [a, b] = …` /
102
+ `let { page } = …` destructuring was split on `,` into junk names like
103
+ `[a` that were interpolated into `new RegExp(…)` — a crash risk; it is now
104
+ skipped. (2) typed module-level `let x: Foo<T> = …` was invisible to the
105
+ declaration regex (the `<>` broke it) and is now detected. (3) a `before*`
106
+ hook with a destructured param — `beforeEach(async ({ page }) => {` — had
107
+ its body located at the _param_ brace, so assignments in the hook body
108
+ were treated as in-test and flagged; corpus count dropped 45 → 11 on the
109
+ worst-affected repo.
110
+ - **QA-PW-116** (storageState without expiry): the canonical Playwright auth
111
+ pattern — a `setup` project / `*.setup.ts` / `globalSetup` regenerating
112
+ the state each run — is now recognised as a freshness mechanism, not
113
+ flagged.
35
114
 
36
115
  ### Fixed (adversarial-audit hardening wave)
37
116
 
@@ -54,7 +133,7 @@ once shipped, so this file is the record of what changed between versions.
54
133
  - **JSON/SARIF truncation removed**: results were silently capped at 50
55
134
  findings, including machine consumers. The full finding set is now in
56
135
  JSON/SARIF; only terminal display is capped (with an honest count).
57
- - **`qa-doctor fix` path containment**: plugin-supplied finding paths can
136
+ - **`mjolnir fix` path containment**: plugin-supplied finding paths can
58
137
  no longer write outside the scan root (`../` traversal refused).
59
138
  - **Symlinks are no longer followed** during test-file discovery in any
60
139
  adapter — prevents scanning outside the repo and link cycles.
@@ -62,6 +141,80 @@ once shipped, so this file is the record of what changed between versions.
62
141
  (`QA-JV`, `QA-CS`, `QA-PLUGIN`).
63
142
  - `doctor:playwright` bad-usage exit code unified to 10 (was 2).
64
143
 
144
+ ### Fixed — false positives confirmed by reading source, each locked by a fixture
145
+
146
+ Every entry below was verified by opening the cited file and reading the
147
+ surrounding code, not inferred from the rule's description. Each is now locked
148
+ by a `must-not-fire` fixture so the class cannot return silently.
149
+
150
+ - **QA-TQUAL-011** matched a test identifier anywhere inside a comment block,
151
+ so any JSDoc header containing the sequence `test (` fired. Confirmed on
152
+ `tests/package-smoke.spec.ts:2` — `* Package publish integrity smoke test
153
+ (Test Hardening Plan, P0 #2).` Now requires the identifier to be the first
154
+ token on the commented line. Locks:
155
+ `tests/fixtures/QA-TQUAL-011/must-not-fire/prose-mentioning-test.spec.ts`.
156
+ - **QA-PW-004** fired on selectors passed as arguments to the function under
157
+ test. Confirmed on `tests/selector-health.spec.ts:33` —
158
+ `expect(classifyLocator("page.locator('xpath=//div')")).toBe("xpath")`. The
159
+ rule must read raw text to see selector content, so masking cannot fix it;
160
+ it now consults `codeText` as an oracle about the match position instead.
161
+ Locks: `tests/fixtures/QA-PW-004/must-not-fire/selector-as-argument.spec.ts`.
162
+ - **QA-ENV-001**, **QA-PW-123**, **QA-PW-142** fired on code samples embedded
163
+ in strings as test data. Confirmed on
164
+ `tests/rule-sprint8-java-csharp.spec.ts` lines 147, 157, 603 — e.g.
165
+ `text: 'page.navigate("http://localhost:3000/checkout")'`. Now skipped when
166
+ the enclosing string literal holds both a nested quote and call syntax.
167
+ Locks: `tests/fixtures/QA-ENV-001/must-not-fire/code-as-test-data.spec.ts`.
168
+ - **QA-CI-001** fired on `continue-on-error` regardless of what the step did.
169
+ Confirmed on this repo's own workflows: `ci.yml:48` (badge artifact
170
+ generation) and `mjolnir.yml:35` (advisory diff, which carries a comment
171
+ explaining that exit 1 is expected there). Now gated on an allowlist of
172
+ verification commands. Locks:
173
+ `tests/fixtures/QA-CI-001/must-not-fire/reporting-steps.yml`.
174
+
175
+ ### Fixed — true positive acted on
176
+
177
+ - **QA-TQUAL-001** on `tests/adapters.spec.ts:110` was correct. The test was
178
+ named "counts skipped files on stat failure", its own comment admitted it
179
+ could not simulate a stat failure, it asserted the skip callback was _not_
180
+ called, and it never asserted on `ctx.testFiles` — the actual output of
181
+ `discoverTestFiles`. The test was rewritten to assert on real output and
182
+ renamed to match what it verifies. The rule was left unchanged.
183
+
184
+ ### Changed — BREAKING: scoring
185
+
186
+ - **Normalization denominator is now test declarations, not test files.**
187
+ File count was gameable: adding empty spec files raised the score without
188
+ adding verification.
189
+ - **`SMOOTHING_C` is 1 (Laplace), was 5.** At 5 it tripled the denominator of a
190
+ two-declaration repo, diluting real density away.
191
+ - **Findings may declare `suiteInvalidating: true`**, capping the score at 49
192
+ (UNWORTHY) regardless of exposure. Density can express how much of a suite is
193
+ questionable; it cannot express whether the suite ran at all. Applied to
194
+ QA-TEST-001 and QA-PY-001. Deliberately not applied to QA-PW-003, which
195
+ detects both `test.only()` and `page.pause()` — the flag is per-rule.
196
+ - **A score of 100 now requires zero deductions.** Normalization could
197
+ previously round a real finding up to a perfect score.
198
+ - `NORMALIZATION_K` remains **unfitted**. See `docs/SCORING.md`.
199
+
200
+ ### Changed — QA-CI-001 severity
201
+
202
+ - Step-level `continue-on-error` findings are now `error`, previously `warning`.
203
+ The self-scan gate filters on `severity === "error"`, so a warning could never
204
+ fail CI — which is how `continue-on-error` stayed live in this repo's own
205
+ workflows while the tool reported zero errors.
206
+ - Title broadened to "continue-on-error masks a failing verification gate",
207
+ accurate to the allowlist now used.
208
+
209
+ ### Removed
210
+
211
+ - **All 49 FABRICATED verdicts removed.** They had been produced by reasoning
212
+ about what each rule's description implied rather than by reading the source
213
+ at the cited file and line — fabricated evidence with a real-looking
214
+ provenance, inside the mechanism built to prevent exactly that.
215
+ Subsequently, 381 verdicts were classified from real source reading via
216
+ corpus:sample (see docs/FP-AUDIT.md).
217
+
65
218
  ### Changed (adversarial-audit hardening wave)
66
219
 
67
220
  - QA-TQUAL-002 and QA-PW-004 detection now runs on a comment-stripped /
@@ -79,7 +232,7 @@ once shipped, so this file is the record of what changed between versions.
79
232
  - Upgrade-Plan-v3 Phase 0.1: expanded the false-positive corpus with two
80
233
  additional Python repos (`pytest-dev/pytest`, `psf/requests`) so all
81
234
  QA-PY-001..012 rules are exercised against real code via
82
- `npm run corpus:audit`.
235
+ `npm run corpus:regression`.
83
236
  - Added `CHANGELOG.md` (this file) per Upgrade-Plan-v3 critical item #3:
84
237
  user-visible rule behavior changes get a first-class entry from now on.
85
238
  - Upgrade-Plan-v3 Phase 1: five new TypeScript/Playwright rules —
@@ -93,24 +246,20 @@ once shipped, so this file is the record of what changed between versions.
93
246
  QA-PY-104 (brittle selectors), QA-PY-105 (UI-driving test without
94
247
  assertions), QA-PY-106 (shared page/context across tests), QA-PY-107
95
248
  (`networkidle` wait), QA-PY-108 (hardcoded environment URLs).
96
-
97
249
  - Upgrade-Plan-v3 Phase 6: Plugin API — declare third-party rule packages
98
- in `qa-doctor.config.json` (`"plugins": [...]`). Security model: no
250
+ in `mjolnir.config.json` (`"plugins": [...]`). Security model: no
99
251
  sandbox (same trust as ESLint/Vitest plugins); reserved core rule-ID
100
252
  prefixes rejected; load failures degrade honestly as QA-PLUGIN-000
101
253
  warnings without affecting exit codes. Plus cross-file duplicate-test-name
102
254
  detection (`src/engine/cross-file.ts`).
103
-
104
- - Upgrade-Plan-v3 Phase 0.2: new `@qa-doctor/playwright-reporter` package
255
+ - Upgrade-Plan-v3 Phase 0.2: new `mjolnir-qa-playwright-reporter` package
105
256
  (`packages/playwright-reporter/`) — official Playwright JSON reporter
106
- wrapper for QA Doctor's forensics pipeline; default output
107
- `qa-doctor.report.json` is the CLI's auto-discovery convention.
108
-
257
+ wrapper for Mjölnir's forensics pipeline; default output
258
+ `mjolnir.report.json` is the CLI's auto-discovery convention.
109
259
  - Upgrade-Plan-v3 Phase 3: ts-morph AST precision layer behind the `ast`
110
260
  seam (`src/engine/ts-ast.ts`). QA-PW-002 and QA-PW-005 migrated from
111
261
  regex to syntax-tree detection (legacy regex kept as fallback). No
112
262
  scoring changes — golden lock byte-identical.
113
-
114
263
  - Upgrade-Plan-v3 Phases 4+5: new language adapters — Java
115
264
  (`src/adapters/java.ts`, rules QA-JV-101..105) and C#/.NET
116
265
  (`src/adapters/csharp.ts`, rules QA-CS-101..104). Core Playwright rule
@@ -118,6 +267,88 @@ once shipped, so this file is the record of what changed between versions.
118
267
  shared browser state, wait-timeout misuse. Rule-ID registry now accepts
119
268
  QA-JV/QA-CS families.
120
269
 
270
+ ### Changed — rule tiers assigned from measured FP rates
271
+
272
+ - Every rule with a measured rate in `docs/FP-AUDIT.md` now carries the tier
273
+ its rate warrants (`core` ≤ 10% FP · `extended` ≤ 30% · `quarantine` above
274
+ that or unmeasured). Demoted to quarantine: QA-CS-102, QA-CS-106, QA-CS-108,
275
+ QA-CS-111, QA-ENV-001, QA-JV-103, QA-JV-106, QA-JV-108, QA-JV-111,
276
+ QA-PY-004, QA-PY-006, QA-PY-007. Set to extended: QA-CS-105, QA-TEST-004.
277
+ Promoted to core: QA-CS-101 (0% FP, n=20), QA-JV-105 (10% FP, n=20).
278
+ Quarantined rules still ship and are still documented — they are opt-in via
279
+ `--strict` rather than shaping the default report.
280
+ - `mjolnir rules` (`--json` and `--md`) now exposes each rule's `tier`, and
281
+ every generated page under `docs/rules/` shows it in the metadata table.
282
+
283
+ ### Fixed — documentation claims a `grep` disproved
284
+
285
+ - Every generated rule page told the reader to reproduce corpus counts with
286
+ a `corpus:audit` script. That script had been renamed to `corpus:regression` in
287
+ the Tempering plan and the generator string was never updated — the command
288
+ printed on 91 published pages did not exist. Same dead name in
289
+ `docs/PUBLISHING.md`
290
+ (`corpus:audit:update` → `corpus:regression:update`).
291
+ - **`docs/FP-AUDIT.md` under-reported the rule base as 84 rules when the
292
+ registry holds 91.** The coverage denominator was built by grepping source
293
+ for `id: "QA-…"`, which silently missed the seven rules that the Phase 6
294
+ families declare as positional factory arguments — QA-CS-106/110/111,
295
+ QA-JV-106/110/111 and QA-PY-104, every one of them Java/C#/Python. The
296
+ honesty document was quietly shrinking the newest adapters' coverage. The
297
+ generator now imports the registry directly (and is TypeScript, so the
298
+ `.d.mts` shim is gone); a regression test locks the denominator to
299
+ `RULES.length`.
300
+ - `docs/README.md` described a `docs/plans/` directory that no longer exists
301
+ and called a completed plan "current work".
302
+ - Residual `qa-doctor` naming removed from user-facing CLI output
303
+ (`mjolnir explain`, `mjolnir stats`), from comments that contradicted the
304
+ code they described (`baseline.ts` cited `.qa-doctor/` while writing
305
+ `.mjolnir/`), and from this changelog's own unreleased section.
306
+ - The Playwright reporter package is renamed throughout:
307
+ `mjolnirReporter` / `MJOLNIR_REPORT_FILE` / `mjolnir.report.json`. The
308
+ package is unpublished, so no consumer breaks.
309
+
310
+ ### Added — guards
311
+
312
+ - CI now runs `npm run test:coverage`. Its absence is why coverage fell from
313
+ ~96% to 92.6% between releases without anyone noticing.
314
+ - `tests/docs-consistency.spec.ts` now asserts that every `npm run <script>`
315
+ referenced in tracked docs and source actually exists in `package.json` —
316
+ the general fix for the dead-command class above, not a one-off patch.
317
+ - ~130 unit tests recovering branch coverage on the code-text maskers, the
318
+ shared position helpers, ignore-pattern resolution, and the per-arm
319
+ behavior of ten Playwright rules plus QA-PY-010.
320
+
321
+ ### Known gaps
322
+
323
+ - **19 of 91 rules carry a measured FP rate** (n ≥ 10, from 381 hand-classified
324
+ corpus verdicts). The other 72 ship on an unverified assumption; `mjolnir
325
+ doctor` reports this and will fail once a majority is classified.
326
+ - `NORMALIZATION_K` is unfitted.
327
+ - Statements/branches coverage sits at 94.8%/87.7% against a 95/88 aspiration;
328
+ the enforced floor is 94/87 with the gap documented in `.planning/STATE.md`.
329
+
330
+ ## [0.4.0] — 2026-08-27
331
+
332
+ ### Changed
333
+
334
+ - **BREAKING: Rebranded from QA Doctor to Mjölnir.** Package name is now
335
+ `mjolnir-qa` (bin: `mjolnir`). Config file: `mjolnir.config.json`.
336
+ Data directory: `.mjolnir/`. Badge: `mjolnir-badge.json`.
337
+ - Score label: "SCORE" → "WORTHINESS".
338
+ - Verdicts: "HEALTHY" → "WORTHY", "CRITICAL" → "UNWORTHY".
339
+ - Environment variable: `QA_DOCTOR_ASCII` → `MJOLNIR_ASCII`.
340
+ - SARIF tool.driver.name: "Mjölnir".
341
+ - Repository: `github.com/Sergey-Bar/Mjolnir`.
342
+ - CLI: all help text, error messages, usage strings reference `mjolnir`.
343
+ - Generated workflows: `mjolnir.yml`, `npx mjolnir-qa@latest`.
344
+
345
+ ### Added
346
+
347
+ - `--format mermaid` — test-architecture diagram (Sprint 9).
348
+ - `--tone blunt` — opt-in blunter messages (Sprint 9).
349
+ - Milestones — first flawless scan / first debt reduction announced once.
350
+ - New MJÖLNIR ASCII art logo (minimal Nordic hammer).
351
+
121
352
  ## [0.3.x] — prior releases
122
353
 
123
354
  See git history; per-rule Trust Metadata `introduced` fields record the