mjolnir-qa 0.5.33 → 0.5.35
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.md +6 -0
- package/dist/cli.d.mts +11 -2
- package/dist/cli.mjs +124 -2
- package/dist/mcp/stdio.mjs +124 -2
- 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.35] — 2026-09-08
|
|
13
|
+
|
|
14
|
+
### Changes since 0
|
|
15
|
+
|
|
16
|
+
- P3a: GitLab CI — Code Quality report, MR recipe, exit-code discipline (#65)
|
|
17
|
+
|
|
18
|
+
## [0.5.34] — 2026-09-08
|
|
19
|
+
|
|
20
|
+
### Changes since 0
|
|
21
|
+
|
|
22
|
+
- p2: structural anti-dilution — deduction-mass ceilings close the padding vector (plan 1788853205786 P2, decision 4) (#64)
|
|
23
|
+
|
|
12
24
|
## [0.5.33] — 2026-09-08
|
|
13
25
|
|
|
14
26
|
### Changes since 0.5.32
|
package/README.md
CHANGED
|
@@ -203,6 +203,7 @@ pinned to the `v1` major tag) by default, or plain `npx` with
|
|
|
203
203
|
| `mjolnir forensics ./test-results/` | Runtime evidence from a real run |
|
|
204
204
|
| `mjolnir handoff` | Remediation plan for a coding agent |
|
|
205
205
|
| `mjolnir --json` / `--format sarif` | Machine-readable / GitHub Code Scanning |
|
|
206
|
+
| `mjolnir --format codequality` | GitLab Code Quality report (MR widget artifact) |
|
|
206
207
|
| `mjolnir --strict` | Also run quarantine-tier rules (higher FP risk) |
|
|
207
208
|
|
|
208
209
|
<details>
|
|
@@ -565,6 +566,10 @@ Or wire it into GitHub Code Scanning natively via SARIF:
|
|
|
565
566
|
sarif_file: mjolnir.sarif
|
|
566
567
|
```
|
|
567
568
|
|
|
569
|
+
On GitLab, `--format codequality` emits the Code Quality report the MR
|
|
570
|
+
widget and diff annotations consume
|
|
571
|
+
([docs/GITLAB-CI.md](docs/GITLAB-CI.md)).
|
|
572
|
+
|
|
568
573
|
Editor and pipeline setup: [docs/SARIF-INTEGRATION.md](docs/SARIF-INTEGRATION.md).
|
|
569
574
|
|
|
570
575
|
### Changed-scope attribution
|
|
@@ -718,6 +723,7 @@ Rule IDs (`QA-<FAMILY>-NNN`) are immutable once shipped and never reused.
|
|
|
718
723
|
| [docs/VERSIONING.md](docs/VERSIONING.md) | Semver policy, frozen surfaces, deprecation cycle |
|
|
719
724
|
| [docs/machine-contract.md](docs/machine-contract.md) | The canonical machine-readable result |
|
|
720
725
|
| [docs/SARIF-INTEGRATION.md](docs/SARIF-INTEGRATION.md) | SARIF output + editor/CI setup |
|
|
726
|
+
| [docs/GITLAB-CI.md](docs/GITLAB-CI.md) | GitLab: Code Quality report, MR recipe, gate |
|
|
721
727
|
| [docs/rules/](docs/rules/) | Generated per-rule catalog |
|
|
722
728
|
| [CONTRIBUTING.md](CONTRIBUTING.md) | Dev setup + contribution workflow |
|
|
723
729
|
| [SUPPORT.md](SUPPORT.md) | Where to ask, report and get help |
|
package/dist/cli.d.mts
CHANGED
|
@@ -201,6 +201,15 @@ interface ScanResult {
|
|
|
201
201
|
testDeclarationCount?: number;
|
|
202
202
|
/** Raw deduction total before normalization (Phase 5 — transparency). */
|
|
203
203
|
rawDeductions?: number;
|
|
204
|
+
/**
|
|
205
|
+
* The evidence-discounted deduction mass the P2 anti-dilution ceiling
|
|
206
|
+
* caps against (equals rawDeductions today — E0 charges 0, E1 halves;
|
|
207
|
+
* the ceiling input is deliberately the full discount surface).
|
|
208
|
+
* Additive within schemaVersion 1; present so consumers recompute the
|
|
209
|
+
* ceiling from docs/SCORING.md formula v2 without re-deriving
|
|
210
|
+
* evidence levels.
|
|
211
|
+
*/
|
|
212
|
+
effectiveDeductions?: number;
|
|
204
213
|
/** Number of findings suppressed by active config entries (suppression transparency). */
|
|
205
214
|
suppressionCount?: number;
|
|
206
215
|
/**
|
|
@@ -556,7 +565,7 @@ interface CliArgs {
|
|
|
556
565
|
verbose: boolean;
|
|
557
566
|
maxDurationMs: number;
|
|
558
567
|
scopeChanged: boolean;
|
|
559
|
-
format: "terminal" | "json" | "sarif" | "mermaid";
|
|
568
|
+
format: "terminal" | "json" | "sarif" | "mermaid" | "codequality";
|
|
560
569
|
/** --width override for terminal box/gauge wrapping (Sprint 5 Task 22). */
|
|
561
570
|
width?: number;
|
|
562
571
|
/** --ascii / --no-ascii override for shouldUseAscii()'s heuristic. */
|
|
@@ -728,7 +737,7 @@ declare const runScan: typeof runScan$1, buildUniversalRules: typeof buildUniver
|
|
|
728
737
|
* `scripts/sync-sarif-version.cjs` on release and guarded by
|
|
729
738
|
* `tests/version-consistency.spec.ts` locally.
|
|
730
739
|
*/
|
|
731
|
-
declare const CLI_VERSION = "0.5.
|
|
740
|
+
declare const CLI_VERSION = "0.5.35";
|
|
732
741
|
/** A usage-error detail: the offending token, when one exists. */
|
|
733
742
|
interface UsageErrorDetail {
|
|
734
743
|
/** The unknown flag or rejected value (e.g. `--nope`, `loud`). */
|
package/dist/cli.mjs
CHANGED
|
@@ -10074,6 +10074,58 @@ function computeDimensions(findings) {
|
|
|
10074
10074
|
for (const dim of byCategory.values()) dim.score = Math.max(0, 100 - deductions.get(dim.category));
|
|
10075
10075
|
return [...byCategory.values()].sort((a, b) => a.category.localeCompare(b.category));
|
|
10076
10076
|
}
|
|
10077
|
+
/**
|
|
10078
|
+
* Deduction-mass ceilings (product-gap-remediation master plan P2,
|
|
10079
|
+
* plan 1788853205786 — structural anti-dilution, decision 4).
|
|
10080
|
+
*
|
|
10081
|
+
* The Goodhart vector the density formula permits: fixed deduction mass
|
|
10082
|
+
* ÷ padded denominator → score → 99. A repo with 80 warning-points of
|
|
10083
|
+
* real findings and 10,000 test declarations reads 99+ ("one minor
|
|
10084
|
+
* issue") because the RATE is tiny — the padding is free. Density stays
|
|
10085
|
+
* as the legitimate differentiator for small masses (a lone warning in
|
|
10086
|
+
* a 10k suite must still read ≥ 95), but it can no longer dilute a
|
|
10087
|
+
* large mass: each absolute deduction-mass band caps the score. Padding
|
|
10088
|
+
* cannot move a ceiling — the vector is structurally closed. 80
|
|
10089
|
+
* warning-pts read ≤ 75 in ANY suite size.
|
|
10090
|
+
*
|
|
10091
|
+
* The error-severity floor (95) is the ≥ 8 band seen from the other
|
|
10092
|
+
* side: 1 error ≥ 8 pts, so it is subsumed and left standing as a named
|
|
10093
|
+
* special case for prose continuity. The > 0 band is the honesty guard
|
|
10094
|
+
* (clampWithFindings) — subsumed as well.
|
|
10095
|
+
*
|
|
10096
|
+
* Calibration (P2.4): these boundaries were chosen to hold the three
|
|
10097
|
+
* known data points (self 100, golden 49 via suiteVoided, demo 67) and
|
|
10098
|
+
* are then FROZEN with a changelog entry. Never tune them to flatter a
|
|
10099
|
+
* repo (docs/SCORING.md Correction section is the precedent).
|
|
10100
|
+
*/
|
|
10101
|
+
const DEDUCTION_MASS_CEILINGS = [
|
|
10102
|
+
{
|
|
10103
|
+
minMass: 160,
|
|
10104
|
+
ceiling: 65
|
|
10105
|
+
},
|
|
10106
|
+
{
|
|
10107
|
+
minMass: 80,
|
|
10108
|
+
ceiling: 75
|
|
10109
|
+
},
|
|
10110
|
+
{
|
|
10111
|
+
minMass: 40,
|
|
10112
|
+
ceiling: 85
|
|
10113
|
+
},
|
|
10114
|
+
{
|
|
10115
|
+
minMass: 8,
|
|
10116
|
+
ceiling: 95
|
|
10117
|
+
}
|
|
10118
|
+
];
|
|
10119
|
+
/**
|
|
10120
|
+
* The score ceiling for a given evidence-discounted deduction mass.
|
|
10121
|
+
* 0 → no ceiling (the 100 honesty guard lives in clampWithFindings);
|
|
10122
|
+
* below the smallest band → no ceiling beyond the named guards.
|
|
10123
|
+
* Exported for the JSON contract (effectiveDeductions) and the tests.
|
|
10124
|
+
*/
|
|
10125
|
+
function massCeiling(totalDeduction) {
|
|
10126
|
+
for (const band of DEDUCTION_MASS_CEILINGS) if (totalDeduction >= band.minMass) return band.ceiling;
|
|
10127
|
+
return null;
|
|
10128
|
+
}
|
|
10077
10129
|
function computeTotal(dimensions, findings, exposure) {
|
|
10078
10130
|
if (findings.length === 0) return 100;
|
|
10079
10131
|
let totalDeduction = 0;
|
|
@@ -10086,6 +10138,8 @@ function computeTotal(dimensions, findings, exposure) {
|
|
|
10086
10138
|
const rate = totalDeduction / (declarations + 1);
|
|
10087
10139
|
score = clampWithFindings(100 - Math.min(100, rate * 5), totalDeduction);
|
|
10088
10140
|
} else score = clampWithFindings(100 - totalDeduction, totalDeduction);
|
|
10141
|
+
const ceiling = massCeiling(totalDeduction);
|
|
10142
|
+
if (ceiling !== null && score > ceiling) score = ceiling;
|
|
10089
10143
|
if (findings.some((f) => f.severity === "error" && deductionFor(f) > 0) && score > 95) score = 95;
|
|
10090
10144
|
return suiteVoided ? Math.min(score, 49) : score;
|
|
10091
10145
|
}
|
|
@@ -12322,6 +12376,7 @@ async function runScan$1(args, hooks = {}) {
|
|
|
12322
12376
|
});
|
|
12323
12377
|
const dimensions = computeDimensions(findings);
|
|
12324
12378
|
const rawDeductions = findings.reduce((sum, f) => sum + deductionFor(f), 0);
|
|
12379
|
+
const effectiveDeductions = rawDeductions;
|
|
12325
12380
|
const total = computeTotal(dimensions, findings, {
|
|
12326
12381
|
testDeclarations: testDeclarationCount,
|
|
12327
12382
|
testFileCount,
|
|
@@ -12346,6 +12401,7 @@ async function runScan$1(args, hooks = {}) {
|
|
|
12346
12401
|
testFileCount,
|
|
12347
12402
|
testDeclarationCount,
|
|
12348
12403
|
rawDeductions,
|
|
12404
|
+
effectiveDeductions,
|
|
12349
12405
|
suppressionCount,
|
|
12350
12406
|
...pluginsLoaded.length > 0 ? { plugins: pluginsLoaded } : {},
|
|
12351
12407
|
agenticProfile: computeAgenticProfile(fileProvenance, findings),
|
|
@@ -12900,6 +12956,10 @@ function appendScoreSection(lines, result, p, width, ascii) {
|
|
|
12900
12956
|
lines.push(` ${scoreGauge(result.score, p, gaugeWidth, ascii)}`);
|
|
12901
12957
|
lines.push(` ${p.dim(headlineFor(state, result.findings.length))}`);
|
|
12902
12958
|
if (result.rawDeductions !== void 0 && result.testDeclarationCount) lines.push(` ${p.dim(`(${result.rawDeductions} raw pts / ${result.testDeclarationCount} test declarations — normalized)`)}`);
|
|
12959
|
+
if (result.effectiveDeductions !== void 0 && result.score !== null) {
|
|
12960
|
+
const ceiling = massCeiling(result.effectiveDeductions);
|
|
12961
|
+
if (ceiling !== null && result.score <= ceiling && result.rawDeductions !== void 0) lines.push(` ${p.dim(`(capped: deduction mass ${result.effectiveDeductions} pts — absolute ceiling ${ceiling})`)}`);
|
|
12962
|
+
}
|
|
12903
12963
|
if (result.suppressionCount && result.suppressionCount > 0) lines.push(` ${p.dim(`(${result.suppressionCount} finding(s) suppressed by config)`)}`);
|
|
12904
12964
|
lines.push("");
|
|
12905
12965
|
}
|
|
@@ -13290,7 +13350,7 @@ function renderSarif(result, repoRootUri) {
|
|
|
13290
13350
|
tool: { driver: {
|
|
13291
13351
|
name: "Mjölnir",
|
|
13292
13352
|
informationUri: "https://github.com/Sergey-Bar/Mjolnir",
|
|
13293
|
-
version: "0.5.
|
|
13353
|
+
version: "0.5.35",
|
|
13294
13354
|
rules: [...rules.values()].map((r) => {
|
|
13295
13355
|
const meta = RULES.find((x) => x.id === r.id);
|
|
13296
13356
|
return {
|
|
@@ -13362,6 +13422,62 @@ function renderSarif(result, repoRootUri) {
|
|
|
13362
13422
|
return JSON.stringify(sarif, null, 2);
|
|
13363
13423
|
}
|
|
13364
13424
|
//#endregion
|
|
13425
|
+
//#region src/reporter/codequality.ts
|
|
13426
|
+
/**
|
|
13427
|
+
* GitLab Code Quality reporter (product-gap-remediation master plan P3a,
|
|
13428
|
+
* plan 1788853205786 — flag 2, decision 2: GitLab is the in-plan second
|
|
13429
|
+
* platform).
|
|
13430
|
+
*
|
|
13431
|
+
* Emits the Code Quality report schema GitLab's `codequality` artifact
|
|
13432
|
+
* type consumes (docs.gitlab.com/ee/ci/testing/code_quality.html) so
|
|
13433
|
+
* findings render as MR widgets and diff annotations inside existing
|
|
13434
|
+
* governance — the same distribution logic as SARIF for GitHub, never a
|
|
13435
|
+
* second truth: this is a deterministic projection of the same
|
|
13436
|
+
* ScanResult the JSON contract carries.
|
|
13437
|
+
*
|
|
13438
|
+
* Usage: mjolnir --format codequality > gl-code-quality-report.json
|
|
13439
|
+
*
|
|
13440
|
+
* Severity map (documented in docs/GITLAB-CI.md):
|
|
13441
|
+
* error → major (categorical defects — the gate's core)
|
|
13442
|
+
* warning → minor
|
|
13443
|
+
* info → info
|
|
13444
|
+
*
|
|
13445
|
+
* Fingerprint stability: GitLab deduplicates MR findings across runs by
|
|
13446
|
+
* `fingerprint` — an unstable fingerprint would resurrect closed
|
|
13447
|
+
* findings on every push (the exact PR-noise the MR flow exists to
|
|
13448
|
+
* avoid). The fingerprint is sha256 over ruleId + repo-relative path +
|
|
13449
|
+
* line + column + message — the same finding shape the --why command
|
|
13450
|
+
* addresses, so a moved line legitimately re-reports and an unchanged
|
|
13451
|
+
* finding stays silent.
|
|
13452
|
+
*/
|
|
13453
|
+
function codeQualitySeverity(severity) {
|
|
13454
|
+
if (severity === "error") return "major";
|
|
13455
|
+
if (severity === "warning") return "minor";
|
|
13456
|
+
return "info";
|
|
13457
|
+
}
|
|
13458
|
+
/**
|
|
13459
|
+
* sha256 hex over the identifying tuple. Exported for the contract test:
|
|
13460
|
+
* the fingerprint MUST be stable across runs for an unchanged finding
|
|
13461
|
+
* (GitLab dedup contract) and MUST change when the finding's identity
|
|
13462
|
+
* changes (otherwise an MR could hide a moved-and-still-broken line).
|
|
13463
|
+
*/
|
|
13464
|
+
function codeQualityFingerprint(f) {
|
|
13465
|
+
return createHash("sha256").update(`${f.ruleId}\u0000${f.file}\u0000${f.line}\u0000${f.column}\u0000${f.message}`).digest("hex");
|
|
13466
|
+
}
|
|
13467
|
+
function renderCodeQuality(result) {
|
|
13468
|
+
const issues = result.findings.map((f) => ({
|
|
13469
|
+
description: f.message,
|
|
13470
|
+
check_name: f.ruleId,
|
|
13471
|
+
fingerprint: codeQualityFingerprint(f),
|
|
13472
|
+
severity: codeQualitySeverity(f.severity),
|
|
13473
|
+
location: {
|
|
13474
|
+
path: f.file.replaceAll("\\", "/"),
|
|
13475
|
+
lines: { begin: f.line }
|
|
13476
|
+
}
|
|
13477
|
+
}));
|
|
13478
|
+
return JSON.stringify(issues, null, 2) + "\n";
|
|
13479
|
+
}
|
|
13480
|
+
//#endregion
|
|
13365
13481
|
//#region src/reporter/mermaid.ts
|
|
13366
13482
|
function sanitizeId(raw) {
|
|
13367
13483
|
return raw.replace(/[^a-z0-9]/gi, "_");
|
|
@@ -16324,6 +16440,10 @@ const HELP_FLAGS = [
|
|
|
16324
16440
|
flag: "--format sarif",
|
|
16325
16441
|
summary: "SARIF 2.1 for GitHub Code Scanning"
|
|
16326
16442
|
},
|
|
16443
|
+
{
|
|
16444
|
+
flag: "--format codequality",
|
|
16445
|
+
summary: "GitLab Code Quality report (MR widget artifact)"
|
|
16446
|
+
},
|
|
16327
16447
|
{
|
|
16328
16448
|
flag: "--format mermaid",
|
|
16329
16449
|
summary: "test-architecture diagram"
|
|
@@ -18788,7 +18908,7 @@ const { runScan, buildUniversalRules, fallbackWorkspace, pathMatchesGlob, isVali
|
|
|
18788
18908
|
* `scripts/sync-sarif-version.cjs` on release and guarded by
|
|
18789
18909
|
* `tests/version-consistency.spec.ts` locally.
|
|
18790
18910
|
*/
|
|
18791
|
-
const CLI_VERSION = "0.5.
|
|
18911
|
+
const CLI_VERSION = "0.5.35";
|
|
18792
18912
|
function parseArgs(argv, onError) {
|
|
18793
18913
|
const args = {
|
|
18794
18914
|
target: ".",
|
|
@@ -18811,6 +18931,7 @@ function parseArgs(argv, onError) {
|
|
|
18811
18931
|
const fmt = argv[++i];
|
|
18812
18932
|
if (fmt === "sarif") args.format = "sarif";
|
|
18813
18933
|
else if (fmt === "mermaid") args.format = "mermaid";
|
|
18934
|
+
else if (fmt === "codequality") args.format = "codequality";
|
|
18814
18935
|
else if (fmt === "json") {
|
|
18815
18936
|
args.format = "json";
|
|
18816
18937
|
args.json = true;
|
|
@@ -19195,6 +19316,7 @@ async function runScanCommand(argv, io = {
|
|
|
19195
19316
|
}
|
|
19196
19317
|
if (args.format === "sarif") io.out(renderSarif(result, pathToFileURL(target).href));
|
|
19197
19318
|
else if (args.format === "mermaid") io.out(renderMermaid(result));
|
|
19319
|
+
else if (args.format === "codequality") io.out(renderCodeQuality(result));
|
|
19198
19320
|
else if (args.json) io.out(JSON.stringify({
|
|
19199
19321
|
...result,
|
|
19200
19322
|
contract: buildMachineContract(result)
|
package/dist/mcp/stdio.mjs
CHANGED
|
@@ -10073,6 +10073,58 @@ function computeDimensions(findings) {
|
|
|
10073
10073
|
for (const dim of byCategory.values()) dim.score = Math.max(0, 100 - deductions.get(dim.category));
|
|
10074
10074
|
return [...byCategory.values()].sort((a, b) => a.category.localeCompare(b.category));
|
|
10075
10075
|
}
|
|
10076
|
+
/**
|
|
10077
|
+
* Deduction-mass ceilings (product-gap-remediation master plan P2,
|
|
10078
|
+
* plan 1788853205786 — structural anti-dilution, decision 4).
|
|
10079
|
+
*
|
|
10080
|
+
* The Goodhart vector the density formula permits: fixed deduction mass
|
|
10081
|
+
* ÷ padded denominator → score → 99. A repo with 80 warning-points of
|
|
10082
|
+
* real findings and 10,000 test declarations reads 99+ ("one minor
|
|
10083
|
+
* issue") because the RATE is tiny — the padding is free. Density stays
|
|
10084
|
+
* as the legitimate differentiator for small masses (a lone warning in
|
|
10085
|
+
* a 10k suite must still read ≥ 95), but it can no longer dilute a
|
|
10086
|
+
* large mass: each absolute deduction-mass band caps the score. Padding
|
|
10087
|
+
* cannot move a ceiling — the vector is structurally closed. 80
|
|
10088
|
+
* warning-pts read ≤ 75 in ANY suite size.
|
|
10089
|
+
*
|
|
10090
|
+
* The error-severity floor (95) is the ≥ 8 band seen from the other
|
|
10091
|
+
* side: 1 error ≥ 8 pts, so it is subsumed and left standing as a named
|
|
10092
|
+
* special case for prose continuity. The > 0 band is the honesty guard
|
|
10093
|
+
* (clampWithFindings) — subsumed as well.
|
|
10094
|
+
*
|
|
10095
|
+
* Calibration (P2.4): these boundaries were chosen to hold the three
|
|
10096
|
+
* known data points (self 100, golden 49 via suiteVoided, demo 67) and
|
|
10097
|
+
* are then FROZEN with a changelog entry. Never tune them to flatter a
|
|
10098
|
+
* repo (docs/SCORING.md Correction section is the precedent).
|
|
10099
|
+
*/
|
|
10100
|
+
const DEDUCTION_MASS_CEILINGS = [
|
|
10101
|
+
{
|
|
10102
|
+
minMass: 160,
|
|
10103
|
+
ceiling: 65
|
|
10104
|
+
},
|
|
10105
|
+
{
|
|
10106
|
+
minMass: 80,
|
|
10107
|
+
ceiling: 75
|
|
10108
|
+
},
|
|
10109
|
+
{
|
|
10110
|
+
minMass: 40,
|
|
10111
|
+
ceiling: 85
|
|
10112
|
+
},
|
|
10113
|
+
{
|
|
10114
|
+
minMass: 8,
|
|
10115
|
+
ceiling: 95
|
|
10116
|
+
}
|
|
10117
|
+
];
|
|
10118
|
+
/**
|
|
10119
|
+
* The score ceiling for a given evidence-discounted deduction mass.
|
|
10120
|
+
* 0 → no ceiling (the 100 honesty guard lives in clampWithFindings);
|
|
10121
|
+
* below the smallest band → no ceiling beyond the named guards.
|
|
10122
|
+
* Exported for the JSON contract (effectiveDeductions) and the tests.
|
|
10123
|
+
*/
|
|
10124
|
+
function massCeiling(totalDeduction) {
|
|
10125
|
+
for (const band of DEDUCTION_MASS_CEILINGS) if (totalDeduction >= band.minMass) return band.ceiling;
|
|
10126
|
+
return null;
|
|
10127
|
+
}
|
|
10076
10128
|
function computeTotal(dimensions, findings, exposure) {
|
|
10077
10129
|
if (findings.length === 0) return 100;
|
|
10078
10130
|
let totalDeduction = 0;
|
|
@@ -10085,6 +10137,8 @@ function computeTotal(dimensions, findings, exposure) {
|
|
|
10085
10137
|
const rate = totalDeduction / (declarations + 1);
|
|
10086
10138
|
score = clampWithFindings(100 - Math.min(100, rate * 5), totalDeduction);
|
|
10087
10139
|
} else score = clampWithFindings(100 - totalDeduction, totalDeduction);
|
|
10140
|
+
const ceiling = massCeiling(totalDeduction);
|
|
10141
|
+
if (ceiling !== null && score > ceiling) score = ceiling;
|
|
10088
10142
|
if (findings.some((f) => f.severity === "error" && deductionFor(f) > 0) && score > 95) score = 95;
|
|
10089
10143
|
return suiteVoided ? Math.min(score, 49) : score;
|
|
10090
10144
|
}
|
|
@@ -12321,6 +12375,7 @@ async function runScan$1(args, hooks = {}) {
|
|
|
12321
12375
|
});
|
|
12322
12376
|
const dimensions = computeDimensions(findings);
|
|
12323
12377
|
const rawDeductions = findings.reduce((sum, f) => sum + deductionFor(f), 0);
|
|
12378
|
+
const effectiveDeductions = rawDeductions;
|
|
12324
12379
|
const total = computeTotal(dimensions, findings, {
|
|
12325
12380
|
testDeclarations: testDeclarationCount,
|
|
12326
12381
|
testFileCount,
|
|
@@ -12345,6 +12400,7 @@ async function runScan$1(args, hooks = {}) {
|
|
|
12345
12400
|
testFileCount,
|
|
12346
12401
|
testDeclarationCount,
|
|
12347
12402
|
rawDeductions,
|
|
12403
|
+
effectiveDeductions,
|
|
12348
12404
|
suppressionCount,
|
|
12349
12405
|
...pluginsLoaded.length > 0 ? { plugins: pluginsLoaded } : {},
|
|
12350
12406
|
agenticProfile: computeAgenticProfile(fileProvenance, findings),
|
|
@@ -12899,6 +12955,10 @@ function appendScoreSection(lines, result, p, width, ascii) {
|
|
|
12899
12955
|
lines.push(` ${scoreGauge(result.score, p, gaugeWidth, ascii)}`);
|
|
12900
12956
|
lines.push(` ${p.dim(headlineFor(state, result.findings.length))}`);
|
|
12901
12957
|
if (result.rawDeductions !== void 0 && result.testDeclarationCount) lines.push(` ${p.dim(`(${result.rawDeductions} raw pts / ${result.testDeclarationCount} test declarations — normalized)`)}`);
|
|
12958
|
+
if (result.effectiveDeductions !== void 0 && result.score !== null) {
|
|
12959
|
+
const ceiling = massCeiling(result.effectiveDeductions);
|
|
12960
|
+
if (ceiling !== null && result.score <= ceiling && result.rawDeductions !== void 0) lines.push(` ${p.dim(`(capped: deduction mass ${result.effectiveDeductions} pts — absolute ceiling ${ceiling})`)}`);
|
|
12961
|
+
}
|
|
12902
12962
|
if (result.suppressionCount && result.suppressionCount > 0) lines.push(` ${p.dim(`(${result.suppressionCount} finding(s) suppressed by config)`)}`);
|
|
12903
12963
|
lines.push("");
|
|
12904
12964
|
}
|
|
@@ -13289,7 +13349,7 @@ function renderSarif(result, repoRootUri) {
|
|
|
13289
13349
|
tool: { driver: {
|
|
13290
13350
|
name: "Mjölnir",
|
|
13291
13351
|
informationUri: "https://github.com/Sergey-Bar/Mjolnir",
|
|
13292
|
-
version: "0.5.
|
|
13352
|
+
version: "0.5.35",
|
|
13293
13353
|
rules: [...rules.values()].map((r) => {
|
|
13294
13354
|
const meta = RULES.find((x) => x.id === r.id);
|
|
13295
13355
|
return {
|
|
@@ -13361,6 +13421,62 @@ function renderSarif(result, repoRootUri) {
|
|
|
13361
13421
|
return JSON.stringify(sarif, null, 2);
|
|
13362
13422
|
}
|
|
13363
13423
|
//#endregion
|
|
13424
|
+
//#region src/reporter/codequality.ts
|
|
13425
|
+
/**
|
|
13426
|
+
* GitLab Code Quality reporter (product-gap-remediation master plan P3a,
|
|
13427
|
+
* plan 1788853205786 — flag 2, decision 2: GitLab is the in-plan second
|
|
13428
|
+
* platform).
|
|
13429
|
+
*
|
|
13430
|
+
* Emits the Code Quality report schema GitLab's `codequality` artifact
|
|
13431
|
+
* type consumes (docs.gitlab.com/ee/ci/testing/code_quality.html) so
|
|
13432
|
+
* findings render as MR widgets and diff annotations inside existing
|
|
13433
|
+
* governance — the same distribution logic as SARIF for GitHub, never a
|
|
13434
|
+
* second truth: this is a deterministic projection of the same
|
|
13435
|
+
* ScanResult the JSON contract carries.
|
|
13436
|
+
*
|
|
13437
|
+
* Usage: mjolnir --format codequality > gl-code-quality-report.json
|
|
13438
|
+
*
|
|
13439
|
+
* Severity map (documented in docs/GITLAB-CI.md):
|
|
13440
|
+
* error → major (categorical defects — the gate's core)
|
|
13441
|
+
* warning → minor
|
|
13442
|
+
* info → info
|
|
13443
|
+
*
|
|
13444
|
+
* Fingerprint stability: GitLab deduplicates MR findings across runs by
|
|
13445
|
+
* `fingerprint` — an unstable fingerprint would resurrect closed
|
|
13446
|
+
* findings on every push (the exact PR-noise the MR flow exists to
|
|
13447
|
+
* avoid). The fingerprint is sha256 over ruleId + repo-relative path +
|
|
13448
|
+
* line + column + message — the same finding shape the --why command
|
|
13449
|
+
* addresses, so a moved line legitimately re-reports and an unchanged
|
|
13450
|
+
* finding stays silent.
|
|
13451
|
+
*/
|
|
13452
|
+
function codeQualitySeverity(severity) {
|
|
13453
|
+
if (severity === "error") return "major";
|
|
13454
|
+
if (severity === "warning") return "minor";
|
|
13455
|
+
return "info";
|
|
13456
|
+
}
|
|
13457
|
+
/**
|
|
13458
|
+
* sha256 hex over the identifying tuple. Exported for the contract test:
|
|
13459
|
+
* the fingerprint MUST be stable across runs for an unchanged finding
|
|
13460
|
+
* (GitLab dedup contract) and MUST change when the finding's identity
|
|
13461
|
+
* changes (otherwise an MR could hide a moved-and-still-broken line).
|
|
13462
|
+
*/
|
|
13463
|
+
function codeQualityFingerprint(f) {
|
|
13464
|
+
return createHash("sha256").update(`${f.ruleId}\u0000${f.file}\u0000${f.line}\u0000${f.column}\u0000${f.message}`).digest("hex");
|
|
13465
|
+
}
|
|
13466
|
+
function renderCodeQuality(result) {
|
|
13467
|
+
const issues = result.findings.map((f) => ({
|
|
13468
|
+
description: f.message,
|
|
13469
|
+
check_name: f.ruleId,
|
|
13470
|
+
fingerprint: codeQualityFingerprint(f),
|
|
13471
|
+
severity: codeQualitySeverity(f.severity),
|
|
13472
|
+
location: {
|
|
13473
|
+
path: f.file.replaceAll("\\", "/"),
|
|
13474
|
+
lines: { begin: f.line }
|
|
13475
|
+
}
|
|
13476
|
+
}));
|
|
13477
|
+
return JSON.stringify(issues, null, 2) + "\n";
|
|
13478
|
+
}
|
|
13479
|
+
//#endregion
|
|
13364
13480
|
//#region src/reporter/mermaid.ts
|
|
13365
13481
|
function sanitizeId(raw) {
|
|
13366
13482
|
return raw.replace(/[^a-z0-9]/gi, "_");
|
|
@@ -15515,6 +15631,10 @@ const HELP_FLAGS = [
|
|
|
15515
15631
|
flag: "--format sarif",
|
|
15516
15632
|
summary: "SARIF 2.1 for GitHub Code Scanning"
|
|
15517
15633
|
},
|
|
15634
|
+
{
|
|
15635
|
+
flag: "--format codequality",
|
|
15636
|
+
summary: "GitLab Code Quality report (MR widget artifact)"
|
|
15637
|
+
},
|
|
15518
15638
|
{
|
|
15519
15639
|
flag: "--format mermaid",
|
|
15520
15640
|
summary: "test-architecture diagram"
|
|
@@ -18424,7 +18544,7 @@ const { runScan, buildUniversalRules, fallbackWorkspace, pathMatchesGlob, isVali
|
|
|
18424
18544
|
* `scripts/sync-sarif-version.cjs` on release and guarded by
|
|
18425
18545
|
* `tests/version-consistency.spec.ts` locally.
|
|
18426
18546
|
*/
|
|
18427
|
-
const CLI_VERSION = "0.5.
|
|
18547
|
+
const CLI_VERSION = "0.5.35";
|
|
18428
18548
|
function parseArgs(argv, onError) {
|
|
18429
18549
|
const args = {
|
|
18430
18550
|
target: ".",
|
|
@@ -18447,6 +18567,7 @@ function parseArgs(argv, onError) {
|
|
|
18447
18567
|
const fmt = argv[++i];
|
|
18448
18568
|
if (fmt === "sarif") args.format = "sarif";
|
|
18449
18569
|
else if (fmt === "mermaid") args.format = "mermaid";
|
|
18570
|
+
else if (fmt === "codequality") args.format = "codequality";
|
|
18450
18571
|
else if (fmt === "json") {
|
|
18451
18572
|
args.format = "json";
|
|
18452
18573
|
args.json = true;
|
|
@@ -18831,6 +18952,7 @@ async function runScanCommand(argv, io = {
|
|
|
18831
18952
|
}
|
|
18832
18953
|
if (args.format === "sarif") io.out(renderSarif(result, pathToFileURL(target).href));
|
|
18833
18954
|
else if (args.format === "mermaid") io.out(renderMermaid(result));
|
|
18955
|
+
else if (args.format === "codequality") io.out(renderCodeQuality(result));
|
|
18834
18956
|
else if (args.json) io.out(JSON.stringify({
|
|
18835
18957
|
...result,
|
|
18836
18958
|
contract: buildMachineContract(result)
|
package/package.json
CHANGED