mjolnir-qa 0.5.37 → 0.5.38
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 +6 -0
- package/dist/cli.d.mts +12 -2
- package/dist/cli.mjs +214 -7
- package/dist/mcp/stdio.mjs +213 -6
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -9,6 +9,12 @@ 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.38] — 2026-09-09
|
|
13
|
+
|
|
14
|
+
### Changes since 0
|
|
15
|
+
|
|
16
|
+
- P7: agent loop — mjolnir verify + MCP verify tool + install surfaces (#68)
|
|
17
|
+
|
|
12
18
|
## [0.5.37] — 2026-09-08
|
|
13
19
|
|
|
14
20
|
### Changes since 0
|
package/dist/cli.d.mts
CHANGED
|
@@ -783,7 +783,7 @@ declare const runScan: typeof runScan$1, buildUniversalRules: typeof buildUniver
|
|
|
783
783
|
* `scripts/sync-sarif-version.cjs` on release and guarded by
|
|
784
784
|
* `tests/version-consistency.spec.ts` locally.
|
|
785
785
|
*/
|
|
786
|
-
declare const CLI_VERSION = "0.5.
|
|
786
|
+
declare const CLI_VERSION = "0.5.38";
|
|
787
787
|
/** A usage-error detail: the offending token, when one exists. */
|
|
788
788
|
interface UsageErrorDetail {
|
|
789
789
|
/** The unknown flag or rejected value (e.g. `--nope`, `loud`). */
|
|
@@ -898,6 +898,16 @@ declare function runBaselineCommand(argv: string[], io?: {
|
|
|
898
898
|
err: Output;
|
|
899
899
|
}): Promise<number>;
|
|
900
900
|
/** Testable `diff` handler (Sprint 6 Task 24) — new/worsened debt only. */
|
|
901
|
+
/** Testable `verify` handler — the agent-loop verb (master plan P7,
|
|
902
|
+
* plan 1788853205786). Scans the target and diffs against the committed
|
|
903
|
+
* baseline, rendering the before/after digest the agent loop consumes
|
|
904
|
+
* (resolved per §15 · new · unchanged by ruleId+location · score delta).
|
|
905
|
+
* Exit semantics are the frozen contract: 0 clean · 1 new error
|
|
906
|
+
* findings · 2 partial scan or no baseline · 20 internal. */
|
|
907
|
+
declare function runVerifyCommand(argv: string[], io?: {
|
|
908
|
+
out: Output;
|
|
909
|
+
err: Output;
|
|
910
|
+
}): Promise<number>;
|
|
901
911
|
declare function runDiffCommand(argv: string[], io?: {
|
|
902
912
|
out: Output;
|
|
903
913
|
err: Output;
|
|
@@ -943,4 +953,4 @@ declare function runHelpCommand(argv: string[], io?: {
|
|
|
943
953
|
}): number;
|
|
944
954
|
declare function isEntryPoint(): boolean;
|
|
945
955
|
//#endregion
|
|
946
|
-
export { CLI_VERSION, type CliArgs, EVIDENCE_OVERRIDES, KNOWN_RULE_IDS, OVERLAP_META_BY_RULE_ID, type Output, SUITE_INVALIDATING_RULE_IDS, type ScanHooks, UsageErrorDetail, buildUniversalRules, discoverRuntimeReport, err, exitForFindings, fallbackWorkspace, internalErrorMessage, isEntryPoint, isValidFindingRecord, levenshtein, main, nearestFlags, out, parseArgs, pathMatchesGlob, runBadgeCommand, runBaselineCommand, runCiInstall, runCreateRuleCommand, runDebtCommand, runDiffCommand, runDoctorCommand, runDoctorPlaywright, runExplainCommand, runFixCommand, runForensicsCommand, runHandoverCommand, runHelpCommand, runImpactCommand, runInitCommand, runMutationCommand, runPrCommentCommand, runPwReportCommand, runRulesCommand, runScan, runScanCommand, runStatsCommand, runSuppressions, runTriageCommand, usageErrorMessage };
|
|
956
|
+
export { CLI_VERSION, type CliArgs, EVIDENCE_OVERRIDES, KNOWN_RULE_IDS, OVERLAP_META_BY_RULE_ID, type Output, SUITE_INVALIDATING_RULE_IDS, type ScanHooks, UsageErrorDetail, buildUniversalRules, discoverRuntimeReport, err, exitForFindings, fallbackWorkspace, internalErrorMessage, isEntryPoint, isValidFindingRecord, levenshtein, main, nearestFlags, out, parseArgs, pathMatchesGlob, runBadgeCommand, runBaselineCommand, runCiInstall, runCreateRuleCommand, runDebtCommand, runDiffCommand, runDoctorCommand, runDoctorPlaywright, runExplainCommand, runFixCommand, runForensicsCommand, runHandoverCommand, runHelpCommand, runImpactCommand, runInitCommand, runMutationCommand, runPrCommentCommand, runPwReportCommand, runRulesCommand, runScan, runScanCommand, runStatsCommand, runSuppressions, runTriageCommand, runVerifyCommand, usageErrorMessage };
|
package/dist/cli.mjs
CHANGED
|
@@ -13464,7 +13464,7 @@ function renderSarif(result, repoRootUri) {
|
|
|
13464
13464
|
tool: { driver: {
|
|
13465
13465
|
name: "Mjölnir",
|
|
13466
13466
|
informationUri: "https://github.com/Sergey-Bar/Mjolnir",
|
|
13467
|
-
version: "0.5.
|
|
13467
|
+
version: "0.5.38",
|
|
13468
13468
|
rules: [...rules.values()].map((r) => {
|
|
13469
13469
|
const meta = RULES.find((x) => x.id === r.id);
|
|
13470
13470
|
return {
|
|
@@ -14697,6 +14697,7 @@ function agentBrief(surface, cwdNote) {
|
|
|
14697
14697
|
"- Exit 0 = clean. Exit 1 = new findings on changed lines: fix them (smallest behavior-preserving change) or justify them to the user.",
|
|
14698
14698
|
`- npx mjolnir-qa@${CLI_VERSION} why <file>:<line> explains any finding (evidence level, measured FP rate, fix).`,
|
|
14699
14699
|
`- npx mjolnir-qa@${CLI_VERSION} handoff renders a full remediation plan from a saved --json report.`,
|
|
14700
|
+
`- Agent loop: establish the before-state once with npx mjolnir-qa@${CLI_VERSION} baseline (commits .mjolnir/baseline.json); after fixing, npx mjolnir-qa@${CLI_VERSION} verify prints the before/after digest — resolved (per §15 lifecycle) / new / unchanged by ruleId+location, and the score delta. Exit 0 clean, 1 new errors, 2 partial or no baseline.`,
|
|
14700
14701
|
"- NEVER suppress a finding merely to obtain a green scan (suppressions live in mjolnir.config.json, require a reason, and expire after 90 days).",
|
|
14701
14702
|
"- After fixing: re-run the scan, report files changed, report checks not run, report unresolved findings honestly.",
|
|
14702
14703
|
"",
|
|
@@ -15525,6 +15526,113 @@ function renderBaselineDiff(diff) {
|
|
|
15525
15526
|
return lines.join("\n");
|
|
15526
15527
|
}
|
|
15527
15528
|
//#endregion
|
|
15529
|
+
//#region src/commands/verify.ts
|
|
15530
|
+
/**
|
|
15531
|
+
* `mjolnir verify` — the agent-loop digest (product-gap-remediation
|
|
15532
|
+
* master plan P7, plan 1788853205786 — flag agent, decision 6).
|
|
15533
|
+
*
|
|
15534
|
+
* An agent fixes findings, then runs `mjolnir verify` against the
|
|
15535
|
+
* committed baseline (established once with `mjolnir baseline`). The
|
|
15536
|
+
* digest answers exactly the loop's questions, derived from the SAME
|
|
15537
|
+
* §15 comparison machinery as `mjolnir diff` (no second truth):
|
|
15538
|
+
*
|
|
15539
|
+
* - RESOLVED — baseline findings no longer present, each with its
|
|
15540
|
+
* lifecycle resolution (only VERIFIED-RESOLVED is a fix claim);
|
|
15541
|
+
* - NEW — findings the fix introduced;
|
|
15542
|
+
* - UNCHANGED — pre-existing debt, grouped by ruleId + location
|
|
15543
|
+
* (the agent-loop key: an agent works by location, not message);
|
|
15544
|
+
* - SCORE DELTA — before (baseline) → after (this scan).
|
|
15545
|
+
*
|
|
15546
|
+
* Exit semantics are the frozen contract: 0 clean · 1 new error
|
|
15547
|
+
* findings · 2 partial scan or no baseline (nothing to verify against
|
|
15548
|
+
* — the agent must first establish the before-state).
|
|
15549
|
+
*/
|
|
15550
|
+
function buildVerifyDigest(result, baseline) {
|
|
15551
|
+
const diff = diffAgainstBaseline(result, baseline);
|
|
15552
|
+
const digest = {
|
|
15553
|
+
hasBaseline: diff.hasBaseline,
|
|
15554
|
+
...diff.baselineCapturedAt !== void 0 ? { baselineCapturedAt: diff.baselineCapturedAt } : {},
|
|
15555
|
+
...diff.baselineCommit !== void 0 ? { baselineCommit: diff.baselineCommit } : {},
|
|
15556
|
+
resolved: diff.resolvedFindings.map((f) => ({
|
|
15557
|
+
ruleId: f.ruleId,
|
|
15558
|
+
file: f.file,
|
|
15559
|
+
resolution: f.resolution.status
|
|
15560
|
+
})),
|
|
15561
|
+
new: diff.newFindings.map((f) => ({
|
|
15562
|
+
ruleId: f.ruleId,
|
|
15563
|
+
file: f.file,
|
|
15564
|
+
line: f.line,
|
|
15565
|
+
severity: f.severity
|
|
15566
|
+
})),
|
|
15567
|
+
unchanged: [],
|
|
15568
|
+
unchangedCount: diff.unchangedCount,
|
|
15569
|
+
scoreBefore: diff.baselineScore ?? null,
|
|
15570
|
+
scoreAfter: result.score ?? null,
|
|
15571
|
+
scoreDelta: diff.baselineScore !== void 0 && result.score !== null ? result.score - diff.baselineScore : null
|
|
15572
|
+
};
|
|
15573
|
+
if (diff.hasBaseline && baseline) {
|
|
15574
|
+
const byRule = /* @__PURE__ */ new Map();
|
|
15575
|
+
const headByFingerprint = /* @__PURE__ */ new Map();
|
|
15576
|
+
for (const f of result.findings) headByFingerprint.set(`${f.ruleId}\u0000${f.file}\u0000${f.message}`, f);
|
|
15577
|
+
for (const b of baseline.findings) {
|
|
15578
|
+
const head = headByFingerprint.get(`${b.ruleId}\u0000${b.file}\u0000${b.message}`);
|
|
15579
|
+
if (!head) continue;
|
|
15580
|
+
let group = byRule.get(b.ruleId);
|
|
15581
|
+
if (!group) {
|
|
15582
|
+
group = {
|
|
15583
|
+
ruleId: b.ruleId,
|
|
15584
|
+
locations: []
|
|
15585
|
+
};
|
|
15586
|
+
byRule.set(b.ruleId, group);
|
|
15587
|
+
}
|
|
15588
|
+
group.locations.push(`${head.file}:${head.line}`);
|
|
15589
|
+
}
|
|
15590
|
+
digest.unchanged = [...byRule.values()].map((g) => ({
|
|
15591
|
+
ruleId: g.ruleId,
|
|
15592
|
+
locations: g.locations.sort()
|
|
15593
|
+
}));
|
|
15594
|
+
}
|
|
15595
|
+
return digest;
|
|
15596
|
+
}
|
|
15597
|
+
function renderVerifyDigest(digest) {
|
|
15598
|
+
if (!digest.hasBaseline) return [
|
|
15599
|
+
"VERIFY — no baseline",
|
|
15600
|
+
"",
|
|
15601
|
+
"No committed baseline at .mjolnir/baseline.json — nothing to verify",
|
|
15602
|
+
"against. Establish the before-state once with `mjolnir baseline`,",
|
|
15603
|
+
"then re-run `mjolnir verify` after the fix."
|
|
15604
|
+
].join("\n");
|
|
15605
|
+
const lines = [];
|
|
15606
|
+
lines.push("VERIFY — before/after digest");
|
|
15607
|
+
lines.push("");
|
|
15608
|
+
if (digest.scoreDelta !== null) {
|
|
15609
|
+
const dir = digest.scoreDelta > 0 ? "improved" : digest.scoreDelta < 0 ? "worse" : "unchanged";
|
|
15610
|
+
lines.push(`score: ${digest.scoreBefore} → ${digest.scoreAfter} (${dir}, Δ${digest.scoreDelta >= 0 ? "+" : ""}${digest.scoreDelta})`);
|
|
15611
|
+
}
|
|
15612
|
+
lines.push(`${digest.resolved.length} resolved · ${digest.new.length} new · ${digest.unchangedCount} unchanged`);
|
|
15613
|
+
lines.push("");
|
|
15614
|
+
if (digest.resolved.length > 0) {
|
|
15615
|
+
lines.push("RESOLVED (per §15 lifecycle — only VERIFIED-RESOLVED is a fix claim):");
|
|
15616
|
+
for (const r of digest.resolved) lines.push(` ${r.ruleId} ${r.file} — ${r.resolution}`);
|
|
15617
|
+
lines.push("");
|
|
15618
|
+
}
|
|
15619
|
+
if (digest.new.length > 0) {
|
|
15620
|
+
lines.push("NEW (introduced by the change under verification):");
|
|
15621
|
+
for (const n of digest.new) lines.push(` ${n.ruleId} ${n.file}:${n.line} (${n.severity})`);
|
|
15622
|
+
lines.push("");
|
|
15623
|
+
}
|
|
15624
|
+
if (digest.unchanged.length > 0) {
|
|
15625
|
+
lines.push("UNCHANGED (pre-existing debt — out of this fix's scope):");
|
|
15626
|
+
for (const g of digest.unchanged) {
|
|
15627
|
+
lines.push(` ${g.ruleId} × ${g.locations.length}`);
|
|
15628
|
+
for (const loc of g.locations.slice(0, 5)) lines.push(` ${loc}`);
|
|
15629
|
+
if (g.locations.length > 5) lines.push(` … and ${g.locations.length - 5} more`);
|
|
15630
|
+
}
|
|
15631
|
+
lines.push("");
|
|
15632
|
+
}
|
|
15633
|
+
return lines.join("\n").trimEnd();
|
|
15634
|
+
}
|
|
15635
|
+
//#endregion
|
|
15528
15636
|
//#region src/mcp/server.ts
|
|
15529
15637
|
/** Hard caps (§21 threat model: parameter size + resource bounds). */
|
|
15530
15638
|
const MAX_PARAM_BYTES = 65536;
|
|
@@ -15586,6 +15694,18 @@ const MCP_TOOLS = [
|
|
|
15586
15694
|
},
|
|
15587
15695
|
required: ["path", "scanResult"]
|
|
15588
15696
|
}
|
|
15697
|
+
},
|
|
15698
|
+
{
|
|
15699
|
+
name: "verify",
|
|
15700
|
+
description: "Agent-loop digest (1:1 with `mjolnir verify`): scan the target and diff against the committed baseline. Returns resolved (per §15 lifecycle), new, unchanged (grouped by ruleId + location), and the score delta. Read-only; the same transport guardrails as every other tool.",
|
|
15701
|
+
inputSchema: {
|
|
15702
|
+
type: "object",
|
|
15703
|
+
properties: { path: {
|
|
15704
|
+
type: "string",
|
|
15705
|
+
description: "Directory to scan and verify."
|
|
15706
|
+
} },
|
|
15707
|
+
required: ["path"]
|
|
15708
|
+
}
|
|
15589
15709
|
}
|
|
15590
15710
|
];
|
|
15591
15711
|
/** Strict param validation — unknown/missing/mistyped ⇒ INVALID_PARAMS. */
|
|
@@ -15602,6 +15722,9 @@ function validateParams(name, args) {
|
|
|
15602
15722
|
if (typeof args["path"] !== "string" || args["path"].length === 0) return "diff requires a non-empty string `path`";
|
|
15603
15723
|
if (typeof args["scanResult"] !== "object" || args["scanResult"] === null) return "diff requires the `scanResult` object from a previous scan tool call";
|
|
15604
15724
|
}
|
|
15725
|
+
if (name === "verify") {
|
|
15726
|
+
if (typeof args["path"] !== "string" || args["path"].length === 0) return "verify requires a non-empty string `path`";
|
|
15727
|
+
}
|
|
15605
15728
|
return null;
|
|
15606
15729
|
}
|
|
15607
15730
|
/**
|
|
@@ -15718,6 +15841,52 @@ async function handleToolCall(call) {
|
|
|
15718
15841
|
}
|
|
15719
15842
|
};
|
|
15720
15843
|
}
|
|
15844
|
+
if (call.name === "verify") {
|
|
15845
|
+
const target = resolve(call.args["path"]);
|
|
15846
|
+
if (!existsSync(target)) return {
|
|
15847
|
+
jsonrpc: "2.0",
|
|
15848
|
+
id: call.id,
|
|
15849
|
+
error: {
|
|
15850
|
+
code: MCP_ERRORS.INVALID_PARAMS,
|
|
15851
|
+
message: `verify target does not exist: ${target}`
|
|
15852
|
+
}
|
|
15853
|
+
};
|
|
15854
|
+
const baselinePath = join(target, DEFAULT_BASELINE_PATH);
|
|
15855
|
+
if (!existsSync(baselinePath)) return {
|
|
15856
|
+
jsonrpc: "2.0",
|
|
15857
|
+
id: call.id,
|
|
15858
|
+
result: {
|
|
15859
|
+
hasBaseline: false,
|
|
15860
|
+
note: "no committed baseline at .mjolnir/baseline.json — establish the before-state with `mjolnir baseline` first"
|
|
15861
|
+
}
|
|
15862
|
+
};
|
|
15863
|
+
const result = await serializeScan(() => runScan({
|
|
15864
|
+
target,
|
|
15865
|
+
json: true,
|
|
15866
|
+
verbose: false,
|
|
15867
|
+
maxDurationMs: Number.POSITIVE_INFINITY,
|
|
15868
|
+
scopeChanged: false,
|
|
15869
|
+
format: "json"
|
|
15870
|
+
}));
|
|
15871
|
+
const digest = buildVerifyDigest(result, loadBaseline(baselinePath));
|
|
15872
|
+
return {
|
|
15873
|
+
jsonrpc: "2.0",
|
|
15874
|
+
id: call.id,
|
|
15875
|
+
result: {
|
|
15876
|
+
hasBaseline: digest.hasBaseline,
|
|
15877
|
+
baselineCapturedAt: digest.baselineCapturedAt,
|
|
15878
|
+
baselineCommit: digest.baselineCommit,
|
|
15879
|
+
resolved: digest.resolved,
|
|
15880
|
+
new: digest.new,
|
|
15881
|
+
unchanged: digest.unchanged,
|
|
15882
|
+
unchangedCount: digest.unchangedCount,
|
|
15883
|
+
scoreBefore: digest.scoreBefore,
|
|
15884
|
+
scoreAfter: digest.scoreAfter,
|
|
15885
|
+
scoreDelta: digest.scoreDelta,
|
|
15886
|
+
partial: result.partial
|
|
15887
|
+
}
|
|
15888
|
+
};
|
|
15889
|
+
}
|
|
15721
15890
|
return {
|
|
15722
15891
|
jsonrpc: "2.0",
|
|
15723
15892
|
id: call.id,
|
|
@@ -16669,10 +16838,16 @@ const HELP_ENTRIES = [
|
|
|
16669
16838
|
},
|
|
16670
16839
|
{
|
|
16671
16840
|
verb: "diff",
|
|
16672
|
-
summary: "
|
|
16673
|
-
usage: "mjolnir diff [path]",
|
|
16841
|
+
summary: "lifecycle diff vs the committed baseline",
|
|
16842
|
+
usage: "mjolnir diff [path] [--json] [scan flags]",
|
|
16674
16843
|
examples: ["mjolnir diff"]
|
|
16675
16844
|
},
|
|
16845
|
+
{
|
|
16846
|
+
verb: "verify",
|
|
16847
|
+
summary: "agent-loop digest: resolved/new/unchanged vs baseline + score delta",
|
|
16848
|
+
usage: "mjolnir verify [path] [--json] [scan flags]",
|
|
16849
|
+
examples: ["mjolnir verify"]
|
|
16850
|
+
},
|
|
16676
16851
|
{
|
|
16677
16852
|
verb: "impact",
|
|
16678
16853
|
summary: "what a commit introduced vs resolved, since a prior commit",
|
|
@@ -16759,7 +16934,7 @@ const HELP_ENTRIES = [
|
|
|
16759
16934
|
},
|
|
16760
16935
|
{
|
|
16761
16936
|
verb: "mcp",
|
|
16762
|
-
summary: "run as an MCP server over stdio (scan / explain / diff tools)",
|
|
16937
|
+
summary: "run as an MCP server over stdio (scan / explain / diff / verify tools)",
|
|
16763
16938
|
usage: "mjolnir mcp",
|
|
16764
16939
|
examples: ["mjolnir mcp"]
|
|
16765
16940
|
}
|
|
@@ -16912,7 +17087,8 @@ const GROUPS = [
|
|
|
16912
17087
|
"badge",
|
|
16913
17088
|
"impact",
|
|
16914
17089
|
"baseline",
|
|
16915
|
-
"diff"
|
|
17090
|
+
"diff",
|
|
17091
|
+
"verify"
|
|
16916
17092
|
]
|
|
16917
17093
|
},
|
|
16918
17094
|
{
|
|
@@ -19246,7 +19422,7 @@ const { runScan, buildUniversalRules, fallbackWorkspace, pathMatchesGlob, isVali
|
|
|
19246
19422
|
* `scripts/sync-sarif-version.cjs` on release and guarded by
|
|
19247
19423
|
* `tests/version-consistency.spec.ts` locally.
|
|
19248
19424
|
*/
|
|
19249
|
-
const CLI_VERSION = "0.5.
|
|
19425
|
+
const CLI_VERSION = "0.5.38";
|
|
19250
19426
|
function parseArgs(argv, onError) {
|
|
19251
19427
|
const args = {
|
|
19252
19428
|
target: ".",
|
|
@@ -20004,6 +20180,36 @@ async function runBaselineCommand(argv, io = {
|
|
|
20004
20180
|
}
|
|
20005
20181
|
}
|
|
20006
20182
|
/** Testable `diff` handler (Sprint 6 Task 24) — new/worsened debt only. */
|
|
20183
|
+
/** Testable `verify` handler — the agent-loop verb (master plan P7,
|
|
20184
|
+
* plan 1788853205786). Scans the target and diffs against the committed
|
|
20185
|
+
* baseline, rendering the before/after digest the agent loop consumes
|
|
20186
|
+
* (resolved per §15 · new · unchanged by ruleId+location · score delta).
|
|
20187
|
+
* Exit semantics are the frozen contract: 0 clean · 1 new error
|
|
20188
|
+
* findings · 2 partial scan or no baseline · 20 internal. */
|
|
20189
|
+
async function runVerifyCommand(argv, io = {
|
|
20190
|
+
out,
|
|
20191
|
+
err
|
|
20192
|
+
}) {
|
|
20193
|
+
const args = parseArgsOrUsage(argv, io);
|
|
20194
|
+
if (!args) return 10;
|
|
20195
|
+
try {
|
|
20196
|
+
const target = resolve(args.target);
|
|
20197
|
+
const invalid = validateScanTarget(target, io.err);
|
|
20198
|
+
if (invalid !== null) return invalid;
|
|
20199
|
+
const result = await runScan({
|
|
20200
|
+
...args,
|
|
20201
|
+
target
|
|
20202
|
+
});
|
|
20203
|
+
const digest = buildVerifyDigest(result, loadBaseline(join(target, DEFAULT_BASELINE_PATH), (w) => io.err(w)));
|
|
20204
|
+
io.out(renderVerifyDigest(digest));
|
|
20205
|
+
if (result.partial) return 2;
|
|
20206
|
+
if (!digest.hasBaseline) return 2;
|
|
20207
|
+
return digest.new.some((f) => f.severity === "error") ? 1 : 0;
|
|
20208
|
+
} catch (err) {
|
|
20209
|
+
internalErrorMessage(err, io.err, args?.debug === true);
|
|
20210
|
+
return 20;
|
|
20211
|
+
}
|
|
20212
|
+
}
|
|
20007
20213
|
async function runDiffCommand(argv, io = {
|
|
20008
20214
|
out,
|
|
20009
20215
|
err
|
|
@@ -20207,6 +20413,7 @@ async function main(argv = process$1.argv.slice(2), io = {
|
|
|
20207
20413
|
if (argv[0] === "impact") return runImpactCommand(argv.slice(1));
|
|
20208
20414
|
if (argv[0] === "baseline") return runBaselineCommand(argv.slice(1));
|
|
20209
20415
|
if (argv[0] === "diff") return runDiffCommand(argv.slice(1));
|
|
20416
|
+
if (argv[0] === "verify") return runVerifyCommand(argv.slice(1), io);
|
|
20210
20417
|
if (argv[0] === "pr-comment") return runPrCommentCommand(argv.slice(1));
|
|
20211
20418
|
if (argv[0] === "summary") return runSummaryCommand(argv.slice(1), io);
|
|
20212
20419
|
if (argv[0] === "stats") return runStatsCommand(argv.slice(1));
|
|
@@ -20278,4 +20485,4 @@ function isEntryPoint() {
|
|
|
20278
20485
|
}
|
|
20279
20486
|
if (isEntryPoint()) process$1.exitCode = await main();
|
|
20280
20487
|
//#endregion
|
|
20281
|
-
export { CLI_VERSION, EVIDENCE_OVERRIDES, KNOWN_RULE_IDS, OVERLAP_META_BY_RULE_ID, SUITE_INVALIDATING_RULE_IDS, buildUniversalRules, discoverRuntimeReport, err, exitForFindings, fallbackWorkspace, internalErrorMessage, isEntryPoint, isValidFindingRecord, levenshtein, main, nearestFlags, out, parseArgs, pathMatchesGlob, runBadgeCommand, runBaselineCommand, runCiInstall, runCreateRuleCommand, runDebtCommand, runDiffCommand, runDoctorCommand, runDoctorPlaywright, runExplainCommand, runFixCommand, runForensicsCommand, runHandoverCommand, runHelpCommand, runImpactCommand, runInitCommand, runMutationCommand, runPrCommentCommand, runPwReportCommand, runRulesCommand, runScan, runScanCommand, runStatsCommand, runSuppressions, runTriageCommand, usageErrorMessage };
|
|
20488
|
+
export { CLI_VERSION, EVIDENCE_OVERRIDES, KNOWN_RULE_IDS, OVERLAP_META_BY_RULE_ID, SUITE_INVALIDATING_RULE_IDS, buildUniversalRules, discoverRuntimeReport, err, exitForFindings, fallbackWorkspace, internalErrorMessage, isEntryPoint, isValidFindingRecord, levenshtein, main, nearestFlags, out, parseArgs, pathMatchesGlob, runBadgeCommand, runBaselineCommand, runCiInstall, runCreateRuleCommand, runDebtCommand, runDiffCommand, runDoctorCommand, runDoctorPlaywright, runExplainCommand, runFixCommand, runForensicsCommand, runHandoverCommand, runHelpCommand, runImpactCommand, runInitCommand, runMutationCommand, runPrCommentCommand, runPwReportCommand, runRulesCommand, runScan, runScanCommand, runStatsCommand, runSuppressions, runTriageCommand, runVerifyCommand, usageErrorMessage };
|
package/dist/mcp/stdio.mjs
CHANGED
|
@@ -13463,7 +13463,7 @@ function renderSarif(result, repoRootUri) {
|
|
|
13463
13463
|
tool: { driver: {
|
|
13464
13464
|
name: "Mjölnir",
|
|
13465
13465
|
informationUri: "https://github.com/Sergey-Bar/Mjolnir",
|
|
13466
|
-
version: "0.5.
|
|
13466
|
+
version: "0.5.38",
|
|
13467
13467
|
rules: [...rules.values()].map((r) => {
|
|
13468
13468
|
const meta = RULES.find((x) => x.id === r.id);
|
|
13469
13469
|
return {
|
|
@@ -14696,6 +14696,7 @@ function agentBrief(surface, cwdNote) {
|
|
|
14696
14696
|
"- Exit 0 = clean. Exit 1 = new findings on changed lines: fix them (smallest behavior-preserving change) or justify them to the user.",
|
|
14697
14697
|
`- npx mjolnir-qa@${CLI_VERSION} why <file>:<line> explains any finding (evidence level, measured FP rate, fix).`,
|
|
14698
14698
|
`- npx mjolnir-qa@${CLI_VERSION} handoff renders a full remediation plan from a saved --json report.`,
|
|
14699
|
+
`- Agent loop: establish the before-state once with npx mjolnir-qa@${CLI_VERSION} baseline (commits .mjolnir/baseline.json); after fixing, npx mjolnir-qa@${CLI_VERSION} verify prints the before/after digest — resolved (per §15 lifecycle) / new / unchanged by ruleId+location, and the score delta. Exit 0 clean, 1 new errors, 2 partial or no baseline.`,
|
|
14699
14700
|
"- NEVER suppress a finding merely to obtain a green scan (suppressions live in mjolnir.config.json, require a reason, and expire after 90 days).",
|
|
14700
14701
|
"- After fixing: re-run the scan, report files changed, report checks not run, report unresolved findings honestly.",
|
|
14701
14702
|
"",
|
|
@@ -15860,10 +15861,16 @@ const HELP_ENTRIES = [
|
|
|
15860
15861
|
},
|
|
15861
15862
|
{
|
|
15862
15863
|
verb: "diff",
|
|
15863
|
-
summary: "
|
|
15864
|
-
usage: "mjolnir diff [path]",
|
|
15864
|
+
summary: "lifecycle diff vs the committed baseline",
|
|
15865
|
+
usage: "mjolnir diff [path] [--json] [scan flags]",
|
|
15865
15866
|
examples: ["mjolnir diff"]
|
|
15866
15867
|
},
|
|
15868
|
+
{
|
|
15869
|
+
verb: "verify",
|
|
15870
|
+
summary: "agent-loop digest: resolved/new/unchanged vs baseline + score delta",
|
|
15871
|
+
usage: "mjolnir verify [path] [--json] [scan flags]",
|
|
15872
|
+
examples: ["mjolnir verify"]
|
|
15873
|
+
},
|
|
15867
15874
|
{
|
|
15868
15875
|
verb: "impact",
|
|
15869
15876
|
summary: "what a commit introduced vs resolved, since a prior commit",
|
|
@@ -15950,7 +15957,7 @@ const HELP_ENTRIES = [
|
|
|
15950
15957
|
},
|
|
15951
15958
|
{
|
|
15952
15959
|
verb: "mcp",
|
|
15953
|
-
summary: "run as an MCP server over stdio (scan / explain / diff tools)",
|
|
15960
|
+
summary: "run as an MCP server over stdio (scan / explain / diff / verify tools)",
|
|
15954
15961
|
usage: "mjolnir mcp",
|
|
15955
15962
|
examples: ["mjolnir mcp"]
|
|
15956
15963
|
}
|
|
@@ -16103,7 +16110,8 @@ const GROUPS = [
|
|
|
16103
16110
|
"badge",
|
|
16104
16111
|
"impact",
|
|
16105
16112
|
"baseline",
|
|
16106
|
-
"diff"
|
|
16113
|
+
"diff",
|
|
16114
|
+
"verify"
|
|
16107
16115
|
]
|
|
16108
16116
|
},
|
|
16109
16117
|
{
|
|
@@ -17089,6 +17097,113 @@ function renderBaselineDiff(diff) {
|
|
|
17089
17097
|
return lines.join("\n");
|
|
17090
17098
|
}
|
|
17091
17099
|
//#endregion
|
|
17100
|
+
//#region src/commands/verify.ts
|
|
17101
|
+
/**
|
|
17102
|
+
* `mjolnir verify` — the agent-loop digest (product-gap-remediation
|
|
17103
|
+
* master plan P7, plan 1788853205786 — flag agent, decision 6).
|
|
17104
|
+
*
|
|
17105
|
+
* An agent fixes findings, then runs `mjolnir verify` against the
|
|
17106
|
+
* committed baseline (established once with `mjolnir baseline`). The
|
|
17107
|
+
* digest answers exactly the loop's questions, derived from the SAME
|
|
17108
|
+
* §15 comparison machinery as `mjolnir diff` (no second truth):
|
|
17109
|
+
*
|
|
17110
|
+
* - RESOLVED — baseline findings no longer present, each with its
|
|
17111
|
+
* lifecycle resolution (only VERIFIED-RESOLVED is a fix claim);
|
|
17112
|
+
* - NEW — findings the fix introduced;
|
|
17113
|
+
* - UNCHANGED — pre-existing debt, grouped by ruleId + location
|
|
17114
|
+
* (the agent-loop key: an agent works by location, not message);
|
|
17115
|
+
* - SCORE DELTA — before (baseline) → after (this scan).
|
|
17116
|
+
*
|
|
17117
|
+
* Exit semantics are the frozen contract: 0 clean · 1 new error
|
|
17118
|
+
* findings · 2 partial scan or no baseline (nothing to verify against
|
|
17119
|
+
* — the agent must first establish the before-state).
|
|
17120
|
+
*/
|
|
17121
|
+
function buildVerifyDigest(result, baseline) {
|
|
17122
|
+
const diff = diffAgainstBaseline(result, baseline);
|
|
17123
|
+
const digest = {
|
|
17124
|
+
hasBaseline: diff.hasBaseline,
|
|
17125
|
+
...diff.baselineCapturedAt !== void 0 ? { baselineCapturedAt: diff.baselineCapturedAt } : {},
|
|
17126
|
+
...diff.baselineCommit !== void 0 ? { baselineCommit: diff.baselineCommit } : {},
|
|
17127
|
+
resolved: diff.resolvedFindings.map((f) => ({
|
|
17128
|
+
ruleId: f.ruleId,
|
|
17129
|
+
file: f.file,
|
|
17130
|
+
resolution: f.resolution.status
|
|
17131
|
+
})),
|
|
17132
|
+
new: diff.newFindings.map((f) => ({
|
|
17133
|
+
ruleId: f.ruleId,
|
|
17134
|
+
file: f.file,
|
|
17135
|
+
line: f.line,
|
|
17136
|
+
severity: f.severity
|
|
17137
|
+
})),
|
|
17138
|
+
unchanged: [],
|
|
17139
|
+
unchangedCount: diff.unchangedCount,
|
|
17140
|
+
scoreBefore: diff.baselineScore ?? null,
|
|
17141
|
+
scoreAfter: result.score ?? null,
|
|
17142
|
+
scoreDelta: diff.baselineScore !== void 0 && result.score !== null ? result.score - diff.baselineScore : null
|
|
17143
|
+
};
|
|
17144
|
+
if (diff.hasBaseline && baseline) {
|
|
17145
|
+
const byRule = /* @__PURE__ */ new Map();
|
|
17146
|
+
const headByFingerprint = /* @__PURE__ */ new Map();
|
|
17147
|
+
for (const f of result.findings) headByFingerprint.set(`${f.ruleId}\u0000${f.file}\u0000${f.message}`, f);
|
|
17148
|
+
for (const b of baseline.findings) {
|
|
17149
|
+
const head = headByFingerprint.get(`${b.ruleId}\u0000${b.file}\u0000${b.message}`);
|
|
17150
|
+
if (!head) continue;
|
|
17151
|
+
let group = byRule.get(b.ruleId);
|
|
17152
|
+
if (!group) {
|
|
17153
|
+
group = {
|
|
17154
|
+
ruleId: b.ruleId,
|
|
17155
|
+
locations: []
|
|
17156
|
+
};
|
|
17157
|
+
byRule.set(b.ruleId, group);
|
|
17158
|
+
}
|
|
17159
|
+
group.locations.push(`${head.file}:${head.line}`);
|
|
17160
|
+
}
|
|
17161
|
+
digest.unchanged = [...byRule.values()].map((g) => ({
|
|
17162
|
+
ruleId: g.ruleId,
|
|
17163
|
+
locations: g.locations.sort()
|
|
17164
|
+
}));
|
|
17165
|
+
}
|
|
17166
|
+
return digest;
|
|
17167
|
+
}
|
|
17168
|
+
function renderVerifyDigest(digest) {
|
|
17169
|
+
if (!digest.hasBaseline) return [
|
|
17170
|
+
"VERIFY — no baseline",
|
|
17171
|
+
"",
|
|
17172
|
+
"No committed baseline at .mjolnir/baseline.json — nothing to verify",
|
|
17173
|
+
"against. Establish the before-state once with `mjolnir baseline`,",
|
|
17174
|
+
"then re-run `mjolnir verify` after the fix."
|
|
17175
|
+
].join("\n");
|
|
17176
|
+
const lines = [];
|
|
17177
|
+
lines.push("VERIFY — before/after digest");
|
|
17178
|
+
lines.push("");
|
|
17179
|
+
if (digest.scoreDelta !== null) {
|
|
17180
|
+
const dir = digest.scoreDelta > 0 ? "improved" : digest.scoreDelta < 0 ? "worse" : "unchanged";
|
|
17181
|
+
lines.push(`score: ${digest.scoreBefore} → ${digest.scoreAfter} (${dir}, Δ${digest.scoreDelta >= 0 ? "+" : ""}${digest.scoreDelta})`);
|
|
17182
|
+
}
|
|
17183
|
+
lines.push(`${digest.resolved.length} resolved · ${digest.new.length} new · ${digest.unchangedCount} unchanged`);
|
|
17184
|
+
lines.push("");
|
|
17185
|
+
if (digest.resolved.length > 0) {
|
|
17186
|
+
lines.push("RESOLVED (per §15 lifecycle — only VERIFIED-RESOLVED is a fix claim):");
|
|
17187
|
+
for (const r of digest.resolved) lines.push(` ${r.ruleId} ${r.file} — ${r.resolution}`);
|
|
17188
|
+
lines.push("");
|
|
17189
|
+
}
|
|
17190
|
+
if (digest.new.length > 0) {
|
|
17191
|
+
lines.push("NEW (introduced by the change under verification):");
|
|
17192
|
+
for (const n of digest.new) lines.push(` ${n.ruleId} ${n.file}:${n.line} (${n.severity})`);
|
|
17193
|
+
lines.push("");
|
|
17194
|
+
}
|
|
17195
|
+
if (digest.unchanged.length > 0) {
|
|
17196
|
+
lines.push("UNCHANGED (pre-existing debt — out of this fix's scope):");
|
|
17197
|
+
for (const g of digest.unchanged) {
|
|
17198
|
+
lines.push(` ${g.ruleId} × ${g.locations.length}`);
|
|
17199
|
+
for (const loc of g.locations.slice(0, 5)) lines.push(` ${loc}`);
|
|
17200
|
+
if (g.locations.length > 5) lines.push(` … and ${g.locations.length - 5} more`);
|
|
17201
|
+
}
|
|
17202
|
+
lines.push("");
|
|
17203
|
+
}
|
|
17204
|
+
return lines.join("\n").trimEnd();
|
|
17205
|
+
}
|
|
17206
|
+
//#endregion
|
|
17092
17207
|
//#region src/commands/stats.ts
|
|
17093
17208
|
/**
|
|
17094
17209
|
* `mjolnir stats` — Sprint 6 Task 26 (Master-Stabilization-Plan.md).
|
|
@@ -18882,7 +18997,7 @@ const { runScan, buildUniversalRules, fallbackWorkspace, pathMatchesGlob, isVali
|
|
|
18882
18997
|
* `scripts/sync-sarif-version.cjs` on release and guarded by
|
|
18883
18998
|
* `tests/version-consistency.spec.ts` locally.
|
|
18884
18999
|
*/
|
|
18885
|
-
const CLI_VERSION = "0.5.
|
|
19000
|
+
const CLI_VERSION = "0.5.38";
|
|
18886
19001
|
function parseArgs(argv, onError) {
|
|
18887
19002
|
const args = {
|
|
18888
19003
|
target: ".",
|
|
@@ -19640,6 +19755,36 @@ async function runBaselineCommand(argv, io = {
|
|
|
19640
19755
|
}
|
|
19641
19756
|
}
|
|
19642
19757
|
/** Testable `diff` handler (Sprint 6 Task 24) — new/worsened debt only. */
|
|
19758
|
+
/** Testable `verify` handler — the agent-loop verb (master plan P7,
|
|
19759
|
+
* plan 1788853205786). Scans the target and diffs against the committed
|
|
19760
|
+
* baseline, rendering the before/after digest the agent loop consumes
|
|
19761
|
+
* (resolved per §15 · new · unchanged by ruleId+location · score delta).
|
|
19762
|
+
* Exit semantics are the frozen contract: 0 clean · 1 new error
|
|
19763
|
+
* findings · 2 partial scan or no baseline · 20 internal. */
|
|
19764
|
+
async function runVerifyCommand(argv, io = {
|
|
19765
|
+
out,
|
|
19766
|
+
err
|
|
19767
|
+
}) {
|
|
19768
|
+
const args = parseArgsOrUsage(argv, io);
|
|
19769
|
+
if (!args) return 10;
|
|
19770
|
+
try {
|
|
19771
|
+
const target = resolve(args.target);
|
|
19772
|
+
const invalid = validateScanTarget(target, io.err);
|
|
19773
|
+
if (invalid !== null) return invalid;
|
|
19774
|
+
const result = await runScan({
|
|
19775
|
+
...args,
|
|
19776
|
+
target
|
|
19777
|
+
});
|
|
19778
|
+
const digest = buildVerifyDigest(result, loadBaseline(join(target, DEFAULT_BASELINE_PATH), (w) => io.err(w)));
|
|
19779
|
+
io.out(renderVerifyDigest(digest));
|
|
19780
|
+
if (result.partial) return 2;
|
|
19781
|
+
if (!digest.hasBaseline) return 2;
|
|
19782
|
+
return digest.new.some((f) => f.severity === "error") ? 1 : 0;
|
|
19783
|
+
} catch (err) {
|
|
19784
|
+
internalErrorMessage(err, io.err, args?.debug === true);
|
|
19785
|
+
return 20;
|
|
19786
|
+
}
|
|
19787
|
+
}
|
|
19643
19788
|
async function runDiffCommand(argv, io = {
|
|
19644
19789
|
out,
|
|
19645
19790
|
err
|
|
@@ -19843,6 +19988,7 @@ async function main(argv = process$1.argv.slice(2), io = {
|
|
|
19843
19988
|
if (argv[0] === "impact") return runImpactCommand(argv.slice(1));
|
|
19844
19989
|
if (argv[0] === "baseline") return runBaselineCommand(argv.slice(1));
|
|
19845
19990
|
if (argv[0] === "diff") return runDiffCommand(argv.slice(1));
|
|
19991
|
+
if (argv[0] === "verify") return runVerifyCommand(argv.slice(1), io);
|
|
19846
19992
|
if (argv[0] === "pr-comment") return runPrCommentCommand(argv.slice(1));
|
|
19847
19993
|
if (argv[0] === "summary") return runSummaryCommand(argv.slice(1), io);
|
|
19848
19994
|
if (argv[0] === "stats") return runStatsCommand(argv.slice(1));
|
|
@@ -19975,6 +20121,18 @@ const MCP_TOOLS = [
|
|
|
19975
20121
|
},
|
|
19976
20122
|
required: ["path", "scanResult"]
|
|
19977
20123
|
}
|
|
20124
|
+
},
|
|
20125
|
+
{
|
|
20126
|
+
name: "verify",
|
|
20127
|
+
description: "Agent-loop digest (1:1 with `mjolnir verify`): scan the target and diff against the committed baseline. Returns resolved (per §15 lifecycle), new, unchanged (grouped by ruleId + location), and the score delta. Read-only; the same transport guardrails as every other tool.",
|
|
20128
|
+
inputSchema: {
|
|
20129
|
+
type: "object",
|
|
20130
|
+
properties: { path: {
|
|
20131
|
+
type: "string",
|
|
20132
|
+
description: "Directory to scan and verify."
|
|
20133
|
+
} },
|
|
20134
|
+
required: ["path"]
|
|
20135
|
+
}
|
|
19978
20136
|
}
|
|
19979
20137
|
];
|
|
19980
20138
|
/** Strict param validation — unknown/missing/mistyped ⇒ INVALID_PARAMS. */
|
|
@@ -19991,6 +20149,9 @@ function validateParams(name, args) {
|
|
|
19991
20149
|
if (typeof args["path"] !== "string" || args["path"].length === 0) return "diff requires a non-empty string `path`";
|
|
19992
20150
|
if (typeof args["scanResult"] !== "object" || args["scanResult"] === null) return "diff requires the `scanResult` object from a previous scan tool call";
|
|
19993
20151
|
}
|
|
20152
|
+
if (name === "verify") {
|
|
20153
|
+
if (typeof args["path"] !== "string" || args["path"].length === 0) return "verify requires a non-empty string `path`";
|
|
20154
|
+
}
|
|
19994
20155
|
return null;
|
|
19995
20156
|
}
|
|
19996
20157
|
/**
|
|
@@ -20107,6 +20268,52 @@ async function handleToolCall(call) {
|
|
|
20107
20268
|
}
|
|
20108
20269
|
};
|
|
20109
20270
|
}
|
|
20271
|
+
if (call.name === "verify") {
|
|
20272
|
+
const target = resolve(call.args["path"]);
|
|
20273
|
+
if (!existsSync(target)) return {
|
|
20274
|
+
jsonrpc: "2.0",
|
|
20275
|
+
id: call.id,
|
|
20276
|
+
error: {
|
|
20277
|
+
code: MCP_ERRORS.INVALID_PARAMS,
|
|
20278
|
+
message: `verify target does not exist: ${target}`
|
|
20279
|
+
}
|
|
20280
|
+
};
|
|
20281
|
+
const baselinePath = join(target, DEFAULT_BASELINE_PATH);
|
|
20282
|
+
if (!existsSync(baselinePath)) return {
|
|
20283
|
+
jsonrpc: "2.0",
|
|
20284
|
+
id: call.id,
|
|
20285
|
+
result: {
|
|
20286
|
+
hasBaseline: false,
|
|
20287
|
+
note: "no committed baseline at .mjolnir/baseline.json — establish the before-state with `mjolnir baseline` first"
|
|
20288
|
+
}
|
|
20289
|
+
};
|
|
20290
|
+
const result = await serializeScan(() => runScan({
|
|
20291
|
+
target,
|
|
20292
|
+
json: true,
|
|
20293
|
+
verbose: false,
|
|
20294
|
+
maxDurationMs: Number.POSITIVE_INFINITY,
|
|
20295
|
+
scopeChanged: false,
|
|
20296
|
+
format: "json"
|
|
20297
|
+
}));
|
|
20298
|
+
const digest = buildVerifyDigest(result, loadBaseline(baselinePath));
|
|
20299
|
+
return {
|
|
20300
|
+
jsonrpc: "2.0",
|
|
20301
|
+
id: call.id,
|
|
20302
|
+
result: {
|
|
20303
|
+
hasBaseline: digest.hasBaseline,
|
|
20304
|
+
baselineCapturedAt: digest.baselineCapturedAt,
|
|
20305
|
+
baselineCommit: digest.baselineCommit,
|
|
20306
|
+
resolved: digest.resolved,
|
|
20307
|
+
new: digest.new,
|
|
20308
|
+
unchanged: digest.unchanged,
|
|
20309
|
+
unchangedCount: digest.unchangedCount,
|
|
20310
|
+
scoreBefore: digest.scoreBefore,
|
|
20311
|
+
scoreAfter: digest.scoreAfter,
|
|
20312
|
+
scoreDelta: digest.scoreDelta,
|
|
20313
|
+
partial: result.partial
|
|
20314
|
+
}
|
|
20315
|
+
};
|
|
20316
|
+
}
|
|
20110
20317
|
return {
|
|
20111
20318
|
jsonrpc: "2.0",
|
|
20112
20319
|
id: call.id,
|
package/package.json
CHANGED