mjolnir-qa 0.6.7 → 0.6.13

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,6 +9,42 @@ 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.6.13] — 2026-09-09
13
+
14
+ ### Changes since 0.6.12
15
+
16
+ - fix(lint): {4} quantifier for the remaining literal spaces in sync-census regex
17
+
18
+ ## [0.6.12] — 2026-09-09
19
+
20
+ ### Changes since 0.6.11
21
+
22
+ - fix(lint): no-regex-spaces in sync-census tooling ({2} quantifier)
23
+
24
+ ## [0.6.11] — 2026-09-09
25
+
26
+ ### Changes since 0.6.10
27
+
28
+ - fix(lint): exclude scripts/*.mts from the typed-parser project route
29
+
30
+ ## [0.6.10] — 2026-09-09
31
+
32
+ ### Changes since 0.6.9
33
+
34
+ - fix(lint): scripts/*.mts one-shot tooling runs under the plain JS ruleset
35
+
36
+ ## [0.6.9] — 2026-09-09
37
+
38
+ ### Changes since 0.6.8
39
+
40
+ - feat(measurement): WI-14 closeout — 77/77 measured, 0 PROVISIONAL (1.0.0 census achieved)
41
+
42
+ ## [0.6.8] — 2026-09-09
43
+
44
+ ### Changes since 0
45
+
46
+ - feat(corpus): WI-14 harvest waves 1-5 — census 60 → 72 of 78 measured
47
+
12
48
  ## [0.6.7] — 2026-09-09
13
49
 
14
50
  ### Changes since 0.6.6
package/README.md CHANGED
@@ -262,7 +262,7 @@ have no such requirement.)
262
262
 
263
263
  ## What Mjölnir finds
264
264
 
265
- **<!-- census:total-rules -->78 rules<!-- /census:total-rules -->** in four families — **test hygiene**, **test quality**,
265
+ **<!-- census:total-rules -->77 rules<!-- /census:total-rules -->** in four families — **test hygiene**, **test quality**,
266
266
  **Playwright**, **CI integrity** — over TypeScript/JavaScript, Python,
267
267
  Java, C# and GitHub Actions YAML, covering Playwright in all four bindings
268
268
  plus pytest, JUnit, TestNG, NUnit, xUnit, MSTest, Jest, Vitest and Mocha,
@@ -442,9 +442,9 @@ Rung by rung: [docs/TERMINOLOGY.md](docs/TERMINOLOGY.md).
442
442
 
443
443
  ### How much of this is measured
444
444
 
445
- **<!-- census:measured-of-total -->57 of 78<!-- /census:measured-of-total --> rules carry a false-positive rate measured against real OSS code**
445
+ **<!-- census:measured-of-total -->77 of 77<!-- /census:measured-of-total --> rules carry a false-positive rate measured against real OSS code**
446
446
  (≥ 10 hand-classified findings each — [docs/FP-AUDIT.md](docs/FP-AUDIT.md)).
447
- The other <!-- census:unmeasured -->21<!-- /census:unmeasured --> ship on the author's estimate and say so, per rule, in
447
+ The other <!-- census:unmeasured -->0<!-- /census:unmeasured --> ship on the author's estimate and say so, per rule, in
448
448
  `mjolnir explain`; `mjolnir rules --unmeasured` lists them, and every scan
449
449
  footer reports how many of the rules that actually _fired_ are measured.
450
450
 
@@ -685,7 +685,7 @@ artifacts.
685
685
  product does what the requirement asked for.
686
686
  - **A 100 is not proof of a good suite.** Whether your suite covers your
687
687
  actual risk is a different question, and this tool does not answer it.
688
- - **<!-- census:unmeasured-of-total -->21 of 78<!-- /census:unmeasured-of-total --> rules ship on an estimate**, not a measured rate — disclosed
688
+ - **<!-- census:unmeasured-of-total -->0 of 77<!-- /census:unmeasured-of-total --> rules ship on an estimate**, not a measured rate — disclosed
689
689
  per rule, not buried here.
690
690
  - **E1 is not E2.** Heuristic findings are worth reading, not worth
691
691
  applying blindly.
package/dist/cli.d.mts CHANGED
@@ -826,7 +826,7 @@ declare const runScan: typeof runScan$1, buildUniversalRules: typeof buildUniver
826
826
  * `scripts/sync-sarif-version.cjs` on release and guarded by
827
827
  * `tests/version-consistency.spec.ts` locally.
828
828
  */
