mjolnir-qa 0.5.11 → 0.5.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +32 -0
- package/README.ar.md +21 -15
- package/README.bn.md +17 -9
- package/README.br.md +25 -16
- package/README.bs.md +23 -15
- package/README.da.md +23 -16
- package/README.de.md +24 -16
- package/README.es.md +25 -16
- package/README.fr.md +25 -16
- package/README.gr.md +24 -16
- package/README.he.md +22 -15
- package/README.it.md +25 -16
- package/README.ja.md +16 -9
- package/README.ko.md +23 -15
- package/README.md +124 -225
- package/README.no.md +23 -16
- package/README.pl.md +17 -9
- package/README.ru.md +26 -17
- package/README.th.md +16 -9
- package/README.tr.md +23 -16
- package/README.uk.md +24 -16
- package/README.vi.md +22 -15
- package/README.zh.md +21 -15
- package/README.zht.md +21 -15
- package/dist/cli.d.mts +1 -1
- package/dist/cli.mjs +84 -40
- package/dist/mcp/stdio.mjs +84 -40
- package/package.json +6 -1
package/dist/cli.mjs
CHANGED
|
@@ -12480,26 +12480,26 @@ function topFixes(findings, n = 3) {
|
|
|
12480
12480
|
}
|
|
12481
12481
|
//#endregion
|
|
12482
12482
|
//#region src/reporter/art.ts
|
|
12483
|
-
/**
|
|
12483
|
+
/**
|
|
12484
|
+
* The report's wordmark. A LOCKUP, not a picture of a hammer.
|
|
12485
|
+
*
|
|
12486
|
+
* This used to be eight rows of box-drawing that spelled out a hammer —
|
|
12487
|
+
* directly above the score instrument, which is also a hammer. Two
|
|
12488
|
+
* hammers, ten lines apart, and the smaller static one came first, so
|
|
12489
|
+
* appendScoreSection's own claim that "the hammer is the instrument: the
|
|
12490
|
+
* first thing the eye lands on" was false in every report the tool has
|
|
12491
|
+
* ever printed.
|
|
12492
|
+
*
|
|
12493
|
+
* There is one hammer now, and it is the one that means something: the
|
|
12494
|
+
* state-resolved instrument in HAMMER_STATES. The wordmark stays out of
|
|
12495
|
+
* its way.
|
|
12496
|
+
*/
|
|
12484
12497
|
const LOGO = `
|
|
12485
|
-
|
|
12486
|
-
║ ║
|
|
12487
|
-
╠═══════════╣ M J Ö L N I R
|
|
12488
|
-
║ ║ ║
|
|
12489
|
-
╚═════╩═════╝ VERIFICATION TRUST ENGINE
|
|
12490
|
-
║
|
|
12491
|
-
║
|
|
12498
|
+
M J Ö L N I R · VERIFICATION TRUST ENGINE
|
|
12492
12499
|
`;
|
|
12493
|
-
/** Plain-ASCII fallback
|
|
12494
|
-
* block-drawing LOGO above renders as mangled "?" glyphs. */
|
|
12500
|
+
/** Plain-ASCII fallback for cmd.exe/legacy consoles. */
|
|
12495
12501
|
const LOGO_ASCII = `
|
|
12496
|
-
|
|
12497
|
-
| |
|
|
12498
|
-
+-----------+ M J O L N I R
|
|
12499
|
-
| | |
|
|
12500
|
-
+-----+-----+ VERIFICATION TRUST ENGINE
|
|
12501
|
-
|
|
|
12502
|
-
|
|
|
12502
|
+
M J O L N I R - VERIFICATION TRUST ENGINE
|
|
12503
12503
|
`;
|
|
12504
12504
|
const TROPHY = String.raw`
|
|
12505
12505
|
___________
|
|
@@ -13000,10 +13000,15 @@ const CARD_GUTTER = " ";
|
|
|
13000
13000
|
function pushCard(lines, card, ui) {
|
|
13001
13001
|
const { p, width } = ui;
|
|
13002
13002
|
const contentWidth = Math.max(20, width - 2 - 4 - CARD_LABEL_PAD);
|
|
13003
|
-
|
|
13003
|
+
const header = ` ${severityIcon(card.severity, ui)} ${p.bold(card.loc)}`;
|
|
13004
|
+
if (measure(`${header} ${card.evidence}`) <= width) lines.push(`${header} ${p.dim(card.evidence)}`);
|
|
13005
|
+
else {
|
|
13006
|
+
lines.push(header);
|
|
13007
|
+
lines.push(`${CARD_GUTTER}${p.dim(card.evidence)}`);
|
|
13008
|
+
}
|
|
13004
13009
|
const fields = [
|
|
13005
13010
|
{
|
|
13006
|
-
label: "
|
|
13011
|
+
label: "Finding",
|
|
13007
13012
|
text: card.problem,
|
|
13008
13013
|
dim: false
|
|
13009
13014
|
},
|
|
@@ -13034,13 +13039,13 @@ function pushCard(lines, card, ui) {
|
|
|
13034
13039
|
}
|
|
13035
13040
|
/**
|
|
13036
13041
|
* The findings experience — EVIDENCE layer. Cards carry
|
|
13037
|
-
* severity →
|
|
13042
|
+
* severity → Finding → Impact → Fix → Verify; the evidence tag sits
|
|
13038
13043
|
* beside the title. >3 findings sharing a rule collapse under one
|
|
13039
13044
|
* "same fix applies" header. Non-verbose shows MAX_CARDS cards plus an
|
|
13040
13045
|
* overflow line; --verbose shows everything.
|
|
13041
13046
|
*/
|
|
13042
13047
|
function appendFindings(lines, result, counts, verbose, ui, tone) {
|
|
13043
|
-
const { p } = ui;
|
|
13048
|
+
const { p, width } = ui;
|
|
13044
13049
|
if (counts.total === 0) return;
|
|
13045
13050
|
if (result.findings.length === 0) {
|
|
13046
13051
|
lines.push(ui.p.dim(" filtered view: no findings in the selected category"));
|
|
@@ -13089,9 +13094,21 @@ function appendFindings(lines, result, counts, verbose, ui, tone) {
|
|
|
13089
13094
|
hiddenRules.add(unit.ruleId);
|
|
13090
13095
|
continue;
|
|
13091
13096
|
}
|
|
13092
|
-
|
|
13093
|
-
|
|
13094
|
-
|
|
13097
|
+
const groupHead = ` ${severityIcon(maxSeverity(unit.findings), ui)} ${p.bold(sanitizeData(unit.ruleId))} ${p.dim(`× ${n} — same fix applies`)}`;
|
|
13098
|
+
const groupEvidence = evidenceTag$1(first);
|
|
13099
|
+
if (measure(`${groupHead} ${groupEvidence}`) <= width) lines.push(`${groupHead} ${p.dim(groupEvidence)}`);
|
|
13100
|
+
else {
|
|
13101
|
+
lines.push(groupHead);
|
|
13102
|
+
lines.push(`${CARD_GUTTER}${p.dim(groupEvidence)}`);
|
|
13103
|
+
}
|
|
13104
|
+
const groupContentWidth = Math.max(20, width - 2 - 4 - CARD_LABEL_PAD);
|
|
13105
|
+
wrapLines(sanitizeData(first.fix), groupContentWidth).forEach((seg, i) => {
|
|
13106
|
+
const label = i === 0 ? p.accent("Fix".padEnd(CARD_LABEL_PAD)) : " ".repeat(CARD_LABEL_PAD);
|
|
13107
|
+
lines.push(`${CARD_GUTTER}${label}${p.dim(seg)}`);
|
|
13108
|
+
});
|
|
13109
|
+
for (const f of unit.findings) wrapLines(`· ${sanitizeData(f.file)}:${f.line} — ${sanitizeData(f.message)}`, groupContentWidth).forEach((seg, i) => {
|
|
13110
|
+
lines.push(`${CARD_GUTTER}${" ".repeat(CARD_LABEL_PAD)}${p.dim(i === 0 ? seg : ` ${seg}`)}`);
|
|
13111
|
+
});
|
|
13095
13112
|
lines.push("");
|
|
13096
13113
|
shown++;
|
|
13097
13114
|
continue;
|
|
@@ -13129,22 +13146,34 @@ function appendForgedBlock(lines, p, ascii) {
|
|
|
13129
13146
|
lines.push(p.forged(TROPHY));
|
|
13130
13147
|
lines.push("");
|
|
13131
13148
|
}
|
|
13149
|
+
/**
|
|
13150
|
+
* Pushes dimmed prose that respects the terminal width.
|
|
13151
|
+
*
|
|
13152
|
+
* The honesty footer used to be pushed as single unbroken strings — the
|
|
13153
|
+
* rule-coverage line alone is ~147 columns, so it overflowed every
|
|
13154
|
+
* default 80- or 100-column terminal and ignored `--width` entirely.
|
|
13155
|
+
* `wrapText` is the same helper the finding cards already use.
|
|
13156
|
+
*/
|
|
13157
|
+
function pushWrapped(lines, p, text, width) {
|
|
13158
|
+
const indent = " ";
|
|
13159
|
+
for (const line of wrapText(text, Math.max(20, width - 2))) lines.push(p.dim(`${indent}${line}`));
|
|
13160
|
+
}
|
|
13132
13161
|
function appendFooter(lines, result, ui) {
|
|
13133
|
-
const { p } = ui;
|
|
13162
|
+
const { p, width } = ui;
|
|
13134
13163
|
lines.push(...buildFooter({
|
|
13135
13164
|
ui,
|
|
13136
13165
|
complete: result.analysisStatus.discovery !== "partial",
|
|
13137
13166
|
durationMs: result.analysisStatus.durationMs
|
|
13138
13167
|
}));
|
|
13139
13168
|
const advisory = result.findings.filter((f) => (f.evidenceLevel ?? deriveEvidenceLevel(f.findingType, f.confidence)) === "E0").length;
|
|
13140
|
-
if (advisory > 0) lines
|
|
13169
|
+
if (advisory > 0) pushWrapped(lines, p, `${advisory} advisory finding${advisory === 1 ? "" : "s"} (E0 — observation only, no score impact)`, width);
|
|
13141
13170
|
if (result.findings.length > 0) {
|
|
13142
13171
|
const firedRuleIds = new Set(result.findings.map((f) => f.ruleId));
|
|
13143
13172
|
const measuredHere = [...firedRuleIds].filter((id) => MEASURED_FP[id] !== void 0).length;
|
|
13144
|
-
lines
|
|
13173
|
+
pushWrapped(lines, p, `Rule coverage: ${measuredHere}/${firedRuleIds.size} rules that fired here have a measured false-positive rate; the rest are heuristics. \`mjolnir rules --unmeasured\` lists them.`, width);
|
|
13145
13174
|
const verified = result.findings.filter((f) => f.runtimeCorroboration !== void 0).length;
|
|
13146
|
-
if (verified > 0) lines
|
|
13147
|
-
else lines
|
|
13175
|
+
if (verified > 0) pushWrapped(lines, p, `Runtime evidence: ${verified}/${result.findings.length} findings corroborated by a real run report (trust L3–L5); the rest are static-only.`, width);
|
|
13176
|
+
else pushWrapped(lines, p, `Runtime evidence: not available — no run report (mjolnir.report.json / test-results) next to the scan target; all findings are static-only (L0–L2).`, width);
|
|
13148
13177
|
}
|
|
13149
13178
|
const profile = result.agenticProfile;
|
|
13150
13179
|
if (profile && (profile.generatedMarkedFiles > 0 || profile.codegenLikeFiles > 0)) {
|
|
@@ -13224,7 +13253,7 @@ function renderSarif(result, repoRootUri) {
|
|
|
13224
13253
|
tool: { driver: {
|
|
13225
13254
|
name: "Mjölnir",
|
|
13226
13255
|
informationUri: "https://github.com/Sergey-Bar/Mjolnir",
|
|
13227
|
-
version: "0.5.
|
|
13256
|
+
version: "0.5.13",
|
|
13228
13257
|
rules: [...rules.values()].map((r) => {
|
|
13229
13258
|
const meta = RULES.find((x) => x.id === r.id);
|
|
13230
13259
|
return {
|
|
@@ -13582,7 +13611,7 @@ function renderPrComment(result, options = {}) {
|
|
|
13582
13611
|
for (const d of result.dimensions) lines.push(`| ${escapeMarkdown(d.category)} | ${d.score}/100 |`);
|
|
13583
13612
|
lines.push("");
|
|
13584
13613
|
}
|
|
13585
|
-
if (findings.length === 0) lines.push("✅ No new
|
|
13614
|
+
if (findings.length === 0) lines.push("✅ No new findings in this PR's changes.");
|
|
13586
13615
|
else {
|
|
13587
13616
|
const errors = findings.filter((f) => f.severity === "error");
|
|
13588
13617
|
const warnings = findings.filter((f) => f.severity === "warning");
|
|
@@ -17456,14 +17485,29 @@ function explainRule(ruleId, fixturesRoot) {
|
|
|
17456
17485
|
ok: true,
|
|
17457
17486
|
rule,
|
|
17458
17487
|
exampleFinding: example,
|
|
17459
|
-
exampleFixturePath: fixturePath
|
|
17488
|
+
exampleFixturePath: fixturePath,
|
|
17489
|
+
exampleFixtureRelPath: relative(fixturesRoot, fixturePath).replaceAll("\\", "/")
|
|
17460
17490
|
};
|
|
17461
17491
|
}
|
|
17462
|
-
|
|
17492
|
+
/**
|
|
17493
|
+
* Default column budget when no width is supplied.
|
|
17494
|
+
*
|
|
17495
|
+
* `explain`'s prose used to be pushed as unbroken strings — the
|
|
17496
|
+
* "HOW TO VERIFY THE FIX" paragraph alone is 150 columns — so every
|
|
17497
|
+
* explanation overflowed a default terminal. Renderers here take a width
|
|
17498
|
+
* rather than reading process.stdout, so output stays a pure function of
|
|
17499
|
+
* its arguments (same rule the reporter's palette follows).
|
|
17500
|
+
*/
|
|
17501
|
+
const DEFAULT_EXPLAIN_WIDTH = 80;
|
|
17502
|
+
function renderExplain(result, width = DEFAULT_EXPLAIN_WIDTH) {
|
|
17463
17503
|
if (!result.ok || !result.rule) return `explain failed: ${result.error ?? "unknown error"}`;
|
|
17464
17504
|
const r = result.rule;
|
|
17465
17505
|
const evidenceLevel = r.evidenceLevel ?? deriveEvidenceLevel(r.findingType, r.confidence);
|
|
17466
17506
|
const lines = [];
|
|
17507
|
+
/** Pushes prose indented two columns, wrapped to the budget. */
|
|
17508
|
+
const pushBody = (text) => {
|
|
17509
|
+
for (const seg of wrapText(text, Math.max(20, width - 2))) lines.push(` ${seg}`);
|
|
17510
|
+
};
|
|
17467
17511
|
lines.push(sectionHeader(`${r.id} — ${r.title}`, ui$1));
|
|
17468
17512
|
lines.push("");
|
|
17469
17513
|
lines.push(`Severity: ${r.severity}`);
|
|
@@ -17480,19 +17524,19 @@ function renderExplain(result) {
|
|
|
17480
17524
|
if (result.exampleFinding) {
|
|
17481
17525
|
const f = result.exampleFinding;
|
|
17482
17526
|
lines.push("WHAT WAS FOUND (real detector output, not a mockup)");
|
|
17483
|
-
|
|
17527
|
+
pushBody(f.message);
|
|
17484
17528
|
lines.push("");
|
|
17485
17529
|
lines.push("WHY IT MATTERS");
|
|
17486
|
-
|
|
17530
|
+
pushBody(f.why);
|
|
17487
17531
|
lines.push("");
|
|
17488
17532
|
lines.push("HOW TO FIX");
|
|
17489
|
-
|
|
17533
|
+
pushBody(f.fix);
|
|
17490
17534
|
lines.push("");
|
|
17491
|
-
|
|
17492
|
-
} else
|
|
17535
|
+
pushBody(`Example from this rule's own must-fire fixture: ${result.exampleFixtureRelPath ?? result.exampleFixturePath ?? "(unknown path)"}`);
|
|
17536
|
+
} else for (const seg of wrapText("No example available — run this command from a mjolnir checkout (or pass --fixtures-root) so the fixture that proves this rule works can be shown as a real example.", width)) lines.push(seg);
|
|
17493
17537
|
lines.push("");
|
|
17494
17538
|
lines.push("HOW TO VERIFY THE FIX");
|
|
17495
|
-
|
|
17539
|
+
pushBody("Re-run `mjolnir` on the changed file(s) — this finding should no longer appear. `mjolnir --scope changed` scopes the check to just what you touched.");
|
|
17496
17540
|
lines.push("");
|
|
17497
17541
|
lines.push(`Docs: mjolnir rules --md (full catalog, this rule included)`);
|
|
17498
17542
|
return lines.join("\n");
|
|
@@ -17649,7 +17693,7 @@ const { runScan, buildUniversalRules, fallbackWorkspace, pathMatchesGlob, isVali
|
|
|
17649
17693
|
* `scripts/sync-sarif-version.cjs` on release and guarded by
|
|
17650
17694
|
* `tests/version-consistency.spec.ts` locally.
|
|
17651
17695
|
*/
|
|
17652
|
-
const CLI_VERSION = "0.5.
|
|
17696
|
+
const CLI_VERSION = "0.5.13";
|
|
17653
17697
|
function parseArgs(argv, onError) {
|
|
17654
17698
|
const args = {
|
|
17655
17699
|
target: ".",
|
package/dist/mcp/stdio.mjs
CHANGED
|
@@ -12480,26 +12480,26 @@ function topFixes(findings, n = 3) {
|
|
|
12480
12480
|
}
|
|
12481
12481
|
//#endregion
|
|
12482
12482
|
//#region src/reporter/art.ts
|
|
12483
|
-
/**
|
|
12483
|
+
/**
|
|
12484
|
+
* The report's wordmark. A LOCKUP, not a picture of a hammer.
|
|
12485
|
+
*
|
|
12486
|
+
* This used to be eight rows of box-drawing that spelled out a hammer —
|
|
12487
|
+
* directly above the score instrument, which is also a hammer. Two
|
|
12488
|
+
* hammers, ten lines apart, and the smaller static one came first, so
|
|
12489
|
+
* appendScoreSection's own claim that "the hammer is the instrument: the
|
|
12490
|
+
* first thing the eye lands on" was false in every report the tool has
|
|
12491
|
+
* ever printed.
|
|
12492
|
+
*
|
|
12493
|
+
* There is one hammer now, and it is the one that means something: the
|
|
12494
|
+
* state-resolved instrument in HAMMER_STATES. The wordmark stays out of
|
|
12495
|
+
* its way.
|
|
12496
|
+
*/
|
|
12484
12497
|
const LOGO = `
|
|
12485
|
-
|
|
12486
|
-
║ ║
|
|
12487
|
-
╠═══════════╣ M J Ö L N I R
|
|
12488
|
-
║ ║ ║
|
|
12489
|
-
╚═════╩═════╝ VERIFICATION TRUST ENGINE
|
|
12490
|
-
║
|
|
12491
|
-
║
|
|
12498
|
+
M J Ö L N I R · VERIFICATION TRUST ENGINE
|
|
12492
12499
|
`;
|
|
12493
|
-
/** Plain-ASCII fallback
|
|
12494
|
-
* block-drawing LOGO above renders as mangled "?" glyphs. */
|
|
12500
|
+
/** Plain-ASCII fallback for cmd.exe/legacy consoles. */
|
|
12495
12501
|
const LOGO_ASCII = `
|
|
12496
|
-
|
|
12497
|
-
| |
|
|
12498
|
-
+-----------+ M J O L N I R
|
|
12499
|
-
| | |
|
|
12500
|
-
+-----+-----+ VERIFICATION TRUST ENGINE
|
|
12501
|
-
|
|
|
12502
|
-
|
|
|
12502
|
+
M J O L N I R - VERIFICATION TRUST ENGINE
|
|
12503
12503
|
`;
|
|
12504
12504
|
const TROPHY = String.raw`
|
|
12505
12505
|
___________
|
|
@@ -13000,10 +13000,15 @@ const CARD_GUTTER = " ";
|
|
|
13000
13000
|
function pushCard(lines, card, ui) {
|
|
13001
13001
|
const { p, width } = ui;
|
|
13002
13002
|
const contentWidth = Math.max(20, width - 2 - 4 - CARD_LABEL_PAD);
|
|
13003
|
-
|
|
13003
|
+
const header = ` ${severityIcon(card.severity, ui)} ${p.bold(card.loc)}`;
|
|
13004
|
+
if (measure(`${header} ${card.evidence}`) <= width) lines.push(`${header} ${p.dim(card.evidence)}`);
|
|
13005
|
+
else {
|
|
13006
|
+
lines.push(header);
|
|
13007
|
+
lines.push(`${CARD_GUTTER}${p.dim(card.evidence)}`);
|
|
13008
|
+
}
|
|
13004
13009
|
const fields = [
|
|
13005
13010
|
{
|
|
13006
|
-
label: "
|
|
13011
|
+
label: "Finding",
|
|
13007
13012
|
text: card.problem,
|
|
13008
13013
|
dim: false
|
|
13009
13014
|
},
|
|
@@ -13034,13 +13039,13 @@ function pushCard(lines, card, ui) {
|
|
|
13034
13039
|
}
|
|
13035
13040
|
/**
|
|
13036
13041
|
* The findings experience — EVIDENCE layer. Cards carry
|
|
13037
|
-
* severity →
|
|
13042
|
+
* severity → Finding → Impact → Fix → Verify; the evidence tag sits
|
|
13038
13043
|
* beside the title. >3 findings sharing a rule collapse under one
|
|
13039
13044
|
* "same fix applies" header. Non-verbose shows MAX_CARDS cards plus an
|
|
13040
13045
|
* overflow line; --verbose shows everything.
|
|
13041
13046
|
*/
|
|
13042
13047
|
function appendFindings(lines, result, counts, verbose, ui, tone) {
|
|
13043
|
-
const { p } = ui;
|
|
13048
|
+
const { p, width } = ui;
|
|
13044
13049
|
if (counts.total === 0) return;
|
|
13045
13050
|
if (result.findings.length === 0) {
|
|
13046
13051
|
lines.push(ui.p.dim(" filtered view: no findings in the selected category"));
|
|
@@ -13089,9 +13094,21 @@ function appendFindings(lines, result, counts, verbose, ui, tone) {
|
|
|
13089
13094
|
hiddenRules.add(unit.ruleId);
|
|
13090
13095
|
continue;
|
|
13091
13096
|
}
|
|
13092
|
-
|
|
13093
|
-
|
|
13094
|
-
|
|
13097
|
+
const groupHead = ` ${severityIcon(maxSeverity(unit.findings), ui)} ${p.bold(sanitizeData(unit.ruleId))} ${p.dim(`× ${n} — same fix applies`)}`;
|
|
13098
|
+
const groupEvidence = evidenceTag$1(first);
|
|
13099
|
+
if (measure(`${groupHead} ${groupEvidence}`) <= width) lines.push(`${groupHead} ${p.dim(groupEvidence)}`);
|
|
13100
|
+
else {
|
|
13101
|
+
lines.push(groupHead);
|
|
13102
|
+
lines.push(`${CARD_GUTTER}${p.dim(groupEvidence)}`);
|
|
13103
|
+
}
|
|
13104
|
+
const groupContentWidth = Math.max(20, width - 2 - 4 - CARD_LABEL_PAD);
|
|
13105
|
+
wrapLines(sanitizeData(first.fix), groupContentWidth).forEach((seg, i) => {
|
|
13106
|
+
const label = i === 0 ? p.accent("Fix".padEnd(CARD_LABEL_PAD)) : " ".repeat(CARD_LABEL_PAD);
|
|
13107
|
+
lines.push(`${CARD_GUTTER}${label}${p.dim(seg)}`);
|
|
13108
|
+
});
|
|
13109
|
+
for (const f of unit.findings) wrapLines(`· ${sanitizeData(f.file)}:${f.line} — ${sanitizeData(f.message)}`, groupContentWidth).forEach((seg, i) => {
|
|
13110
|
+
lines.push(`${CARD_GUTTER}${" ".repeat(CARD_LABEL_PAD)}${p.dim(i === 0 ? seg : ` ${seg}`)}`);
|
|
13111
|
+
});
|
|
13095
13112
|
lines.push("");
|
|
13096
13113
|
shown++;
|
|
13097
13114
|
continue;
|
|
@@ -13129,22 +13146,34 @@ function appendForgedBlock(lines, p, ascii) {
|
|
|
13129
13146
|
lines.push(p.forged(TROPHY));
|
|
13130
13147
|
lines.push("");
|
|
13131
13148
|
}
|
|
13149
|
+
/**
|
|
13150
|
+
* Pushes dimmed prose that respects the terminal width.
|
|
13151
|
+
*
|
|
13152
|
+
* The honesty footer used to be pushed as single unbroken strings — the
|
|
13153
|
+
* rule-coverage line alone is ~147 columns, so it overflowed every
|
|
13154
|
+
* default 80- or 100-column terminal and ignored `--width` entirely.
|
|
13155
|
+
* `wrapText` is the same helper the finding cards already use.
|
|
13156
|
+
*/
|
|
13157
|
+
function pushWrapped(lines, p, text, width) {
|
|
13158
|
+
const indent = " ";
|
|
13159
|
+
for (const line of wrapText(text, Math.max(20, width - 2))) lines.push(p.dim(`${indent}${line}`));
|
|
13160
|
+
}
|
|
13132
13161
|
function appendFooter(lines, result, ui) {
|
|
13133
|
-
const { p } = ui;
|
|
13162
|
+
const { p, width } = ui;
|
|
13134
13163
|
lines.push(...buildFooter({
|
|
13135
13164
|
ui,
|
|
13136
13165
|
complete: result.analysisStatus.discovery !== "partial",
|
|
13137
13166
|
durationMs: result.analysisStatus.durationMs
|
|
13138
13167
|
}));
|
|
13139
13168
|
const advisory = result.findings.filter((f) => (f.evidenceLevel ?? deriveEvidenceLevel(f.findingType, f.confidence)) === "E0").length;
|
|
13140
|
-
if (advisory > 0) lines
|
|
13169
|
+
if (advisory > 0) pushWrapped(lines, p, `${advisory} advisory finding${advisory === 1 ? "" : "s"} (E0 — observation only, no score impact)`, width);
|
|
13141
13170
|
if (result.findings.length > 0) {
|
|
13142
13171
|
const firedRuleIds = new Set(result.findings.map((f) => f.ruleId));
|
|
13143
13172
|
const measuredHere = [...firedRuleIds].filter((id) => MEASURED_FP[id] !== void 0).length;
|
|
13144
|
-
lines
|
|
13173
|
+
pushWrapped(lines, p, `Rule coverage: ${measuredHere}/${firedRuleIds.size} rules that fired here have a measured false-positive rate; the rest are heuristics. \`mjolnir rules --unmeasured\` lists them.`, width);
|
|
13145
13174
|
const verified = result.findings.filter((f) => f.runtimeCorroboration !== void 0).length;
|
|
13146
|
-
if (verified > 0) lines
|
|
13147
|
-
else lines
|
|
13175
|
+
if (verified > 0) pushWrapped(lines, p, `Runtime evidence: ${verified}/${result.findings.length} findings corroborated by a real run report (trust L3–L5); the rest are static-only.`, width);
|
|
13176
|
+
else pushWrapped(lines, p, `Runtime evidence: not available — no run report (mjolnir.report.json / test-results) next to the scan target; all findings are static-only (L0–L2).`, width);
|
|
13148
13177
|
}
|
|
13149
13178
|
const profile = result.agenticProfile;
|
|
13150
13179
|
if (profile && (profile.generatedMarkedFiles > 0 || profile.codegenLikeFiles > 0)) {
|
|
@@ -13224,7 +13253,7 @@ function renderSarif(result, repoRootUri) {
|
|
|
13224
13253
|
tool: { driver: {
|
|
13225
13254
|
name: "Mjölnir",
|
|
13226
13255
|
informationUri: "https://github.com/Sergey-Bar/Mjolnir",
|
|
13227
|
-
version: "0.5.
|
|
13256
|
+
version: "0.5.13",
|
|
13228
13257
|
rules: [...rules.values()].map((r) => {
|
|
13229
13258
|
const meta = RULES.find((x) => x.id === r.id);
|
|
13230
13259
|
return {
|
|
@@ -13582,7 +13611,7 @@ function renderPrComment(result, options = {}) {
|
|
|
13582
13611
|
for (const d of result.dimensions) lines.push(`| ${escapeMarkdown(d.category)} | ${d.score}/100 |`);
|
|
13583
13612
|
lines.push("");
|
|
13584
13613
|
}
|
|
13585
|
-
if (findings.length === 0) lines.push("✅ No new
|
|
13614
|
+
if (findings.length === 0) lines.push("✅ No new findings in this PR's changes.");
|
|
13586
13615
|
else {
|
|
13587
13616
|
const errors = findings.filter((f) => f.severity === "error");
|
|
13588
13617
|
const warnings = findings.filter((f) => f.severity === "warning");
|
|
@@ -17456,14 +17485,29 @@ function explainRule(ruleId, fixturesRoot) {
|
|
|
17456
17485
|
ok: true,
|
|
17457
17486
|
rule,
|
|
17458
17487
|
exampleFinding: example,
|
|
17459
|
-
exampleFixturePath: fixturePath
|
|
17488
|
+
exampleFixturePath: fixturePath,
|
|
17489
|
+
exampleFixtureRelPath: relative(fixturesRoot, fixturePath).replaceAll("\\", "/")
|
|
17460
17490
|
};
|
|
17461
17491
|
}
|
|
17462
|
-
|
|
17492
|
+
/**
|
|
17493
|
+
* Default column budget when no width is supplied.
|
|
17494
|
+
*
|
|
17495
|
+
* `explain`'s prose used to be pushed as unbroken strings — the
|
|
17496
|
+
* "HOW TO VERIFY THE FIX" paragraph alone is 150 columns — so every
|
|
17497
|
+
* explanation overflowed a default terminal. Renderers here take a width
|
|
17498
|
+
* rather than reading process.stdout, so output stays a pure function of
|
|
17499
|
+
* its arguments (same rule the reporter's palette follows).
|
|
17500
|
+
*/
|
|
17501
|
+
const DEFAULT_EXPLAIN_WIDTH = 80;
|
|
17502
|
+
function renderExplain(result, width = DEFAULT_EXPLAIN_WIDTH) {
|
|
17463
17503
|
if (!result.ok || !result.rule) return `explain failed: ${result.error ?? "unknown error"}`;
|
|
17464
17504
|
const r = result.rule;
|
|
17465
17505
|
const evidenceLevel = r.evidenceLevel ?? deriveEvidenceLevel(r.findingType, r.confidence);
|
|
17466
17506
|
const lines = [];
|
|
17507
|
+
/** Pushes prose indented two columns, wrapped to the budget. */
|
|
17508
|
+
const pushBody = (text) => {
|
|
17509
|
+
for (const seg of wrapText(text, Math.max(20, width - 2))) lines.push(` ${seg}`);
|
|
17510
|
+
};
|
|
17467
17511
|
lines.push(sectionHeader(`${r.id} — ${r.title}`, ui$1));
|
|
17468
17512
|
lines.push("");
|
|
17469
17513
|
lines.push(`Severity: ${r.severity}`);
|
|
@@ -17480,19 +17524,19 @@ function renderExplain(result) {
|
|
|
17480
17524
|
if (result.exampleFinding) {
|
|
17481
17525
|
const f = result.exampleFinding;
|
|
17482
17526
|
lines.push("WHAT WAS FOUND (real detector output, not a mockup)");
|
|
17483
|
-
|
|
17527
|
+
pushBody(f.message);
|
|
17484
17528
|
lines.push("");
|
|
17485
17529
|
lines.push("WHY IT MATTERS");
|
|
17486
|
-
|
|
17530
|
+
pushBody(f.why);
|
|
17487
17531
|
lines.push("");
|
|
17488
17532
|
lines.push("HOW TO FIX");
|
|
17489
|
-
|
|
17533
|
+
pushBody(f.fix);
|
|
17490
17534
|
lines.push("");
|
|
17491
|
-
|
|
17492
|
-
} else
|
|
17535
|
+
pushBody(`Example from this rule's own must-fire fixture: ${result.exampleFixtureRelPath ?? result.exampleFixturePath ?? "(unknown path)"}`);
|
|
17536
|
+
} else for (const seg of wrapText("No example available — run this command from a mjolnir checkout (or pass --fixtures-root) so the fixture that proves this rule works can be shown as a real example.", width)) lines.push(seg);
|
|
17493
17537
|
lines.push("");
|
|
17494
17538
|
lines.push("HOW TO VERIFY THE FIX");
|
|
17495
|
-
|
|
17539
|
+
pushBody("Re-run `mjolnir` on the changed file(s) — this finding should no longer appear. `mjolnir --scope changed` scopes the check to just what you touched.");
|
|
17496
17540
|
lines.push("");
|
|
17497
17541
|
lines.push(`Docs: mjolnir rules --md (full catalog, this rule included)`);
|
|
17498
17542
|
return lines.join("\n");
|
|
@@ -17649,7 +17693,7 @@ const { runScan, buildUniversalRules, fallbackWorkspace, pathMatchesGlob, isVali
|
|
|
17649
17693
|
* `scripts/sync-sarif-version.cjs` on release and guarded by
|
|
17650
17694
|
* `tests/version-consistency.spec.ts` locally.
|
|
17651
17695
|
*/
|
|
17652
|
-
const CLI_VERSION = "0.5.
|
|
17696
|
+
const CLI_VERSION = "0.5.13";
|
|
17653
17697
|
function parseArgs(argv, onError) {
|
|
17654
17698
|
const args = {
|
|
17655
17699
|
target: ".",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mjolnir-qa",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.13",
|
|
4
4
|
"description": "Mjölnir — the Verification Trust Engine for QA. Audits test suites and CI pipelines, reports a worthiness score and prioritized findings.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -44,6 +44,10 @@
|
|
|
44
44
|
"site:audit": "node site/scripts/audit-live.mjs",
|
|
45
45
|
"site:lighthouse": "node site/scripts/lighthouse-run.mjs",
|
|
46
46
|
"docs:demo": "tsx scripts/generate-readme-demo.ts",
|
|
47
|
+
"docs:video:capture": "tsx scripts/video/write-scripts.ts",
|
|
48
|
+
"docs:video:glyphs": "tsx scripts/video/check-glyphs.ts",
|
|
49
|
+
"docs:video:render": "tsx scripts/video/render.ts",
|
|
50
|
+
"docs:video": "npm run docs:video:capture && npm run docs:video:render",
|
|
47
51
|
"docs:translations": "node scripts/check-readme-translations.mjs",
|
|
48
52
|
"self-scan": "node dist/cli.mjs .",
|
|
49
53
|
"prepare": "husky",
|
|
@@ -91,6 +95,7 @@
|
|
|
91
95
|
"husky": "^9.1.7",
|
|
92
96
|
"lighthouse": "^13.4.1",
|
|
93
97
|
"lint-staged": "^17.3.0",
|
|
98
|
+
"playwright-core": "^1.63.0",
|
|
94
99
|
"prettier": "^3.3.0",
|
|
95
100
|
"tsdown": "^0.22.14",
|
|
96
101
|
"tsx": "^4.16.0",
|