mjolnir-qa 0.5.25 → 0.5.27
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 +12 -0
- package/README.ar.md +1 -1
- package/README.bn.md +1 -1
- package/README.br.md +1 -1
- package/README.bs.md +1 -1
- package/README.da.md +1 -1
- package/README.de.md +1 -1
- package/README.es.md +1 -1
- package/README.fr.md +1 -1
- package/README.gr.md +1 -1
- package/README.he.md +1 -1
- package/README.it.md +1 -1
- package/README.ja.md +1 -1
- package/README.ko.md +1 -1
- package/README.md +69 -65
- package/README.no.md +1 -1
- package/README.pl.md +1 -1
- package/README.ru.md +1 -1
- package/README.th.md +1 -1
- package/README.tr.md +1 -1
- package/README.uk.md +1 -1
- package/README.vi.md +1 -1
- package/README.zh.md +1 -1
- package/README.zht.md +1 -1
- package/dist/cli.d.mts +1 -1
- package/dist/cli.mjs +46 -7
- package/dist/mcp/stdio.mjs +46 -7
- package/package.json +1 -1
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.27] — 2026-09-08
|
|
13
|
+
|
|
14
|
+
### Changes since 0.5.26
|
|
15
|
+
|
|
16
|
+
- fix(corpus-sample): refuse PARTIAL scans — findings from a truncated scan are not evidence (same refusal as corpus/audit.ts, D14/§19 discipline)
|
|
17
|
+
|
|
18
|
+
## [0.5.26] — 2026-09-08
|
|
19
|
+
|
|
20
|
+
### Changes since 0.5.25
|
|
21
|
+
|
|
22
|
+
- Certification findings remediation: F1-F5 + P3 (plan 1788806598818) (#57)
|
|
23
|
+
|
|
12
24
|
## [0.5.25] — 2026-09-08
|
|
13
25
|
|
|
14
26
|
### Changes since 0.5.24
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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
|
|
220
|
-
|
|
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
|
|
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
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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.
|
|
731
|
+
declare const CLI_VERSION = "0.5.27";
|
|
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.
|
|
13293
|
+
version: "0.5.27",
|
|
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.
|
|
18665
|
+
const CLI_VERSION = "0.5.27";
|
|
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,
|
package/dist/mcp/stdio.mjs
CHANGED
|
@@ -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.
|
|
13292
|
+
version: "0.5.27",
|
|
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.
|
|
18301
|
+
const CLI_VERSION = "0.5.27";
|
|
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