829
- declare const CLI_VERSION = "0.6.7";
829
+ declare const CLI_VERSION = "0.6.13";
830
830
  /** A usage-error detail: the offending token, when one exists. */
831
831
  interface UsageErrorDetail {
832
832
  /** The unknown flag or rejected value (e.g. `--nope`, `loud`). */
package/dist/cli.mjs CHANGED
@@ -335,6 +335,13 @@ const MEASURED_FP = {
335
335
  ciLow: 0,
336
336
  ciHigh: .2588
337
337
  },
338
+ "QA-CS-104": {
339
+ fpRate: 0,
340
+ n: 10,
341
+ detectorRevision: 1,
342
+ ciLow: 0,
343
+ ciHigh: .2775
344
+ },
338
345
  "QA-CS-105": {
339
346
  fpRate: .25,
340
347
  n: 16,
@@ -342,6 +349,27 @@ const MEASURED_FP = {
342
349
  ciLow: .1018,
343
350
  ciHigh: .495
344
351
  },
352
+ "QA-CS-106": {
353
+ fpRate: .333,
354
+ n: 12,
355
+ detectorRevision: 2,
356
+ ciLow: .1381,
357
+ ciHigh: .6094
358
+ },
359
+ "QA-CS-107": {
360
+ fpRate: .083,
361
+ n: 12,
362
+ detectorRevision: 1,
363
+ ciLow: .0149,
364
+ ciHigh: .3539
365
+ },
366
+ "QA-CS-109": {
367
+ fpRate: 0,
368
+ n: 10,
369
+ detectorRevision: 1,
370
+ ciLow: 0,
371
+ ciHigh: .2775
372
+ },
345
373
  "QA-CYP-001": {
346
374
  fpRate: .2,
347
375
  n: 15,
@@ -349,6 +377,20 @@ const MEASURED_FP = {
349
377
  ciLow: .0705,
350
378
  ciHigh: .4519
351
379
  },
380
+ "QA-CYP-002": {
381
+ fpRate: 0,
382
+ n: 10,
383
+ detectorRevision: 1,
384
+ ciLow: 0,
385
+ ciHigh: .2775
386
+ },
387
+ "QA-CYP-003": {
388
+ fpRate: 0,
389
+ n: 10,
390
+ detectorRevision: 1,
391
+ ciLow: 0,
392
+ ciHigh: .2775
393
+ },
352
394
  "QA-ENV-001": {
353
395
  fpRate: 1,
354
396
  n: 20,
@@ -391,6 +433,20 @@ const MEASURED_FP = {
391
433
  ciLow: .0279,
392
434
  ciHigh: .301
393
435
  },
436
+ "QA-JV-106": {
437
+ fpRate: .333,
438
+ n: 12,
439
+ detectorRevision: 2,
440
+ ciLow: .1381,
441
+ ciHigh: .6094
442
+ },
443
+ "QA-JV-107": {
444
+ fpRate: 0,
445
+ n: 10,
446
+ detectorRevision: 1,
447
+ ciLow: 0,
448
+ ciHigh: .2775
449
+ },
394
450
  "QA-JV-109": {
395
451
  fpRate: 0,
396
452
  n: 18,
@@ -426,6 +482,13 @@ const MEASURED_FP = {
426
482
  ciLow: 0,
427
483
  ciHigh: .1611
428
484
  },
485
+ "QA-PW-102": {
486
+ fpRate: .1,
487
+ n: 10,
488
+ detectorRevision: 2,
489
+ ciLow: .0179,
490
+ ciHigh: .4042
491
+ },
429
492
  "QA-PW-104": {
430
493
  fpRate: 0,
431
494
  n: 10,
@@ -447,6 +510,13 @@ const MEASURED_FP = {
447
510
  ciLow: .3318,
448
511
  ciHigh: .769
449
512
  },
513
+ "QA-PW-116": {
514
+ fpRate: 0,
515
+ n: 10,
516
+ detectorRevision: 1,
517
+ ciLow: 0,
518
+ ciHigh: .2775
519
+ },
450
520
  "QA-PW-117": {
451
521
  fpRate: 0,
452
522
  n: 24,
@@ -475,6 +545,20 @@ const MEASURED_FP = {
475
545
  ciLow: .2127,
476
546
  ciHigh: .7199
477
547
  },
548
+ "QA-PW-124": {
549
+ fpRate: .067,
550
+ n: 15,
551
+ detectorRevision: 2,
552
+ ciLow: .0119,
553
+ ciHigh: .2982
554
+ },
555
+ "QA-PW-125": {
556
+ fpRate: 0,
557
+ n: 10,
558
+ detectorRevision: 1,
559
+ ciLow: 0,
560
+ ciHigh: .2775
561
+ },
478
562
  "QA-PW-140": {
479
563
  fpRate: 0,
480
564
  n: 10,
@@ -587,6 +671,13 @@ const MEASURED_FP = {
587
671
  ciLow: .2459,
588
672
  ciHigh: .5768
589
673
  },
674
+ "QA-PY-101": {
675
+ fpRate: 0,
676
+ n: 10,
677
+ detectorRevision: 1,
678
+ ciLow: 0,
679
+ ciHigh: .2775
680
+ },
590
681
  "QA-PY-103": {
591
682
  fpRate: .08,
592
683
  n: 25,
@@ -594,6 +685,13 @@ const MEASURED_FP = {
594
685
  ciLow: .0222,
595
686
  ciHigh: .2497
596
687
  },
688
+ "QA-PY-104": {
689
+ fpRate: 0,
690
+ n: 10,
691
+ detectorRevision: 2,
692
+ ciLow: 0,
693
+ ciHigh: .2775
694
+ },
597
695
  "QA-PY-105": {
598
696
  fpRate: 0,
599
697
  n: 12,
@@ -601,6 +699,48 @@ const MEASURED_FP = {
601
699
  ciLow: 0,
602
700
  ciHigh: .2425
603
701
  },
702
+ "QA-PY-106": {
703
+ fpRate: 0,
704
+ n: 14,
705
+ detectorRevision: 1,
706
+ ciLow: 0,
707
+ ciHigh: .2153
708
+ },
709
+ "QA-PY-107": {
710
+ fpRate: 0,
711
+ n: 10,
712
+ detectorRevision: 1,
713
+ ciLow: 0,
714
+ ciHigh: .2775
715
+ },
716
+ "QA-PY-108": {
717
+ fpRate: 0,
718
+ n: 16,
719
+ detectorRevision: 1,
720
+ ciLow: 0,
721
+ ciHigh: .1936
722
+ },
723
+ "QA-SE-001": {
724
+ fpRate: 0,
725
+ n: 10,
726
+ detectorRevision: 1,
727
+ ciLow: 0,
728
+ ciHigh: .2775
729
+ },
730
+ "QA-SE-002": {
731
+ fpRate: .077,
732
+ n: 13,
733
+ detectorRevision: 1,
734
+ ciLow: .0137,
735
+ ciHigh: .3331
736
+ },
737
+ "QA-SE-003": {
738
+ fpRate: 0,
739
+ n: 11,
740
+ detectorRevision: 1,
741
+ ciLow: 0,
742
+ ciHigh: .2588
743
+ },
604
744
  "QA-TEST-001": {
605
745
  fpRate: .6,
606
746
  n: 20,
@@ -5933,6 +6073,7 @@ const pwGlobalSetupSharedState = defineRule({
5933
6073
  findingType: "heuristic-risk",
5934
6074
  qaImpact: "HYGIENE",
5935
6075
  appliesTo: "test-files",
6076
+ tier: "extended",
5936
6077
  languages: ["typescript", "javascript"],
5937
6078
  frameworks: ["playwright"],
5938
6079
  falsePositiveRisk: "medium",
@@ -6596,52 +6737,6 @@ const pyPwSyncAsyncMix = defineRule({
6596
6737
  }
6597
6738
  });
6598
6739
  //#endregion
6599
- //#region src/rules/python/qa-py-102-pw-hard-sleep.ts
6600
- /**
6601
- * QA-PY-102 — Playwright-Python: time.sleep() instead of auto-waiting.
6602
- * Severity: warning · Confidence: high · deterministic-defect
6603
- * The #1 Python-Playwright flake source: sleeping "until the page is
6604
- * ready" instead of waiting on a locator condition.
6605
- */
6606
- const pyPwHardSleep = defineRule({
6607
- id: "QA-PY-102",
6608
- category: "QA-PW",
6609
- title: "time.sleep() in Playwright test",
6610
- severity: "warning",
6611
- confidence: "high",
6612
- findingType: "deterministic-defect",
6613
- qaImpact: "FLAKY-RISK",
6614
- appliesTo: "python",
6615
- languages: ["python"],
6616
- frameworks: ["pytest-playwright", "playwright"],
6617
- falsePositiveRisk: "low",
6618
- autofix: false,
6619
- detectionStrategy: "LEXICAL",
6620
- introduced: "0.3.8",
6621
- tier: "quarantine",
6622
- run(ctx) {
6623
- const text = ctx.codeText ?? ctx.text;
6624
- const findings = [];
6625
- if (!ctx.path.endsWith(".py")) return findings;
6626
- if (!/playwright/i.test(text)) return findings;
6627
- const re = /\btime\.sleep\s*\(/g;
6628
- let m;
6629
- while ((m = re.exec(text)) !== null) findings.push({
6630
- severity: "warning",
6631
- confidence: "high",
6632
- findingType: "deterministic-defect",
6633
- qaImpact: "FLAKY-RISK",
6634
- file: ctx.path,
6635
- line: lineAt(text, m.index),
6636
- column: colAt(text, m.index),
6637
- message: "`time.sleep()` used to wait for UI state.",
6638
- why: "Fixed sleeps guess at timing: too short → flaky failures under load, too long → slow suite. Playwright locators already auto-wait for the element to be actionable.",
6639
- fix: "Replace with `expect(page.get_by_text(...)).to_be_visible()` or `page.wait_for_selector`."
6640
- });
6641
- return findings;
6642
- }
6643
- });
6644
- //#endregion
6645
6740
  //#region src/rules/python/qa-py-103-wait-for-timeout.ts
6646
6741
  /**
6647
6742
  * QA-PY-103 — Playwright-Python: wait_for_timeout() as synchronization.
@@ -9150,7 +9245,6 @@ const RULES = [
9150
9245
  pwLocatorNormalize,
9151
9246
  pwCodegenArtifact,
9152
9247
  pyPwSyncAsyncMix,
9153
- pyPwHardSleep,
9154
9248
  pyPwWaitForTimeout,
9155
9249
  pyPwNoAssertions,
9156
9250
  ...sharedPageFamily,
@@ -9208,6 +9302,7 @@ const RETIRED_RULE_IDS = [
9208
9302
  "QA-PY-006",
9209
9303
  "QA-PY-008",
9210
9304
  "QA-PY-010",
9305
+ "QA-PY-102",
9211
9306
  "QA-JV-108",
9212
9307
  "QA-CS-108",
9213
9308
  "QA-JV-110",
@@ -12018,7 +12113,6 @@ const BLUNT_MESSAGES = {
12018
12113
  "QA-PW-101": "A hard sleep in a test is a prayer, not a synchronization strategy.",
12019
12114
  "QA-TEST-004": "A hard sleep in a test is a prayer, not a synchronization strategy.",
12020
12115
  "QA-PY-005": "time.sleep() in a test is a prayer. Python doesn't negotiate with timeouts either.",
12021
- "QA-PY-102": "time.sleep() inside a Playwright test — the browser doesn't pause because you asked nicely.",
12022
12116
  "QA-PY-103": "wait_for_timeout() as synchronization — the Playwright-Python edition of hoping.",
12023
12117
  "QA-JV-102": "Thread.sleep() in a test is a prayer. The JVM doesn't negotiate with timeouts.",
12024
12118
  "QA-JV-105": "waitForTimeout is the Java version of closing your eyes and hoping.",
@@ -12659,7 +12753,7 @@ function renderSarif(result, repoRootUri) {
12659
12753
  tool: { driver: {
12660
12754
  name: "Mjölnir",
12661
12755
  informationUri: "https://github.com/Sergey-Bar/Mjolnir",
12662
- version: "0.6.7",
12756
+ version: "0.6.13",
12663
12757
  rules: [...rules.values()].map((r) => {
12664
12758
  const meta = RULES.find((x) => x.id === r.id);
12665
12759
  return {
@@ -19063,7 +19157,7 @@ const { runScan, buildUniversalRules, fallbackWorkspace, pathMatchesGlob, isVali
19063
19157
  * `scripts/sync-sarif-version.cjs` on release and guarded by
19064
19158
  * `tests/version-consistency.spec.ts` locally.
19065
19159
  */
19066
- const CLI_VERSION = "0.6.7";
19160
+ const CLI_VERSION = "0.6.13";
19067
19161
  function parseArgs(argv, onError) {
19068
19162
  const args = {
19069
19163
  target: ".",
@@ -334,6 +334,13 @@ const MEASURED_FP = {
334
334
  ciLow: 0,
335
335
  ciHigh: .2588
336
336
  },
337
+ "QA-CS-104": {
338
+ fpRate: 0,
339
+ n: 10,
340
+ detectorRevision: 1,
341
+ ciLow: 0,
342
+ ciHigh: .2775
343
+ },
337
344
  "QA-CS-105": {
338
345
  fpRate: .25,
339
346
  n: 16,
@@ -341,6 +348,27 @@ const MEASURED_FP = {
341
348
  ciLow: .1018,
342
349
  ciHigh: .495
343
350
  },
351
+ "QA-CS-106": {
352
+ fpRate: .333,
353
+ n: 12,
354
+ detectorRevision: 2,
355
+ ciLow: .1381,
356
+ ciHigh: .6094
357
+ },
358
+ "QA-CS-107": {
359
+ fpRate: .083,
360
+ n: 12,
361
+ detectorRevision: 1,
362
+ ciLow: .0149,
363
+ ciHigh: .3539
364
+ },
365
+ "QA-CS-109": {
366
+ fpRate: 0,
367
+ n: 10,
368
+ detectorRevision: 1,
369
+ ciLow: 0,
370
+ ciHigh: .2775
371
+ },
344
372
  "QA-CYP-001": {
345
373
  fpRate: .2,
346
374
  n: 15,
@@ -348,6 +376,20 @@ const MEASURED_FP = {
348
376
  ciLow: .0705,
349
377
  ciHigh: .4519
350
378
  },
379
+ "QA-CYP-002": {
380
+ fpRate: 0,
381
+ n: 10,
382
+ detectorRevision: 1,
383
+ ciLow: 0,
384
+ ciHigh: .2775
385
+ },
386
+ "QA-CYP-003": {
387
+ fpRate: 0,
388
+ n: 10,
389
+ detectorRevision: 1,
390
+ ciLow: 0,
391
+ ciHigh: .2775
392
+ },
351
393
  "QA-ENV-001": {
352
394
  fpRate: 1,
353
395
  n: 20,
@@ -390,6 +432,20 @@ const MEASURED_FP = {
390
432
  ciLow: .0279,
391
433
  ciHigh: .301
392
434
  },
435
+ "QA-JV-106": {
436
+ fpRate: .333,
437
+ n: 12,
438
+ detectorRevision: 2,
439
+ ciLow: .1381,
440
+ ciHigh: .6094
441
+ },
442
+ "QA-JV-107": {
443
+ fpRate: 0,
444
+ n: 10,
445
+ detectorRevision: 1,
446
+ ciLow: 0,
447
+ ciHigh: .2775
448
+ },
393
449
  "QA-JV-109": {
394
450
  fpRate: 0,
395
451
  n: 18,
@@ -425,6 +481,13 @@ const MEASURED_FP = {
425
481
  ciLow: 0,
426
482
  ciHigh: .1611
427
483
  },
484
+ "QA-PW-102": {
485
+ fpRate: .1,
486
+ n: 10,
487
+ detectorRevision: 2,
488
+ ciLow: .0179,
489
+ ciHigh: .4042
490
+ },
428
491
  "QA-PW-104": {
429
492
  fpRate: 0,
430
493
  n: 10,
@@ -446,6 +509,13 @@ const MEASURED_FP = {
446
509
  ciLow: .3318,
447
510
  ciHigh: .769
448
511
  },
512
+ "QA-PW-116": {
513
+ fpRate: 0,
514
+ n: 10,
515
+ detectorRevision: 1,
516
+ ciLow: 0,
517
+ ciHigh: .2775
518
+ },
449
519
  "QA-PW-117": {
450
520
  fpRate: 0,
451
521
  n: 24,
@@ -474,6 +544,20 @@ const MEASURED_FP = {
474
544
  ciLow: .2127,
475
545
  ciHigh: .7199
476
546
  },
547
+ "QA-PW-124": {
548
+ fpRate: .067,
549
+ n: 15,
550
+ detectorRevision: 2,
551
+ ciLow: .0119,
552
+ ciHigh: .2982
553
+ },
554
+ "QA-PW-125": {
555
+ fpRate: 0,
556
+ n: 10,
557
+ detectorRevision: 1,
558
+ ciLow: 0,
559
+ ciHigh: .2775
560
+ },
477
561
  "QA-PW-140": {
478
562
  fpRate: 0,
479
563
  n: 10,
@@ -586,6 +670,13 @@ const MEASURED_FP = {
586
670
  ciLow: .2459,
587
671
  ciHigh: .5768
588
672
  },
673
+ "QA-PY-101": {
674
+ fpRate: 0,
675
+ n: 10,
676
+ detectorRevision: 1,
677
+ ciLow: 0,
678
+ ciHigh: .2775
679
+ },
589
680
  "QA-PY-103": {
590
681
  fpRate: .08,
591
682
  n: 25,
@@ -593,6 +684,13 @@ const MEASURED_FP = {
593
684
  ciLow: .0222,
594
685
  ciHigh: .2497
595
686
  },
687
+ "QA-PY-104": {
688
+ fpRate: 0,
689
+ n: 10,
690
+ detectorRevision: 2,
691
+ ciLow: 0,
692
+ ciHigh: .2775
693
+ },
596
694
  "QA-PY-105": {
597
695
  fpRate: 0,
598
696
  n: 12,
@@ -600,6 +698,48 @@ const MEASURED_FP = {
600
698
  ciLow: 0,
601
699
  ciHigh: .2425
602
700
  },
701
+ "QA-PY-106": {
702
+ fpRate: 0,
703
+ n: 14,
704
+ detectorRevision: 1,
705
+ ciLow: 0,
706
+ ciHigh: .2153
707
+ },
708
+ "QA-PY-107": {
709
+ fpRate: 0,
710
+ n: 10,
711
+ detectorRevision: 1,
712
+ ciLow: 0,
713
+ ciHigh: .2775
714
+ },
715
+ "QA-PY-108": {
716
+ fpRate: 0,
717
+ n: 16,
718
+ detectorRevision: 1,
719
+ ciLow: 0,
720
+ ciHigh: .1936
721
+ },
722
+ "QA-SE-001": {
723
+ fpRate: 0,
724
+ n: 10,
725
+ detectorRevision: 1,
726
+ ciLow: 0,
727
+ ciHigh: .2775
728
+ },
729
+ "QA-SE-002": {
730
+ fpRate: .077,
731
+ n: 13,
732
+ detectorRevision: 1,
733
+ ciLow: .0137,
734
+ ciHigh: .3331
735
+ },
736
+ "QA-SE-003": {
737
+ fpRate: 0,
738
+ n: 11,
739
+ detectorRevision: 1,
740
+ ciLow: 0,
741
+ ciHigh: .2588
742
+ },
603
743
  "QA-TEST-001": {
604
744
  fpRate: .6,
605
745
  n: 20,
@@ -5932,6 +6072,7 @@ const pwGlobalSetupSharedState = defineRule({
5932
6072
  findingType: "heuristic-risk",
5933
6073
  qaImpact: "HYGIENE",
5934
6074
  appliesTo: "test-files",
6075
+ tier: "extended",
5935
6076
  languages: ["typescript", "javascript"],
5936
6077
  frameworks: ["playwright"],
5937
6078
  falsePositiveRisk: "medium",
@@ -6595,52 +6736,6 @@ const pyPwSyncAsyncMix = defineRule({
6595
6736
  }
6596
6737
  });
6597
6738
  //#endregion
6598
- //#region src/rules/python/qa-py-102-pw-hard-sleep.ts
6599
- /**
6600
- * QA-PY-102 — Playwright-Python: time.sleep() instead of auto-waiting.
6601
- * Severity: warning · Confidence: high · deterministic-defect
6602
- * The #1 Python-Playwright flake source: sleeping "until the page is
6603
- * ready" instead of waiting on a locator condition.
6604
- */
6605
- const pyPwHardSleep = defineRule({
6606
- id: "QA-PY-102",
6607
- category: "QA-PW",
6608
- title: "time.sleep() in Playwright test",
6609
- severity: "warning",
6610
- confidence: "high",
6611
- findingType: "deterministic-defect",
6612
- qaImpact: "FLAKY-RISK",
6613
- appliesTo: "python",
6614
- languages: ["python"],
6615
- frameworks: ["pytest-playwright", "playwright"],
6616
- falsePositiveRisk: "low",
6617
- autofix: false,
6618
- detectionStrategy: "LEXICAL",
6619
- introduced: "0.3.8",
6620
- tier: "quarantine",
6621
- run(ctx) {
6622
- const text = ctx.codeText ?? ctx.text;
6623
- const findings = [];
6624
- if (!ctx.path.endsWith(".py")) return findings;
6625
- if (!/playwright/i.test(text)) return findings;
6626
- const re = /\btime\.sleep\s*\(/g;
6627
- let m;
6628
- while ((m = re.exec(text)) !== null) findings.push({
6629
- severity: "warning",
6630
- confidence: "high",
6631
- findingType: "deterministic-defect",
6632
- qaImpact: "FLAKY-RISK",
6633
- file: ctx.path,
6634
- line: lineAt(text, m.index),
6635
- column: colAt(text, m.index),
6636
- message: "`time.sleep()` used to wait for UI state.",
6637
- why: "Fixed sleeps guess at timing: too short → flaky failures under load, too long → slow suite. Playwright locators already auto-wait for the element to be actionable.",
6638
- fix: "Replace with `expect(page.get_by_text(...)).to_be_visible()` or `page.wait_for_selector`."
6639
- });
6640
- return findings;
6641
- }
6642
- });
6643
- //#endregion
6644
6739
  //#region src/rules/python/qa-py-103-wait-for-timeout.ts
6645
6740
  /**
6646
6741
  * QA-PY-103 — Playwright-Python: wait_for_timeout() as synchronization.
@@ -9149,7 +9244,6 @@ const RULES = [
9149
9244
  pwLocatorNormalize,
9150
9245
  pwCodegenArtifact,
9151
9246
  pyPwSyncAsyncMix,
9152
- pyPwHardSleep,
9153
9247
  pyPwWaitForTimeout,
9154
9248
  pyPwNoAssertions,
9155
9249
  ...sharedPageFamily,
@@ -9207,6 +9301,7 @@ const RETIRED_RULE_IDS = [
9207
9301
  "QA-PY-006",
9208
9302
  "QA-PY-008",
9209
9303
  "QA-PY-010",
9304
+ "QA-PY-102",
9210
9305
  "QA-JV-108",
9211
9306
  "QA-CS-108",
9212
9307
  "QA-JV-110",
@@ -12017,7 +12112,6 @@ const BLUNT_MESSAGES = {
12017
12112
  "QA-PW-101": "A hard sleep in a test is a prayer, not a synchronization strategy.",
12018
12113
  "QA-TEST-004": "A hard sleep in a test is a prayer, not a synchronization strategy.",
12019
12114
  "QA-PY-005": "time.sleep() in a test is a prayer. Python doesn't negotiate with timeouts either.",
12020
- "QA-PY-102": "time.sleep() inside a Playwright test — the browser doesn't pause because you asked nicely.",
12021
12115
  "QA-PY-103": "wait_for_timeout() as synchronization — the Playwright-Python edition of hoping.",
12022
12116
  "QA-JV-102": "Thread.sleep() in a test is a prayer. The JVM doesn't negotiate with timeouts.",
12023
12117
  "QA-JV-105": "waitForTimeout is the Java version of closing your eyes and hoping.",
@@ -12658,7 +12752,7 @@ function renderSarif(result, repoRootUri) {
12658
12752
  tool: { driver: {
12659
12753
  name: "Mjölnir",
12660
12754
  informationUri: "https://github.com/Sergey-Bar/Mjolnir",
12661
- version: "0.6.7",
12755
+ version: "0.6.13",
12662
12756
  rules: [...rules.values()].map((r) => {
12663
12757
  const meta = RULES.find((x) => x.id === r.id);
12664
12758
  return {
@@ -18638,7 +18732,7 @@ const { runScan, buildUniversalRules, fallbackWorkspace, pathMatchesGlob, isVali
18638
18732
  * `scripts/sync-sarif-version.cjs` on release and guarded by
18639
18733
  * `tests/version-consistency.spec.ts` locally.
18640
18734
  */
18641
- const CLI_VERSION = "0.6.7";
18735
+ const CLI_VERSION = "0.6.13";
18642
18736
  function parseArgs(argv, onError) {
18643
18737
  const args = {
18644
18738
  target: ".",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mjolnir-qa",
3
- "version": "0.6.7",
3
+ "version": "0.6.13",
4
4
  "description": "Mjölnir — the Verification Trust Engine for QA. Audits test suites and CI pipelines, reports a worthiness score and prioritized findings.",
5
5
  "type": "module",
6
6
  "engines": {