mjolnir-qa 0.5.24 → 0.5.26

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,18 @@ 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.5.26] — 2026-09-08
13
+
14
+ ### Changes since 0.5.25
15
+
16
+ - Certification findings remediation: F1-F5 + P3 (plan 1788806598818) (#57)
17
+
18
+ ## [0.5.25] — 2026-09-08
19
+
20
+ ### Changes since 0.5.24
21
+
22
+ - docs: CERTIFICATION-POLICY.md — consolidated owner-ratified lawbook (1-22 + L1-L6), contradiction pass, verdict semantics; eslint/prettier ignore machine-local .mjolnir scratch (#59)
23
+
12
24
  ## [0.5.24] — 2026-09-08
13
25
 
14
26
  ### Changes since 0.5.23
package/README.ar.md CHANGED
@@ -295,7 +295,7 @@ npx mjolnir-qa@latest --scope changed
295
295
  [docs/FP-AUDIT.md](docs/FP-AUDIT.md)). الـ21 الأخرى تُشحن على تقدير
296
296
  المؤلف. ذيل كل فحص يخبرك كم من القواعد التي _انطلقت_ مقيسة؛ و
297
297
  `mjolnir rules --unmeasured` يسرد غير المقيسة؛ وصفحة `mjolnir explain`
298
- لكل قاعدة تُصرّح بحالتها. ننشر المعدل حتى حين يكون قبيحًا — QA-CS-103
298
+ لكل قاعدة تُصرّح بحالتها. ننشر المعدل حتى حين يكون قبيحًا — QA-PW-107
299
299
  يدقّق عند 95% ولذلك هو في الحجر الصحي. تنمية ذلك العدد هي العمل
300
300
  المستمر للمشروع.
301
301
 
package/README.bn.md CHANGED
@@ -299,7 +299,7 @@ false-positive ফায়ারওয়াল।
299
299
  প্রতিটি স্ক্যানের ফুটার বলে দেয় _ফায়ার_ করা রুলগুলোর কতগুলো পরিমাপকৃত;
300
300
  `mjolnir rules --unmeasured` যেগুলো নয় তা তালিকাভুক্ত করে; প্রতিটি রুলের
301
301
  `mjolnir explain` পেজ তার অবস্থা জানায়। আমরা হারটি প্রকাশ করি — এমনকি
302
- কুৎসিত হলেও — QA-CS-103 ৯৫%-এ অডিট হয় এবং এজন্যই কোয়ারেন্টাইনে। সেই
302
+ কুৎসিত হলেও — QA-PW-107 ৯৫%-এ অডিট হয় এবং এজন্যই কোয়ারেন্টাইনে। সেই
303
303
  সংখ্যা বাড়ানোই প্রজেক্টের চলমান কাজ।
304
304
 
305
305
  ### রুল টিয়ার ও ভাষা-পরিপক্বতা
package/README.br.md CHANGED
@@ -305,7 +305,7 @@ código OSS real** (≥ 10 findings classificados à mão cada; veja
305
305
  a estimativa do autor. O rodapé de cada escaneio diz quantas das regras
306
306
  _que dispararam_ são medidas; `mjolnir rules --unmeasured` lista as que
307
307
  não são; a página `mjolnir explain` de cada regra declara seu status.
308
- Publicamos a taxa mesmo quando ela é feia — QA-CS-103 audita em 95 % e
308
+ Publicamos a taxa mesmo quando ela é feia — QA-PW-107 audita em 95 % e
309
309
  está em quarentena por isso. Fazer esse número crescer é o trabalho contínuo
310
310
  do projeto.
311
311
 
package/README.bs.md CHANGED
@@ -297,7 +297,7 @@ OSS kodom** (≥ 10 ručno klasificiranih nalaza svako; vidi
297
297
  procjeni. Podnožje svakog skana kaže koliko od _okinutih_ pravila je
298
298
  izmjereno; `mjolnir rules --unmeasured` izlista neizmjerena; stranica
299
299
  `mjolnir explain` svakog pravila navodi njen status. Objavljujemo stopu
300
- čak i kad je ružna — QA-CS-103 se audita na 95 % i u karanteni je radi
300
+ čak i kad je ružna — QA-PW-107 se audita na 95 % i u karanteni je radi
301
301
  toga. Rast tog broja je neprekidni rad projekta.
302
302
 
303
303
  ### Tierovi pravila i jezična zrelost
package/README.da.md CHANGED
@@ -303,7 +303,7 @@ det er false-positive-firewallen.
303
303
  estimat. Hver scan-fodnote fortæller, hvor mange af de _udløste_ regler,
304
304
  der er målt; `mjolnir rules --unmeasured` lister de uregistrerede; hver
305
305
  regels `mjolnir explain`-side angiver dens status. Vi offentliggør
306
- raten, selv når den er grim — QA-CS-103 auditeres til 95 % og er sat i
306
+ raten, selv når den er grim — QA-PW-107 auditeres til 95 % og er sat i
307
307
  karantæne for det. At få det tal til at vokse er projektets fortsatte
308
308
  arbejde.
309
309
 
package/README.de.md CHANGED
@@ -308,7 +308,7 @@ Schätzung des Autors. Jeder Scan-Footer sagt dir, wie viele der
308
308
  _ausgelösten_ Regeln gemessen sind; `mjolnir rules --unmeasured` listet
309
309
  die nicht gemessenen; die `mjolnir explain`-Seite jeder Regel nennt
310
310
  ihren Status. Wir veröffentlichen die Rate, selbst wenn sie hässlich
311
- ist — QA-CS-103 auditiert bei 95 % und ist deshalb quarantäniert. Diese
311
+ ist — QA-PW-107 auditiert bei 95 % und ist deshalb quarantäniert. Diese
312
312
  Zahl zu vergrößern ist die fortlaufende Arbeit des Projekts.
313
313
 
314
314
  ### Regel-Tiers und Sprachreife
package/README.es.md CHANGED
@@ -305,7 +305,7 @@ código OSS real** (≥ 10 hallazgos clasificados a mano cada una; ver
305
305
  estimación del autor. Cada pie de escaneo te dice cuántas de las reglas
306
306
  _que se dispararon_ están medidas; `mjolnir rules --unmeasured` lista
307
307
  las que no; la página `mjolnir explain` de cada regla declara su
308
- estado. Publicamos la tasa aunque sea fea — QA-CS-103 audita al 95 % y
308
+ estado. Publicamos la tasa aunque sea fea — QA-PW-107 audita al 95 % y
309
309
  está en cuarentena por ello. Hacer crecer esa cifra es el trabajo continuo
310
310
  del proyecto.
311
311
 
package/README.fr.md CHANGED
@@ -307,7 +307,7 @@ l'estimation de l'auteur. Chaque pied de scan vous dit combien des
307
307
  règles _déclenchées_ sont mesurées ; `mjolnir rules --unmeasured` liste
308
308
  celles qui ne le sont pas ; la page `mjolnir explain` de chaque règle
309
309
  énonce son statut. Nous publions le taux même quand il est laid —
310
- QA-CS-103 s'audite à 95 % et est mis en quarantaine pour ça. Faire
310
+ QA-PW-107 s'audite à 95 % et est mis en quarantaine pour ça. Faire
311
311
  grandir ce chiffre est le travail continu du projet.
312
312
 
313
313
  ### Tiers de règles et maturité par langage
package/README.gr.md CHANGED
@@ -303,7 +303,7 @@ npx mjolnir-qa@latest --scope changed
303
303
  _ενεργούς_ κανόνες είναι μετρημένοι· `mjolnir rules --unmeasured`
304
304
  παραθέτει τους άμετρητους· η σελίδα `mjolnir explain` κάθε κανόνα
305
305
  δηλώνει την κατάστασή του. Δημοσιεύουμε το ποσοστό ακόμα κι όταν είναι
306
- άσχημο — ο QA-CS-103 αυτοελέγχεται στο 95 % και είναι σε καραντίνα γι'
306
+ άσχημο — ο QA-PW-107 αυτοελέγχεται στο 95 % και είναι σε καραντίνα γι'
307
307
  αυτό. Να μεγαλώσει αυτός ο αριθμός είναι η συνεχιζόμενη δουλειά του έργου.
308
308
 
309
309
  ### Tiers κανόνων και ωριμότητα γλωσσών
package/README.he.md CHANGED
@@ -294,7 +294,7 @@ npx mjolnir-qa@latest --scope changed
294
294
  המחבר. התחתית של כל סריקה אומרת כמה מהכללים ש_ירו_ נמדדו;
295
295
  `mjolnir rules --unmeasured` מפרט את אלה שלא; עמוד `mjolnir explain`
296
296
  של כל כלל מצהיר על מעמדו. אנחנו מפרסמים את השיעור גם כשהוא מכוער —
297
- QA-CS-103 נבדק ב‑95% ומצוי בהסגר בגין זה. להגדיל את המספר הזה הוא
297
+ QA-PW-107 נבדק ב‑95% ומצוי בהסגר בגין זה. להגדיל את המספר הזה הוא
298
298
  העבודה המתמשכת של הפרויקט.
299
299
 
300
300
  ### רמות (tiers) של כללים ובשלות לפי שפה
package/README.it.md CHANGED
@@ -304,7 +304,7 @@ codice OSS** (≥ 10 riscontri classificati a mano ciascuna; vedi
304
304
  dell'autore. Ogni footer di scansione dice quante delle regole
305
305
  _scattate_ sono misurate; `mjolnir rules --unmeasured` elenca quelle
306
306
  che non lo sono; la pagina `mjolnir explain` di ogni regola dichiara il
307
- suo stato. Pubblichiamo il tasso anche quando è brutto — QA-CS-103 si
307
+ suo stato. Pubblichiamo il tasso anche quando è brutto — QA-PW-107 si
308
308
  audita al 95 % ed è in quarantena per questo. Far crescere quel numero è il
309
309
  lavoro continuo del progetto.
310
310
 
package/README.ja.md CHANGED
@@ -300,7 +300,7 @@ false-positive 率を備えています**(各ルールにつき手作業で分
300
300
  は作者の推定で出荷されます。すべてのスキャンのフッターは、_発火した_
301
301
  ルールのうちいくつが測定済みかを教えてくれます。`mjolnir rules --unmeasured`
302
302
  は未測定のものを列挙します。各ルールの `mjolnir explain` ページはその
303
- 状態を明言します。率は醜くても公開します——QA-CS-103 は 95% で監査され、
303
+ 状態を明言します。率は醜くても公開します——QA-PW-107 は 95% で監査され、
304
304
  それゆえ隔離されています。この数字を増やすことが、プロジェクトの継続的
305
305
  な仕事です。
306
306
 
package/README.ko.md CHANGED
@@ -298,7 +298,7 @@ npx mjolnir-qa@latest --scope changed
298
298
  출시됩니다. 모든 스캔의 바닥글은 _발화한_ 규칙 중 몇 개가 측정되었는지
299
299
  말해줍니다; `mjolnir rules --unmeasured`는 측정되지 않은 것들을 나열합니다;
300
300
  각 규칙의 `mjolnir explain` 페이지는 그 상태를 명시합니다. 수치가 흉해도
301
- 우리는 비율을 공개합니다 — QA-CS-103은 95%로 감사되었고 그래서 격리
301
+ 우리는 비율을 공개합니다 — QA-PW-107은 95%로 감사되었고 그래서 격리
302
302
  계층에 있습니다. 그 숫자를 늘려가는 것이 프로젝트의 지속적인 작업입니다.
303
303
 
304
304
  ### 규칙 계층과 언어 성숙도
package/README.md CHANGED
@@ -216,8 +216,9 @@ Requires Node.js ≥ 22.18. Works on Windows, macOS, and Linux.
216
216
  - **QA / SDET** owning an e2e or integration suite who need evidence the
217
217
  suite actually deserves the green checkmark it produces.
218
218
  - **Platform / DevEx** teams responsible for CI integrity and release
219
- gates — the people who care that a `continue-on-error` never silently
220
- turns a red pipeline green.
219
+ gates — the people who care that a swallowed exit code (`\|\| true`,
220
+ caught by default) or a `continue-on-error` gate (caught under
221
+ `--strict`) never silently turns a red pipeline green.
221
222
  - **OSS maintainers** who want a cheap, always-on verification gate that
222
223
  runs locally and in CI with zero network calls.
223
224
 
@@ -246,8 +247,8 @@ frameworks.
246
247
  **Use AI review too.** It catches nuance, intent, and design flaws no regex
247
248
  can find. Mjölnir catches the structural patterns AI overlooks because they
248
249
  look "intentional" — a committed `.only`, a swallowed exit code, a
249
- `continue-on-error` on a test job. Those aren't bugs that need reasoning;
250
- they're facts that need scanning.
250
+ `continue-on-error` on a test job (that one under `--strict`). Those aren't
251
+ bugs that need reasoning; they're facts that need scanning.
251
252
 
252
253
  ---
253
254
 
@@ -258,7 +259,7 @@ they're facts that need scanning.
258
259
  | ⚖️ | **Worthiness Score** — one number, transparent deduction table, no black box |
259
260
  | 🎭 | **Selector Health Score** — grades your Playwright locators, not just your pass rate |
260
261
  | 🔬 | **Runtime forensics** — reads real Playwright/JUnit run data to catch `TRUE-FLAKE`, not just static guesses |
261
- | 🚨 | **CI-integrity rules** — catches `continue-on-error`, `\|\| true`, and other false-green tricks |
262
+ | 🚨 | **CI-integrity rules** — catches `\|\| true` by default; `continue-on-error` detection runs under `--strict` |
262
263
  | 🐍 | **All four Playwright bindings** — TypeScript, Python, Java, C#/.NET — plus pytest, JUnit/TestNG and CI workflows |
263
264
  | 🔒 | **Local-first** — zero network calls while scanning, zero telemetry, runs in seconds |
264
265
 
@@ -271,74 +272,77 @@ full catalog lives in [`docs/rules/`](docs/rules/),
271
272
  [what it checks](https://sergey-bar.github.io/Mjolnir/guide/what-it-checks),
272
273
  or `mjolnir rules --md`.
273
274
 
275
+ > **Tier** — `quarantine` rules run only under `--strict` and never gate
276
+ > (capped to info); the severity shown is the authored severity.
277
+
274
278
  <details>
275
279
  <summary><strong>Test Hygiene</strong></summary>
276
280
 
277
- | ID | Rule | Severity |
278
- | ----------- | --------------------------------------------------- | -------- |
279
- | QA-TEST-001 | Focused test committed (`.only`, `fit`) | error |
280
- | QA-TEST-002 | Skipped test without justification | error |
281
- | QA-TEST-002 | Skipped test with tracked justification | warning |
282
- | QA-TEST-003 | Test with no assertions | error |
283
- | QA-TEST-004 | Hard sleep (`waitForTimeout`, `sleep()`, `delay()`) | warning |
284
- | QA-TEST-006 | Retry abuse hiding flakiness | warning |
285
- | QA-TEST-010 | Empty test body | error |
281
+ | ID | Rule | Severity | Tier |
282
+ | ----------- | --------------------------------------------------- | -------- | ---------- |
283
+ | QA-TEST-001 | Focused test committed (`.only`, `fit`) | error | quarantine |
284
+ | QA-TEST-002 | Skipped test without justification | error | quarantine |
285
+ | QA-TEST-002 | Skipped test with tracked justification | warning | quarantine |
286
+ | QA-TEST-003 | Test with no assertions | error | quarantine |
287
+ | QA-TEST-004 | Hard sleep (`waitForTimeout`, `sleep()`, `delay()`) | warning | extended |
288
+ | QA-TEST-006 | Retry abuse hiding flakiness | warning | quarantine |
289
+ | QA-TEST-010 | Empty test body | error | quarantine |
286
290
 
287
291
  </details>
288
292
 
289
293
  <details>
290
294
  <summary><strong>Test Quality</strong></summary>
291
295
 
292
- | ID | Rule | Severity |
293
- | ------------ | --------------------------- | -------- |
294
- | QA-TQUAL-001 | Mock-only verification | info |
295
- | QA-TQUAL-002 | Tautological assertion | error |
296
- | QA-TQUAL-009 | Unawaited promise assertion | error |
297
- | QA-TQUAL-011 | Commented-out tests | warning |
296
+ | ID | Rule | Severity | Tier |
297
+ | ------------ | --------------------------- | -------- | ---------- |
298
+ | QA-TQUAL-001 | Mock-only verification | info | quarantine |
299
+ | QA-TQUAL-002 | Tautological assertion | error | quarantine |
300
+ | QA-TQUAL-009 | Unawaited promise assertion | error | quarantine |
301
+ | QA-TQUAL-011 | Commented-out tests | warning | extended |
298
302
 
299
303
  </details>
300
304
 
301
305
  <details>
302
306
  <summary><strong>Playwright 🎭</strong></summary>
303
307
 
304
- | ID | Rule | Severity |
305
- | --------- | ---------------------------------------- | -------- |
306
- | QA-PW-002 | Unawaited locator assertion | error |
307
- | QA-PW-003 | `page.pause()` / `test.only()` committed | error |
308
- | QA-PW-004 | Brittle CSS/XPath selectors | warning |
309
- | QA-PW-005 | Business logic inside `page.evaluate()` | info |
310
- | QA-PW-114 | Legacy element handles (`page.$`) | info |
311
- | QA-PW-118 | `networkidle` waits (flaky by design) | info |
312
- | QA-PW-123 | Hardcoded environment URLs | warning |
308
+ | ID | Rule | Severity | Tier |
309
+ | --------- | ---------------------------------------- | -------- | ---------- |
310
+ | QA-PW-002 | Unawaited locator assertion | error | core |
311
+ | QA-PW-003 | `page.pause()` / `test.only()` committed | error | core |
312
+ | QA-PW-004 | Brittle CSS/XPath selectors | warning | quarantine |
313
+ | QA-PW-005 | Business logic inside `page.evaluate()` | info | quarantine |
314
+ | QA-PW-114 | Legacy element handles (`page.$`) | info | quarantine |
315
+ | QA-PW-118 | `networkidle` waits (flaky by design) | info | quarantine |
316
+ | QA-PW-123 | Hardcoded environment URLs | warning | quarantine |
313
317
 
314
318
  </details>
315
319
 
316
320
  <details>
317
321
  <summary><strong>CI Integrity</strong></summary>
318
322
 
319
- | ID | Rule | Severity |
320
- | --------- | ----------------------------------------------------------------- | -------- |
321
- | QA-CI-001 | `continue-on-error` masks failures | error |
322
- | QA-CI-002 | `\|\| true` swallows exit codes | error |
323
- | QA-CI-005 | Report consumed but never generated | error |
324
- | QA-CI-007 | Retry wrappers around tests | warning |
325
- | QA-CI-008 | Always-success step masks failures | error |
326
- | QA-CI-009 | Test exit code not propagated (`\|` without pipefail, `;` chains) | error |
327
- | QA-CI-010 | Tests skipped where they must block (skip-on-PR guards) | error |
323
+ | ID | Rule | Severity | Tier |
324
+ | --------- | ----------------------------------------------------------------- | -------- | ---------- |
325
+ | QA-CI-001 | `continue-on-error` masks failures | error | quarantine |
326
+ | QA-CI-002 | `\|\| true` swallows exit codes | error | extended |
327
+ | QA-CI-005 | Report consumed but never generated | error | quarantine |
328
+ | QA-CI-007 | Retry wrappers around tests | warning | extended |
329
+ | QA-CI-008 | Always-success step masks failures | error | quarantine |
330
+ | QA-CI-009 | Test exit code not propagated (`\|` without pipefail, `;` chains) | error | extended |
331
+ | QA-CI-010 | Tests skipped where they must block (skip-on-PR guards) | error | quarantine |
328
332
 
329
333
  </details>
330
334
 
331
335
  <details>
332
336
  <summary><strong>Python / pytest 🐍</strong></summary>
333
337
 
334
- | ID | Rule | Severity |
335
- | --------- | ----------------------------------------- | -------- |
336
- | QA-PY-002 | Skipped test (`skip`, non-strict `xfail`) | warning |
337
- | QA-PY-003 | Test function with no assertions | error |
338
- | QA-PY-005 | `time.sleep()` in tests | warning |
339
- | QA-PY-006 | Empty test body (`pass`) | info |
340
- | QA-PY-010 | Random/time dependence without freeze | info |
341
- | QA-PY-012 | Tautological assertion | error |
338
+ | ID | Rule | Severity | Tier |
339
+ | --------- | ----------------------------------------- | -------- | ---------- |
340
+ | QA-PY-002 | Skipped test (`skip`, non-strict `xfail`) | warning | core |
341
+ | QA-PY-003 | Test function with no assertions | error | quarantine |
342
+ | QA-PY-005 | `time.sleep()` in tests | warning | extended |
343
+ | QA-PY-006 | Empty test body (`pass`) | info | quarantine |
344
+ | QA-PY-010 | Random/time dependence without freeze | info | quarantine |
345
+ | QA-PY-012 | Tautological assertion | error | quarantine |
342
346
 
343
347
  20 Python rules total (QA-PY-001…012 pytest hygiene + QA-PY-101…108 Playwright-Python).
344
348
 
@@ -347,30 +351,30 @@ or `mjolnir rules --md`.
347
351
  <details>
348
352
  <summary><strong>Java / JUnit · TestNG ☕</strong></summary>
349
353
 
350
- | ID | Rule | Severity |
351
- | --------- | ---------------------------------------- | -------- |
352
- | QA-JV-101 | Disabled test (`@Disabled`) | warning |
353
- | QA-JV-102 | Hard sleep (`Thread.sleep()`) | warning |
354
- | QA-JV-103 | Test method with no assertions | error |
355
- | QA-JV-105 | Playwright `waitForTimeout()` hard sleep | warning |
356
- | QA-JV-106 | Brittle selector instead of role locator | warning |
357
- | QA-JV-108 | Hardcoded environment URL in test | info |
358
- | QA-JV-111 | Blanket `page.route("**")` mock | info |
354
+ | ID | Rule | Severity | Tier |
355
+ | --------- | ---------------------------------------- | -------- | ---------- |
356
+ | QA-JV-101 | Disabled test (`@Disabled`) | warning | core |
357
+ | QA-JV-102 | Hard sleep (`Thread.sleep()`) | warning | extended |
358
+ | QA-JV-103 | Test method with no assertions | error | extended |
359
+ | QA-JV-105 | Playwright `waitForTimeout()` hard sleep | warning | core |
360
+ | QA-JV-106 | Brittle selector instead of role locator | warning | quarantine |
361
+ | QA-JV-108 | Hardcoded environment URL in test | info | quarantine |
362
+ | QA-JV-111 | Blanket `page.route("**")` mock | info | quarantine |
359
363
 
360
364
  </details>
361
365
 
362
366
  <details>
363
367
  <summary><strong>C# / .NET — NUnit · xUnit · MSTest 🟣</strong></summary>
364
368
 
365
- | ID | Rule | Severity |
366
- | --------- | ------------------------------------------ | -------- |
367
- | QA-CS-101 | Skipped test (`[Ignore]`, `[Fact(Skip=)]`) | warning |
368
- | QA-CS-102 | Hard sleep (`Thread.Sleep` / `Task.Delay`) | warning |
369
- | QA-CS-103 | Test method with no assertions | error |
370
- | QA-CS-105 | `WaitForTimeoutAsync()` hard sleep | warning |
371
- | QA-CS-106 | Brittle selector instead of role locator | warning |
372
- | QA-CS-108 | Hardcoded environment URL in test | info |
373
- | QA-CS-111 | Blanket `page.RouteAsync("**")` mock | info |
369
+ | ID | Rule | Severity | Tier |
370
+ | --------- | ------------------------------------------ | -------- | ---------- |
371
+ | QA-CS-101 | Skipped test (`[Ignore]`, `[Fact(Skip=)]`) | warning | core |
372
+ | QA-CS-102 | Hard sleep (`Thread.Sleep` / `Task.Delay`) | warning | core |
373
+ | QA-CS-103 | Test method with no assertions | error | core |
374
+ | QA-CS-105 | `WaitForTimeoutAsync()` hard sleep | warning | extended |
375
+ | QA-CS-106 | Brittle selector instead of role locator | warning | quarantine |
376
+ | QA-CS-108 | Hardcoded environment URL in test | info | quarantine |
377
+ | QA-CS-111 | Blanket `page.RouteAsync("**")` mock | info | quarantine |
374
378
 
375
379
  </details>
376
380
 
@@ -413,7 +417,7 @@ test failure scheduled for whenever someone touches the markup.
413
417
  estimate. Every scan footer tells you how many of the rules that _fired_
414
418
  are measured; `mjolnir rules --unmeasured` lists the ones that aren't;
415
419
  every rule's `mjolnir explain` page states its status. We publish the rate
416
- even when it's ugly — QA-CS-103 audits at 95% and is quarantined for it.
420
+ even when it's ugly — QA-PW-107 audits at 95% and is quarantined for it.
417
421
  Growing that number is the project's continuing work.
418
422
 
419
423
  ### Rule tiers and language maturity
package/README.no.md CHANGED
@@ -302,7 +302,7 @@ det er false-positive-brannmuren.
302
302
  forfatterens estimat. Hver skann-fotnote forteller hvor mange av de
303
303
  _utløste_ reglene som er målt; `mjolnir rules --unmeasured` lister de
304
304
  umålte; hver regels `mjolnir explain`-side angir statusen. Vi publiserer
305
- raten selv når den er stygg — QA-CS-103 auditeres til 95 % og er satt i
305
+ raten selv når den er stygg — QA-PW-107 auditeres til 95 % og er satt i
306
306
  karantene for det. Å få det tallet til å vokse er prosjektets fortsatte
307
307
  arbeid.
308
308
 
package/README.pl.md CHANGED
@@ -303,7 +303,7 @@ zob. [docs/FP-AUDIT.md](docs/FP-AUDIT.md)). Pozostałe 21 wychodzi na
303
303
  oszacowaniu autora. Stopka każdego skanu mówi, ile z _odpalonych_
304
304
  reguł jest zmierzonych; `mjolnir rules --unmeasured` wypisuje
305
305
  niezmierzone; strona `mjolnir explain` każdej reguły deklaruje jej
306
- status. Publikujemy stopę, nawet gdy jest brzydka — QA-CS-103 audytuje
306
+ status. Publikujemy stopę, nawet gdy jest brzydka — QA-PW-107 audytuje
307
307
  się na 95 % i za to trafia do kwarantanny. Powiększanie tej liczby to
308
308
  stale trwająca praca projektu.
309
309
 
package/README.ru.md CHANGED
@@ -304,7 +304,7 @@ macOS и Linux.
304
304
  выходят на оценке автора. Футер каждого скана говорит, сколько из
305
305
  _сработавших_ правил измерены; `mjolnir rules --unmeasured` перечисляет
306
306
  неизмеренные; страница `mjolnir explain` каждого правила указывает её
307
- статус. Мы публикуем частоту, даже когда она уродлива — QA-CS-103
307
+ статус. Мы публикуем частоту, даже когда она уродлива — QA-PW-107
308
308
  аудируется на 95 % и за это отправлен в карантин. Увеличивать это
309
309
  число — постоянная работа проекта.
310
310
 
package/README.th.md CHANGED
@@ -295,7 +295,7 @@ fixture ลบของตัวเองจะปล่อยไม่ได้
295
295
  [docs/FP-AUDIT.md](docs/FP-AUDIT.md)) อีก 21 กฎออกมาบนการประเมินของผู้เขียน
296
296
  ส่วนท้ายของทุกการสแกนบอกว่ากฎที่ _ยิง_ มีกี่กฎที่วัดแล้ว;
297
297
  `mjolnir rules --unmeasured` แสดงกฎที่ยังไม่วัด; หน้า `mjolnir explain`
298
- ของทุกกฎระบุสถานะ เราเผยแพร่อัตรานี้แม้มันจะน่าเกลียด — QA-CS-103 ตรวจได้
298
+ ของทุกกฎระบุสถานะ เราเผยแพร่อัตรานี้แม้มันจะน่าเกลียด — QA-PW-107 ตรวจได้
299
299
  ที่ 95 % และถูกส่งไปกักกันเพราะเหตุนี้ การทำให้ตัวเลขนั้นโตขึ้นคืองาน
300
300
  ต่อเนื่องของโปรเจกต์
301
301
 
package/README.tr.md CHANGED
@@ -300,7 +300,7 @@ oranı taşıyor** (her biri için ≥ 10 elle sınıflandırılmış bulgu; bkz
300
300
  yayına giriyor. Her tarama alt bilgisi, _tetiklenen_ kuralların kaçının
301
301
  ölçüldüğünü söyler; `mjolnir rules --unmeasured` ölçülmeyenleri listeler;
302
302
  her kuralın `mjolnir explain` sayfası durumunu belirtir. Oranı çirkin
303
- olduğunda bile yayımlarız — QA-CS-103 %95 ile denetleniyor ve bu yüzden
303
+ olduğunda bile yayımlarız — QA-PW-107 %95 ile denetleniyor ve bu yüzden
304
304
  karantinada. O sayıyı büyütmek, projenin süregelen işidir.
305
305
 
306
306
  ### Kural katmanları ve dil olgunluğu
package/README.uk.md CHANGED
@@ -302,7 +302,7 @@ OSS-коді** (по ≥ 10 вручну класифікованих знахі
302
302
  автора. Футер кожного скана каже, скільки із _спрацьованих_ правил
303
303
  виміряно; `mjolnir rules --unmeasured` перелічує невиміряні; сторінка
304
304
  `mjolnir explain` кожного правила вказує її статус. Ми публікуємо
305
- частоту, навіть коли вона негарна — QA-CS-103 аудитується на 95 % і за
305
+ частоту, навіть коли вона негарна — QA-PW-107 аудитується на 95 % і за
306
306
  це відправлено в карантин. Збільшувати це число — постійна робота проєкту.
307
307
 
308
308
  ### Тіри правил і зрілість мов
package/README.vi.md CHANGED
@@ -298,7 +298,7 @@ thật** (≥ 10 finding được phân loại tay mỗi quy tắc; xem
298
298
  ước lượng của tác giả. Chân mỗi bản quét cho biết bao nhiêu quy tắc
299
299
  _đã bắn_ được đo; `mjolnir rules --unmeasured` liệt kê những quy tắc
300
300
  chưa đo; trang `mjolnir explain` của từng quy tắc nêu trạng thái. Chúng
301
- tôi công bố tỷ lệ kể cả khi nó xấu xí — QA-CS-103 kiểm toán ở mức 95 %
301
+ tôi công bố tỷ lệ kể cả khi nó xấu xí — QA-PW-107 kiểm toán ở mức 95 %
302
302
  và bị cách ly vì thế. Mở rộng con số đó là công việc liên tục của
303
303
  dự án.
304
304
 
package/README.zh.md CHANGED
@@ -292,7 +292,7 @@ npx mjolnir-qa@latest --scope changed
292
292
  作者的估计发布。每次扫描的页脚都会告诉你,_触发过的_ 规则中有多少经过
293
293
  测量;`mjolnir rules --unmeasured` 列出未测量的;每条规则的
294
294
  `mjolnir explain` 页面都声明其状态。即使数字难看我们也照样公布——
295
- QA-CS-103 的实测假阳性率是 95%,因此被隔离。把这个数字扩大,是项目的
295
+ QA-PW-107 的实测假阳性率是 95%,因此被隔离。把这个数字扩大,是项目的
296
296
  持续性工作。
297
297
 
298
298
  ### 规则层级与语言成熟度
package/README.zht.md CHANGED
@@ -292,7 +292,7 @@ npx mjolnir-qa@latest --scope changed
292
292
  作者的估計發布。每次掃描的頁尾都會告訴你,_觸發過的_ 規則中有多少經過
293
293
  測量;`mjolnir rules --unmeasured` 列出未測量的;每條規則的
294
294
  `mjolnir explain` 頁面都聲明其狀態。即使數字難看我們也照樣公布——
295
- QA-CS-103 的實測假陽性率是 95%,因此被隔離。把這個數字擴大,是專案的
295
+ QA-PW-107 的實測假陽性率是 95%,因此被隔離。把這個數字擴大,是專案的
296
296
  持續性工作。
297
297
 
298
298
  ### 規則層級與語言成熟度
package/dist/cli.d.mts CHANGED
@@ -728,7 +728,7 @@ declare const runScan: typeof runScan$1, buildUniversalRules: typeof buildUniver
728
728
  * `scripts/sync-sarif-version.cjs` on release and guarded by
729
729
  * `tests/version-consistency.spec.ts` locally.
730
730
  */
731
- declare const CLI_VERSION = "0.5.24";
731
+ declare const CLI_VERSION = "0.5.26";
732
732
  /** A usage-error detail: the offending token, when one exists. */
733
733
  interface UsageErrorDetail {
734
734
  /** The unknown flag or rejected value (e.g. `--nope`, `loud`). */
package/dist/cli.mjs CHANGED
@@ -13290,7 +13290,7 @@ function renderSarif(result, repoRootUri) {
13290
13290
  tool: { driver: {
13291
13291
  name: "Mjölnir",
13292
13292
  informationUri: "https://github.com/Sergey-Bar/Mjolnir",
13293
- version: "0.5.24",
13293
+ version: "0.5.26",
13294
13294
  rules: [...rules.values()].map((r) => {
13295
13295
  const meta = RULES.find((x) => x.id === r.id);
13296
13296
  return {
@@ -13356,7 +13356,7 @@ function renderSarif(result, repoRootUri) {
13356
13356
  ].join("."),
13357
13357
  runs: [repoRootUri ? {
13358
13358
  ...run,
13359
- originalUriBaseIds: { SRCROOT: { uri: repoRootUri.replaceAll("\\", "/").split("/").map((seg) => seg === "" ? "" : encodeURI(seg).replaceAll("#", "%23").replaceAll("?", "%3F")).join("/") } }
13359
+ originalUriBaseIds: { SRCROOT: { uri: repoRootUri.replaceAll("\\", "/").split("/").map((seg) => seg === "" ? "" : encodeURI(seg).replaceAll("%25", "%").replaceAll("#", "%23").replaceAll("?", "%3F")).join("/") } }
13360
13360
  } : run]
13361
13361
  };
13362
13362
  return JSON.stringify(sarif, null, 2);
@@ -13742,7 +13742,11 @@ function errorText$1(err) {
13742
13742
  /**
13743
13743
  * Parse-and-validate a saved report. Throws Error with a
13744
13744
  * human-explanatory message on: invalid JSON, non-object document,
13745
- * wrong schemaVersion, missing findings array.
13745
+ * wrong schemaVersion, missing findings array, missing frameworks
13746
+ * array (certification F1: a schema-1 report is MORE than
13747
+ * `{schemaVersion, findings}` — summary/handoff/why read
13748
+ * `result.frameworks`, so a schema-incomplete stub must be rejected
13749
+ * HERE, by the one shared loader, not crash downstream).
13746
13750
  */
13747
13751
  function validateReportJson(text) {
13748
13752
  let parsed;
@@ -13755,6 +13759,7 @@ function validateReportJson(text) {
13755
13759
  const doc = parsed;
13756
13760
  if (doc.schemaVersion !== 1) throw new Error(`unsupported schemaVersion ${JSON.stringify(doc.schemaVersion)} — expected 1`);
13757
13761
  if (!Array.isArray(doc.findings)) throw new Error("missing a \"findings\" array — is this a Mjölnir --json report?");
13762
+ if (!Array.isArray(doc.frameworks)) throw new Error("missing a \"frameworks\" array — is this a complete Mjölnir --json report?");
13758
13763
  return parsed;
13759
13764
  }
13760
13765
  /** Load + validate a saved report from disk. Throws on any problem. */
@@ -16272,8 +16277,22 @@ function findEntry(verb) {
16272
16277
  function hasVerbHelp(verb) {
16273
16278
  return findEntry(verb) !== void 0;
16274
16279
  }
16280
+ /**
16281
+ * Verbs whose detailed help IS the root help (certification P3):
16282
+ * `scan` is the product's one command — the registry has no separate
16283
+ * scan page, so `mjolnir scan --help` / `mjolnir help scan` must render
16284
+ * the overview (which carries the scan usage lines), not the "no
16285
+ * detailed help" stub. `ci` (the bare stem) and `help` are the same
16286
+ * shape: real verbs, no dedicated page.
16287
+ */
16288
+ const ROOT_HELP_VERBS = /* @__PURE__ */ new Set([
16289
+ "scan",
16290
+ "ci",
16291
+ "help"
16292
+ ]);
16275
16293
  /** One per-verb help page: summary, usage, examples, next step. */
16276
16294
  function renderVerbHelp(verb) {
16295
+ if (!hasVerbHelp(verb) && ROOT_HELP_VERBS.has(verb)) return renderRootHelp();
16277
16296
  const e = findEntry(verb);
16278
16297
  if (!e) return [
16279
16298
  ` No detailed help for "${verb}".`,
@@ -18327,6 +18346,17 @@ function renderDoctorReport(report) {
18327
18346
  /** Versioned JSON schema name for `doctor --json` (G5). */
18328
18347
  const DOCTOR_REPORT_SCHEMA = "mjolnir.doctor-report@1";
18329
18348
  /**
18349
+ * G5 determinism allowlist: names of doctor --json fields whose values
18350
+ * legitimately vary between two runs on the same tree. EMPTY by default
18351
+ * — every field the doctor emits must be deterministic (stable key
18352
+ * order, no timestamps, no absolute paths), and the structural tests
18353
+ * assert this length stays 0. A future field that cannot be
18354
+ * deterministic (e.g. a duration) must be added to this constant
18355
+ * CONSCIOUSLY, with the CI byte-equality gate's expectations updated in
18356
+ * the same commit — never by quietly weakening the byte-compare.
18357
+ */
18358
+ const NON_DETERMINISTIC_FIELDS = [];
18359
+ /**
18330
18360
  * Serializes the doctor report to the machine-readable contract (Phase 5,
18331
18361
  * G5): versioned `schema` field, stable key order (constructed once, here),
18332
18362
  * details limited exactly like the text render, paths already POSIX-relative
@@ -18350,13 +18380,22 @@ function doctorReportJson(report, opts = {}) {
18350
18380
  details: c.details.length > maxDetails ? [...c.details.slice(0, maxDetails), `… and ${c.details.length - maxDetails} more`] : c.details
18351
18381
  };
18352
18382
  });
18353
- return {
18383
+ return stripNonDeterministicFields({
18354
18384
  schema: DOCTOR_REPORT_SCHEMA,
18355
18385
  healthy: report.healthy,
18356
18386
  summary,
18357
18387
  checks,
18358
18388
  measurement: report.measurement
18359
- };
18389
+ });
18390
+ }
18391
+ /**
18392
+ * G5 strip step: removes every allowlisted field from the artifact.
18393
+ * `fields` is a seam (defaults to the shipped NON_DETERMINISTIC_FIELDS)
18394
+ * so the strip path is testable while the allowlist stays empty.
18395
+ */
18396
+ function stripNonDeterministicFields(json, fields = NON_DETERMINISTIC_FIELDS) {
18397
+ for (const field of fields) delete json[field];
18398
+ return json;
18360
18399
  }
18361
18400
  //#endregion
18362
18401
  //#region src/commands/doctor-run.ts
@@ -18623,7 +18662,7 @@ const { runScan, buildUniversalRules, fallbackWorkspace, pathMatchesGlob, isVali
18623
18662
  * `scripts/sync-sarif-version.cjs` on release and guarded by
18624
18663
  * `tests/version-consistency.spec.ts` locally.
18625
18664
  */
18626
- const CLI_VERSION = "0.5.24";
18665
+ const CLI_VERSION = "0.5.26";
18627
18666
  function parseArgs(argv, onError) {
18628
18667
  const args = {
18629
18668
  target: ".",
@@ -19022,7 +19061,7 @@ async function runScanCommand(argv, io = {
19022
19061
  io.out(result.score === null ? "unknown" : String(result.score));
19023
19062
  return exitForFindings(result.findings, args.blocking === "none" ? "advisory" : args.blocking ?? scoreConfig.gate ?? "error");
19024
19063
  }
19025
- if (args.format === "sarif") io.out(renderSarif(result));
19064
+ if (args.format === "sarif") io.out(renderSarif(result, pathToFileURL(target).href));
19026
19065
  else if (args.format === "mermaid") io.out(renderMermaid(result));
19027
19066
  else if (args.json) io.out(JSON.stringify({
19028
19067
  ...result,
@@ -13289,7 +13289,7 @@ function renderSarif(result, repoRootUri) {
13289
13289
  tool: { driver: {
13290
13290
  name: "Mjölnir",
13291
13291
  informationUri: "https://github.com/Sergey-Bar/Mjolnir",
13292
- version: "0.5.24",
13292
+ version: "0.5.26",
13293
13293
  rules: [...rules.values()].map((r) => {
13294
13294
  const meta = RULES.find((x) => x.id === r.id);
13295
13295
  return {
@@ -13355,7 +13355,7 @@ function renderSarif(result, repoRootUri) {
13355
13355
  ].join("."),
13356
13356
  runs: [repoRootUri ? {
13357
13357
  ...run,
13358
- originalUriBaseIds: { SRCROOT: { uri: repoRootUri.replaceAll("\\", "/").split("/").map((seg) => seg === "" ? "" : encodeURI(seg).replaceAll("#", "%23").replaceAll("?", "%3F")).join("/") } }
13358
+ originalUriBaseIds: { SRCROOT: { uri: repoRootUri.replaceAll("\\", "/").split("/").map((seg) => seg === "" ? "" : encodeURI(seg).replaceAll("%25", "%").replaceAll("#", "%23").replaceAll("?", "%3F")).join("/") } }
13359
13359
  } : run]
13360
13360
  };
13361
13361
  return JSON.stringify(sarif, null, 2);
@@ -13741,7 +13741,11 @@ function errorText$1(err) {
13741
13741
  /**
13742
13742
  * Parse-and-validate a saved report. Throws Error with a
13743
13743
  * human-explanatory message on: invalid JSON, non-object document,
13744
- * wrong schemaVersion, missing findings array.
13744
+ * wrong schemaVersion, missing findings array, missing frameworks
13745
+ * array (certification F1: a schema-1 report is MORE than
13746
+ * `{schemaVersion, findings}` — summary/handoff/why read
13747
+ * `result.frameworks`, so a schema-incomplete stub must be rejected
13748
+ * HERE, by the one shared loader, not crash downstream).
13745
13749
  */
13746
13750
  function validateReportJson(text) {
13747
13751
  let parsed;
@@ -13754,6 +13758,7 @@ function validateReportJson(text) {
13754
13758
  const doc = parsed;
13755
13759
  if (doc.schemaVersion !== 1) throw new Error(`unsupported schemaVersion ${JSON.stringify(doc.schemaVersion)} — expected 1`);
13756
13760
  if (!Array.isArray(doc.findings)) throw new Error("missing a \"findings\" array — is this a Mjölnir --json report?");
13761
+ if (!Array.isArray(doc.frameworks)) throw new Error("missing a \"frameworks\" array — is this a complete Mjölnir --json report?");
13757
13762
  return parsed;
13758
13763
  }
13759
13764
  /** Load + validate a saved report from disk. Throws on any problem. */
@@ -15463,8 +15468,22 @@ function findEntry(verb) {
15463
15468
  function hasVerbHelp(verb) {
15464
15469
  return findEntry(verb) !== void 0;
15465
15470
  }
15471
+ /**
15472
+ * Verbs whose detailed help IS the root help (certification P3):
15473
+ * `scan` is the product's one command — the registry has no separate
15474
+ * scan page, so `mjolnir scan --help` / `mjolnir help scan` must render
15475
+ * the overview (which carries the scan usage lines), not the "no
15476
+ * detailed help" stub. `ci` (the bare stem) and `help` are the same
15477
+ * shape: real verbs, no dedicated page.
15478
+ */
15479
+ const ROOT_HELP_VERBS = /* @__PURE__ */ new Set([
15480
+ "scan",
15481
+ "ci",
15482
+ "help"
15483
+ ]);
15466
15484
  /** One per-verb help page: summary, usage, examples, next step. */
15467
15485
  function renderVerbHelp(verb) {
15486
+ if (!hasVerbHelp(verb) && ROOT_HELP_VERBS.has(verb)) return renderRootHelp();
15468
15487
  const e = findEntry(verb);
15469
15488
  if (!e) return [
15470
15489
  ` No detailed help for "${verb}".`,
@@ -17809,6 +17828,17 @@ function renderDoctorReport(report) {
17809
17828
  /** Versioned JSON schema name for `doctor --json` (G5). */
17810
17829
  const DOCTOR_REPORT_SCHEMA = "mjolnir.doctor-report@1";
17811
17830
  /**
17831
+ * G5 determinism allowlist: names of doctor --json fields whose values
17832
+ * legitimately vary between two runs on the same tree. EMPTY by default
17833
+ * — every field the doctor emits must be deterministic (stable key
17834
+ * order, no timestamps, no absolute paths), and the structural tests
17835
+ * assert this length stays 0. A future field that cannot be
17836
+ * deterministic (e.g. a duration) must be added to this constant
17837
+ * CONSCIOUSLY, with the CI byte-equality gate's expectations updated in
17838
+ * the same commit — never by quietly weakening the byte-compare.
17839
+ */
17840
+ const NON_DETERMINISTIC_FIELDS = [];
17841
+ /**
17812
17842
  * Serializes the doctor report to the machine-readable contract (Phase 5,
17813
17843
  * G5): versioned `schema` field, stable key order (constructed once, here),
17814
17844
  * details limited exactly like the text render, paths already POSIX-relative
@@ -17832,13 +17862,22 @@ function doctorReportJson(report, opts = {}) {
17832
17862
  details: c.details.length > maxDetails ? [...c.details.slice(0, maxDetails), `… and ${c.details.length - maxDetails} more`] : c.details
17833
17863
  };
17834
17864
  });
17835
- return {
17865
+ return stripNonDeterministicFields({
17836
17866
  schema: DOCTOR_REPORT_SCHEMA,
17837
17867
  healthy: report.healthy,
17838
17868
  summary,
17839
17869
  checks,
17840
17870
  measurement: report.measurement
17841
- };
17871
+ });
17872
+ }
17873
+ /**
17874
+ * G5 strip step: removes every allowlisted field from the artifact.
17875
+ * `fields` is a seam (defaults to the shipped NON_DETERMINISTIC_FIELDS)
17876
+ * so the strip path is testable while the allowlist stays empty.
17877
+ */
17878
+ function stripNonDeterministicFields(json, fields = NON_DETERMINISTIC_FIELDS) {
17879
+ for (const field of fields) delete json[field];
17880
+ return json;
17842
17881
  }
17843
17882
  //#endregion
17844
17883
  //#region src/commands/doctor-run.ts
@@ -18259,7 +18298,7 @@ const { runScan, buildUniversalRules, fallbackWorkspace, pathMatchesGlob, isVali
18259
18298
  * `scripts/sync-sarif-version.cjs` on release and guarded by
18260
18299
  * `tests/version-consistency.spec.ts` locally.
18261
18300
  */
18262
- const CLI_VERSION = "0.5.24";
18301
+ const CLI_VERSION = "0.5.26";
18263
18302
  function parseArgs(argv, onError) {
18264
18303
  const args = {
18265
18304
  target: ".",
@@ -18658,7 +18697,7 @@ async function runScanCommand(argv, io = {
18658
18697
  io.out(result.score === null ? "unknown" : String(result.score));
18659
18698
  return exitForFindings(result.findings, args.blocking === "none" ? "advisory" : args.blocking ?? scoreConfig.gate ?? "error");
18660
18699
  }
18661
- if (args.format === "sarif") io.out(renderSarif(result));
18700
+ if (args.format === "sarif") io.out(renderSarif(result, pathToFileURL(target).href));
18662
18701
  else if (args.format === "mermaid") io.out(renderMermaid(result));
18663
18702
  else if (args.json) io.out(JSON.stringify({
18664
18703
  ...result,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mjolnir-qa",
3
- "version": "0.5.24",
3
+ "version": "0.5.26",
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": {