mjolnir-qa 6.0.1 → 6.1.0
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 +124 -0
- package/README.md +3 -3
- package/dist/cli.d.mts +12 -4
- package/dist/cli.mjs +148 -70
- package/dist/mcp/stdio.mjs +65 -32
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -11,6 +11,130 @@ once shipped, so this file is the record of what changed between versions.
|
|
|
11
11
|
|
|
12
12
|
## [Unreleased]
|
|
13
13
|
|
|
14
|
+
## [6.1.0] — 2026-10-28
|
|
15
|
+
|
|
16
|
+
A minor release from the second half of the maintenance review. Most of it
|
|
17
|
+
fixes behaviour that was wrong without saying so: report text that rendered as
|
|
18
|
+
escaped markup, warnings that printed twice or not at all, reads that failed
|
|
19
|
+
and were counted as successes, and release workflows that could publish when
|
|
20
|
+
told not to. No verb, flag, Action input or MCP tool was added or removed.
|
|
21
|
+
|
|
22
|
+
### Upgrade notes
|
|
23
|
+
|
|
24
|
+
Two config checks are now strict and exit 10 with the entry named. Both
|
|
25
|
+
configurations used to load and then silently do nothing, so a config that
|
|
26
|
+
fails here was already not doing what it said.
|
|
27
|
+
|
|
28
|
+
- `ignore[].expires` must be a calendar date, `YYYY-MM-DD`. A date-time
|
|
29
|
+
(`2026-09-01T00:00:00Z`) or an unpadded date (`2026-9-1`) left the
|
|
30
|
+
suppression inactive; write the date alone.
|
|
31
|
+
- `plugins` must be an array of package names or `{ "package": "…" }`
|
|
32
|
+
objects. An object such as `{ "acme": {} }` loaded no plugin; write
|
|
33
|
+
`["acme"]`.
|
|
34
|
+
|
|
35
|
+
### Fixed: the scheduled OSV scan runs again
|
|
36
|
+
|
|
37
|
+
- **OSV-Scanner 2.7.0 removed `--skip-git`**, so the push and scheduled scans
|
|
38
|
+
failed before scanning. The flag is gone; pull-request scans never passed it.
|
|
39
|
+
|
|
40
|
+
### Fixed: flag suggestions name only flags that exist
|
|
41
|
+
|
|
42
|
+
- **`mjolnir scan` suggests only flags it accepts.** `--dry-run` and
|
|
43
|
+
`--version` were offered as "Did you mean" right after being rejected, and
|
|
44
|
+
ten accepted flags (`--staged`, `--blocking`, `--enable-plugins`, …) were
|
|
45
|
+
never suggested. A spec now reads the parser to hold the two lists together.
|
|
46
|
+
- **Dead code removed:** the `ci install` line that shadowed the `ci`
|
|
47
|
+
subcommand table, an unreachable `ci --help` arm, an `existsSync` given a
|
|
48
|
+
glob it cannot expand, and two copies of the scan time-budget constants,
|
|
49
|
+
which the CLI and the MCP server now import from the pipeline that applies
|
|
50
|
+
them.
|
|
51
|
+
|
|
52
|
+
### Fixed: CI and release workflows
|
|
53
|
+
|
|
54
|
+
- **A stable release publishes to `latest` directly** (`release.yml`). It
|
|
55
|
+
published to `next` and relied on a token-based repair to move `latest`.
|
|
56
|
+
- **A stable-release resume honours `dry_run` and requires
|
|
57
|
+
`STABLE_RELEASE_AUTHORIZED`.** It ignored both, so a resume dispatched with
|
|
58
|
+
the default `dry_run: true` published, including a dry run's artifact.
|
|
59
|
+
- **The npm propagation wait polls `latest` as well as the version, for up to
|
|
60
|
+
ten minutes, and fails by name.** 6.0.1 took over five minutes to appear,
|
|
61
|
+
and the job timed out.
|
|
62
|
+
- **The Action major-tag workflow moves `vN` to the newest stable `vN.x.y`.**
|
|
63
|
+
A manual dispatch tagged the head of the dispatching branch, and an older
|
|
64
|
+
patch tag could move `vN` backwards.
|
|
65
|
+
- **The dogfood PR comment posts only on same-repository pull requests.** On
|
|
66
|
+
a fork or a manual dispatch the job failed with 403.
|
|
67
|
+
- `github.base_ref` reaches the shell through `env:` rather than script
|
|
68
|
+
interpolation. The dead badge step and a duplicate `check-version` call are
|
|
69
|
+
gone, and the nightly stress run no longer starts at the same minute as
|
|
70
|
+
fuzz.
|
|
71
|
+
- **`ci install` treats a Dependabot action-pin bump as not a
|
|
72
|
+
customisation.** Only the Mjölnir Action's SHA was ignored, so each bump of
|
|
73
|
+
`actions/*` made every workflow the previous release generated read as
|
|
74
|
+
hand-edited. The template's pins follow `mjolnir.yml` again.
|
|
75
|
+
- Action pins bumped: `setup-node` 7.1.0, `upload-artifact` 7.0.2,
|
|
76
|
+
`download-artifact` 8.0.2, CodeQL 4.39.1, OSV-Scanner 2.7.0, Scorecard
|
|
77
|
+
2.5.0. The `web-tree-sitter` 0.25.10 bump is not taken: the
|
|
78
|
+
`package-smoke` spec holds its exact 0.25.6 pin.
|
|
79
|
+
|
|
80
|
+
### Fixed: report rendering, config validation and swallowed errors
|
|
81
|
+
|
|
82
|
+
- **Paths and code-like fixes in the PR comment, step summary and handoff are
|
|
83
|
+
real code spans.** Backslash escapes are literal inside a code span, so
|
|
84
|
+
`__snapshots__` rendered as `\_\_snapshots\_\_`, and a backtick in a path
|
|
85
|
+
closed the span early. The fence is now longer than any backtick run inside.
|
|
86
|
+
- **The INCOMPLETE banner on a complete scan with no runtime evidence says
|
|
87
|
+
so**, and the quarantine coverage note is written out in words. The banner
|
|
88
|
+
used to name "an unreported reason".
|
|
89
|
+
- **`ignore[].expires` must be a calendar date, `YYYY-MM-DD` (exit 10
|
|
90
|
+
otherwise).** A date-time or `2026-9-1` loaded, then left the suppression
|
|
91
|
+
silently inactive with no expired warning, and `2026-02-30` quietly meant
|
|
92
|
+
March 2. The expired warning now uses the same check as
|
|
93
|
+
`mjolnir scan --suppressions`.
|
|
94
|
+
- **`plugins` must be an array of package names or `{ "package": … }`
|
|
95
|
+
objects (exit 10 otherwise).** An object, or an entry of another shape,
|
|
96
|
+
loaded cleanly and the loader ran nothing.
|
|
97
|
+
- **Config warnings print once.** `scan` printed each one twice, and `fix`,
|
|
98
|
+
`ci verify` and `handover` dropped them. Those three now also send the
|
|
99
|
+
plugin gate notice to their own stderr.
|
|
100
|
+
- **MCP `explain` finds the rule's fixture from a checkout**, defaulting to
|
|
101
|
+
`<cwd>/tests/fixtures` as `mjolnir explain` does. It never returned the
|
|
102
|
+
example finding the CLI prints.
|
|
103
|
+
- **Runtime corroboration matches Windows report paths whose case differs
|
|
104
|
+
from the scan root** (`c:\repo` and `C:\Repo`). Each verdict missed its
|
|
105
|
+
finding.
|
|
106
|
+
- **`--changed` marks an unreadable untracked file as fully changed** instead
|
|
107
|
+
of building a million-entry line set for it, which also dropped any finding
|
|
108
|
+
past line 1,000,000.
|
|
109
|
+
- **Swallowed reads are counted:** an unreadable `pom.xml`, `build.gradle` or
|
|
110
|
+
`.csproj` (`java-manifest-unreadable`, `csharp-project-unreadable`) and an
|
|
111
|
+
unreadable rule source file in the detector fingerprint
|
|
112
|
+
(`rules-tree-file-unreadable`). The evidence store's audit fails on an
|
|
113
|
+
unreadable records directory instead of reporting an empty, intact store,
|
|
114
|
+
and `forensics` says why `FLAKY.md` could not be written.
|
|
115
|
+
- **Census version majors sort numerically without the ambient locale**, and
|
|
116
|
+
the deterministic-ordering contract now also catches a `localeCompare`
|
|
117
|
+
whose locale argument is `undefined`.
|
|
118
|
+
|
|
119
|
+
### Fixed: the stable release publishes the artifact it verified
|
|
120
|
+
|
|
121
|
+
- **The first 6.0.1 publish run pushed `v6.0.1` and moved `v6`, then failed
|
|
122
|
+
before `npm publish`.** The verified artifact holds `release-artifact/` and
|
|
123
|
+
`release-assets/`, so `upload-artifact` keeps both directories, and the
|
|
124
|
+
publish job downloaded it into `release-artifact`, one level too deep:
|
|
125
|
+
`sha256sum: release-artifact/mjolnir-qa-6.0.1.tgz: No such file or
|
|
126
|
+
directory`. The GitHub-release job and both resume jobs read the same wrong
|
|
127
|
+
layout. All four now download into the workspace root, and a spec reads the
|
|
128
|
+
upload paths and holds every consumer to them.
|
|
129
|
+
- **The resumed GitHub release attaches the release-trust record**, which the
|
|
130
|
+
first-attempt release already required.
|
|
131
|
+
- **Both publish jobs read no `.npmrc`**, as `release.yml` does, so the
|
|
132
|
+
placeholder token `setup-node` writes there cannot stand in for OIDC trusted
|
|
133
|
+
publishing.
|
|
134
|
+
- **The SBOM checksum records a relative path**, like the tarball's. It
|
|
135
|
+
recorded the runner's absolute workspace path, and checked out only because
|
|
136
|
+
every job ran on the same path.
|
|
137
|
+
|
|
14
138
|
## [6.0.1] — 2026-10-28
|
|
15
139
|
|
|
16
140
|
A patch release. 6.0.0 shipped a GitHub Action whose report step calls a
|
package/README.md
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
npx mjolnir-qa@latest
|
|
20
20
|
```
|
|
21
21
|
|
|
22
|
-
Pin it for reproducible CI: `npx mjolnir-qa@6.0
|
|
22
|
+
Pin it for reproducible CI: `npx mjolnir-qa@6.1.0`. Requires **Node.js ≥ 22.18** on Windows, macOS or Linux.
|
|
23
23
|
|
|
24
24
|
[**Interactive walkthrough**](https://sergey-bar.github.io/Mjolnir/) · [Docs](https://sergey-bar.github.io/Mjolnir/) · [CHANGELOG](CHANGELOG.md) · [Video](assets/video/mjolnir-demo.mp4)
|
|
25
25
|
|
|
@@ -29,7 +29,7 @@ Pin it for reproducible CI: `npx mjolnir-qa@6.0.1`. Requires **Node.js ≥ 22.18
|
|
|
29
29
|
|
|
30
30
|
## Release status
|
|
31
31
|
|
|
32
|
-
The published line is `6.0
|
|
32
|
+
The published line is `6.1.0`, and this working tree is that line. The version table is
|
|
33
33
|
[`docs/ROADMAP.yaml`](docs/ROADMAP.yaml). This document does not publish a tag or authorize a release.
|
|
34
34
|
|
|
35
35
|
---
|
|
@@ -431,7 +431,7 @@ Every finding in a handoff carries its boundary: **E2** = deterministic, apply f
|
|
|
431
431
|
|
|
432
432
|
### Status
|
|
433
433
|
|
|
434
|
-
**Version 6.0
|
|
434
|
+
**Version 6.1.0 published.** The JSON schema and the exit codes are frozen contracts. What comes next, with no invented dates:
|
|
435
435
|
[the public roadmap](https://sergey-bar.github.io/Mjolnir/reference/roadmap).
|
|
436
436
|
|
|
437
437
|
---
|
package/dist/cli.d.mts
CHANGED
|
@@ -1543,7 +1543,7 @@ declare function isValidFindingRecord$1(f: unknown): f is Omit<Finding, "ruleId"
|
|
|
1543
1543
|
* without threading a Project through every adapter.
|
|
1544
1544
|
*/
|
|
1545
1545
|
declare function runScan$1(args: CliArgs, hooks?: ScanHooks): Promise<ScanResult>;
|
|
1546
|
-
declare const ENGINE_VERSION = "6.0
|
|
1546
|
+
declare const ENGINE_VERSION = "6.1.0";
|
|
1547
1547
|
/**
|
|
1548
1548
|
* Shared process IO sinks (certification-audit Phase 5, G6): the variadic
|
|
1549
1549
|
* console sinks and the internal-error reporter lived in cli.ts, which
|
|
@@ -1753,7 +1753,7 @@ export declare function runTrustTrendCommand(argv: string[], io?: {
|
|
|
1753
1753
|
out: Output;
|
|
1754
1754
|
err: Output;
|
|
1755
1755
|
}): Promise<number>;
|
|
1756
|
-
export declare const runScan: typeof runScan$1, buildUniversalRules: typeof buildUniversalRules$1, fallbackWorkspace: typeof fallbackWorkspace$1, pathMatchesGlob: typeof pathMatchesGlob$1, isValidFindingRecord: typeof isValidFindingRecord$1, discoverAndParseRuntimeReport: typeof discoverAndParseRuntimeReport$1, KNOWN_RULE_IDS: ReadonlySet<string>, OVERLAP_META_BY_RULE_ID: ReadonlyMap<string, OverlapMeta>, EVIDENCE_OVERRIDES: ReadonlyMap<string, string>, SUITE_INVALIDATING_RULE_IDS: ReadonlySet<string
|
|
1756
|
+
export declare const runScan: typeof runScan$1, buildUniversalRules: typeof buildUniversalRules$1, fallbackWorkspace: typeof fallbackWorkspace$1, pathMatchesGlob: typeof pathMatchesGlob$1, isValidFindingRecord: typeof isValidFindingRecord$1, discoverAndParseRuntimeReport: typeof discoverAndParseRuntimeReport$1, KNOWN_RULE_IDS: ReadonlySet<string>, OVERLAP_META_BY_RULE_ID: ReadonlyMap<string, OverlapMeta>, EVIDENCE_OVERRIDES: ReadonlyMap<string, string>, SUITE_INVALIDATING_RULE_IDS: ReadonlySet<string>, DEFAULT_MAX_DURATION_MS: number, MAX_DURATION_MS: number;
|
|
1757
1757
|
/** A usage-error detail: the offending token, when one exists. */
|
|
1758
1758
|
export interface UsageErrorDetail {
|
|
1759
1759
|
/** The unknown flag or rejected value (e.g. `--nope`, `loud`). */
|
|
@@ -1761,9 +1761,17 @@ export interface UsageErrorDetail {
|
|
|
1761
1761
|
/** The flag whose value was rejected (`--tone` for `--tone loud`). */
|
|
1762
1762
|
flag?: string | undefined;
|
|
1763
1763
|
}
|
|
1764
|
-
export declare const DEFAULT_MAX_DURATION_MS = 600000;
|
|
1765
|
-
export declare const MAX_DURATION_MS = 3600000;
|
|
1766
1764
|
export declare function parseArgs(argv: string[], onError?: (detail: UsageErrorDetail) => void): CliArgs | null;
|
|
1765
|
+
/**
|
|
1766
|
+
* Scan flags that exist — the "did you mean" candidate pool.
|
|
1767
|
+
*
|
|
1768
|
+
* Every flag `parseArgs` accepts, and nothing it rejects. The pool had
|
|
1769
|
+
* drifted both ways: ten accepted flags were missing, and `--dry-run` and
|
|
1770
|
+
* `--version` were in it although a scan rejects them, so `mjolnir scan
|
|
1771
|
+
* --dry-run` answered `unknown flag "--dry-run"` and then suggested it.
|
|
1772
|
+
* tests/cli/help.spec.ts reads `parseArgs` to hold the two lists together.
|
|
1773
|
+
*/
|
|
1774
|
+
export declare const KNOWN_SCAN_FLAGS: string[];
|
|
1767
1775
|
/** Hand-rolled Levenshtein distance (plan M2: no new dependencies). */
|
|
1768
1776
|
export declare function levenshtein(a: string, b: string): number;
|
|
1769
1777
|
/** Nearest known flags within distance ≤ 2, nearest first. */
|
package/dist/cli.mjs
CHANGED
|
@@ -307,7 +307,8 @@ function stampRuntimeCorroboration(findings, report, scanRoot) {
|
|
|
307
307
|
let n = p.replace(/\\/g, "/");
|
|
308
308
|
if (scanRoot) {
|
|
309
309
|
const root = scanRoot.replace(/\\/g, "/").replace(/\/?$/, "/");
|
|
310
|
-
|
|
310
|
+
const head = n.slice(0, root.length);
|
|
311
|
+
if (/^[a-z]:\//i.test(root) ? head.toLowerCase() === root.toLowerCase() : head === root) n = n.slice(root.length);
|
|
311
312
|
}
|
|
312
313
|
return n.replace(/^\.\//, "").replace(/^\/+/, "");
|
|
313
314
|
};
|
|
@@ -2084,7 +2085,7 @@ function readCandidateBinding(root, manifestPath = CANDIDATE_MANIFEST_PATH) {
|
|
|
2084
2085
|
* guarded by the version-consistency
|
|
2085
2086
|
* spec. cli.ts re-exports this as CLI_VERSION.
|
|
2086
2087
|
*/
|
|
2087
|
-
const ENGINE_VERSION = "6.0
|
|
2088
|
+
const ENGINE_VERSION = "6.1.0";
|
|
2088
2089
|
/**
|
|
2089
2090
|
* Build identity — WHICH BUILD IS RUNNING, as distinct from WHICH RELEASE IS
|
|
2090
2091
|
* PUBLISHED.
|
|
@@ -2411,7 +2412,11 @@ function validateConfigSchema(config) {
|
|
|
2411
2412
|
}
|
|
2412
2413
|
}
|
|
2413
2414
|
if (cfg["plugins"] !== void 0) {
|
|
2414
|
-
if (cfg["plugins"]
|
|
2415
|
+
if (!Array.isArray(cfg["plugins"])) errors.push(`plugins must be an array of package names or { "package": "<name>" } objects, got ${typeof cfg["plugins"]}`);
|
|
2416
|
+
else for (let i = 0; i < cfg["plugins"].length; i++) {
|
|
2417
|
+
const entry = cfg["plugins"][i];
|
|
2418
|
+
if (!(typeof entry === "string" || entry !== null && typeof entry === "object" && !Array.isArray(entry) && typeof entry["package"] === "string")) errors.push(`plugins[${i}] must be a package name or { "package": "<name>" }, got ${JSON.stringify(entry)}`);
|
|
2419
|
+
}
|
|
2415
2420
|
}
|
|
2416
2421
|
return {
|
|
2417
2422
|
valid: errors.length === 0,
|
|
@@ -2476,7 +2481,7 @@ function loadConfig(root, options = {}) {
|
|
|
2476
2481
|
return {
|
|
2477
2482
|
config: parsed,
|
|
2478
2483
|
path: p,
|
|
2479
|
-
warnings: validate(parsed, options.knownRuleIds)
|
|
2484
|
+
warnings: validate(parsed, options.knownRuleIds, options.now, name)
|
|
2480
2485
|
};
|
|
2481
2486
|
} catch (err) {
|
|
2482
2487
|
const msg = err.message;
|
|
@@ -2489,7 +2494,7 @@ function loadConfig(root, options = {}) {
|
|
|
2489
2494
|
warnings: []
|
|
2490
2495
|
};
|
|
2491
2496
|
}
|
|
2492
|
-
function validate(cfg, knownRuleIds) {
|
|
2497
|
+
function validate(cfg, knownRuleIds, now = /* @__PURE__ */ new Date(), fileName = "mjolnir.config.json") {
|
|
2493
2498
|
const warnings = [];
|
|
2494
2499
|
const schemaResult = validateConfigSchema(cfg);
|
|
2495
2500
|
if (!schemaResult.valid) throw new Error(schemaResult.errors.join("; "));
|
|
@@ -2509,10 +2514,10 @@ function validate(cfg, knownRuleIds) {
|
|
|
2509
2514
|
if (!Array.isArray(ign.files)) throw new Error(`ignore for ${ign.ruleId}: "files" must be an array of glob strings, got ${typeof ign.files}`);
|
|
2510
2515
|
for (const g of ign.files) if (typeof g !== "string") throw new Error(`ignore for ${ign.ruleId}: "files" entries must be strings, got ${typeof g} (${JSON.stringify(g)})`);
|
|
2511
2516
|
}
|
|
2512
|
-
if (ign.expires !== void 0 && Number.isNaN(
|
|
2513
|
-
if (ign.expires !== void 0 && !
|
|
2517
|
+
if (ign.expires !== void 0 && Number.isNaN(utcMidnight(ign.expires))) throw new Error(`ignore for ${ign.ruleId}: "expires" must be an ISO date (YYYY-MM-DD), got "${ign.expires}"`);
|
|
2518
|
+
if (ign.expires !== void 0 && !isSuppressionActive(ign, now)) warnings.push(`warning: ignore for ${ign.ruleId} has expired (expires "${ign.expires}") — the entry is kept but currently inactive.`);
|
|
2514
2519
|
}
|
|
2515
|
-
for (const key of Object.keys(cfg)) if (!KNOWN_CONFIG_KEYS.has(key)) warnings.push(`warning:
|
|
2520
|
+
for (const key of Object.keys(cfg)) if (!KNOWN_CONFIG_KEYS.has(key)) warnings.push(`warning: ${fileName} has an unknown top-level key "${key}" — ignored (typo? see README §Configuration).`);
|
|
2516
2521
|
if (cfg.exclude !== void 0) {
|
|
2517
2522
|
if (!Array.isArray(cfg.exclude)) throw new Error(`exclude must be an array of strings, got ${typeof cfg.exclude}`);
|
|
2518
2523
|
for (const p of cfg.exclude) if (typeof p !== "string") throw new Error(`exclude entries must be strings, got ${typeof p} (${JSON.stringify(p)})`);
|
|
@@ -2534,12 +2539,22 @@ function isSuppressionActive(ign, now = /* @__PURE__ */ new Date()) {
|
|
|
2534
2539
|
if (!ign.expires) return true;
|
|
2535
2540
|
return utcMidnight(ign.expires) > utcMidnightOf(now);
|
|
2536
2541
|
}
|
|
2537
|
-
/**
|
|
2542
|
+
/**
|
|
2543
|
+
* UTC-midnight timestamp of an ISO calendar date (YYYY-MM-DD), or NaN.
|
|
2544
|
+
*
|
|
2545
|
+
* NaN for anything that is not a real calendar day, so a load-time check and
|
|
2546
|
+
* the active/expired verdict share one parser and cannot disagree.
|
|
2547
|
+
*/
|
|
2538
2548
|
function utcMidnight(isoDate) {
|
|
2539
|
-
if (!/^\d{4}-\d{2}-\d{2}$/.test(isoDate)) return NaN;
|
|
2540
2549
|
const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(isoDate);
|
|
2541
2550
|
if (!m) return NaN;
|
|
2542
|
-
|
|
2551
|
+
const year = Number(m[1]);
|
|
2552
|
+
const month = Number(m[2]) - 1;
|
|
2553
|
+
const day = Number(m[3]);
|
|
2554
|
+
const t = Date.UTC(year, month, day);
|
|
2555
|
+
const back = new Date(t);
|
|
2556
|
+
if (back.getUTCFullYear() !== year || back.getUTCMonth() !== month || back.getUTCDate() !== day) return NaN;
|
|
2557
|
+
return t;
|
|
2543
2558
|
}
|
|
2544
2559
|
/** UTC-midnight timestamp of a Date (calendar day, timezone-independent). */
|
|
2545
2560
|
function utcMidnightOf(d) {
|
|
@@ -4464,7 +4479,9 @@ const javaAdapter = {
|
|
|
4464
4479
|
const artifact = /<artifactId>([^<]+)<\/artifactId>/.exec(block[0])?.[1] ?? "";
|
|
4465
4480
|
for (const { re, tag } of JAVA_ARTIFACT_TAGS) if (re.test(artifact)) frameworks.add(tag);
|
|
4466
4481
|
}
|
|
4467
|
-
} catch {
|
|
4482
|
+
} catch {
|
|
4483
|
+
recordDegradation("java-manifest-unreadable");
|
|
4484
|
+
}
|
|
4468
4485
|
for (const gradle of ["build.gradle", "build.gradle.kts"]) {
|
|
4469
4486
|
const path = join(root, gradle);
|
|
4470
4487
|
if (!existsSync(path)) continue;
|
|
@@ -4474,7 +4491,9 @@ const javaAdapter = {
|
|
|
4474
4491
|
const artifact = m[1].split(":")[1];
|
|
4475
4492
|
for (const { re, tag } of JAVA_ARTIFACT_TAGS) if (artifact !== void 0 && re.test(artifact)) frameworks.add(tag);
|
|
4476
4493
|
}
|
|
4477
|
-
} catch {
|
|
4494
|
+
} catch {
|
|
4495
|
+
recordDegradation("java-manifest-unreadable");
|
|
4496
|
+
}
|
|
4478
4497
|
}
|
|
4479
4498
|
if (frameworks.size === 0) return {
|
|
4480
4499
|
frameworks: [],
|
|
@@ -4632,9 +4651,13 @@ const csharpAdapter = {
|
|
|
4632
4651
|
const pkg = m[1];
|
|
4633
4652
|
for (const { re, tag } of CS_PACKAGE_TAGS) if (re.test(pkg)) frameworks.add(tag);
|
|
4634
4653
|
}
|
|
4635
|
-
} catch {
|
|
4654
|
+
} catch {
|
|
4655
|
+
recordDegradation("csharp-project-unreadable");
|
|
4656
|
+
}
|
|
4636
4657
|
}
|
|
4637
|
-
} catch {
|
|
4658
|
+
} catch {
|
|
4659
|
+
recordDegradation("csharp-project-unreadable");
|
|
4660
|
+
}
|
|
4638
4661
|
if (frameworks.size === 0) return {
|
|
4639
4662
|
frameworks: [],
|
|
4640
4663
|
unknown: true
|
|
@@ -5585,6 +5608,7 @@ function computeChangedScope(root, baseBranch) {
|
|
|
5585
5608
|
...untrackedSet
|
|
5586
5609
|
])].sort();
|
|
5587
5610
|
const changed = {};
|
|
5611
|
+
const fullyChanged = /* @__PURE__ */ new Set();
|
|
5588
5612
|
const batchDiff = (args, files) => {
|
|
5589
5613
|
const perFile = /* @__PURE__ */ new Map();
|
|
5590
5614
|
if (files.length === 0) return perFile;
|
|
@@ -5636,7 +5660,8 @@ function computeChangedScope(root, baseBranch) {
|
|
|
5636
5660
|
if (untrackedSet.has(file)) {
|
|
5637
5661
|
const all = allLinesOf(root, file);
|
|
5638
5662
|
if (all === null) {
|
|
5639
|
-
|
|
5663
|
+
fullyChanged.add(file);
|
|
5664
|
+
changed[file] = lines;
|
|
5640
5665
|
continue;
|
|
5641
5666
|
}
|
|
5642
5667
|
for (const l of all) lines.add(l);
|
|
@@ -5645,11 +5670,10 @@ function computeChangedScope(root, baseBranch) {
|
|
|
5645
5670
|
}
|
|
5646
5671
|
return {
|
|
5647
5672
|
changed,
|
|
5673
|
+
...fullyChanged.size > 0 ? { fullyChanged } : {},
|
|
5648
5674
|
degraded: false
|
|
5649
5675
|
};
|
|
5650
5676
|
}
|
|
5651
|
-
/** Sentinel size for "fully changed" — bounded, mirrors LIMITS.maxFileBytes scale. */
|
|
5652
|
-
const LIMITS_MAX_LINES = 1e6;
|
|
5653
5677
|
/** Every 1-based line number of an untracked file (it is all new). */
|
|
5654
5678
|
function allLinesOf(root, file) {
|
|
5655
5679
|
const full = join(root, file);
|
|
@@ -5699,6 +5723,7 @@ function parseChangedLines(diff) {
|
|
|
5699
5723
|
function filterToChanged(findings, diff) {
|
|
5700
5724
|
if (diff.degraded) return findings;
|
|
5701
5725
|
return findings.filter((f) => {
|
|
5726
|
+
if (diff.fullyChanged?.has(f.file)) return true;
|
|
5702
5727
|
const lines = diff.changed[f.file];
|
|
5703
5728
|
if (!lines) return false;
|
|
5704
5729
|
for (let l = Math.max(1, f.line - 3); l <= f.line; l++) if (lines.has(l)) return true;
|
|
@@ -14755,7 +14780,9 @@ function hashDir(dir, hash, depth) {
|
|
|
14755
14780
|
else if (entry.isFile() && /\.tsx?$/.test(entry.name)) try {
|
|
14756
14781
|
hash.update(entry.name);
|
|
14757
14782
|
hash.update(readFileSync(full));
|
|
14758
|
-
} catch {
|
|
14783
|
+
} catch {
|
|
14784
|
+
recordDegradation("rules-tree-file-unreadable");
|
|
14785
|
+
}
|
|
14759
14786
|
}
|
|
14760
14787
|
}
|
|
14761
14788
|
/**
|
|
@@ -16363,13 +16390,15 @@ function runForensics(target, options = {}) {
|
|
|
16363
16390
|
report.incompleteReasons = [.../* @__PURE__ */ new Set([...report.incompleteReasons, ...incompleteReasons])];
|
|
16364
16391
|
}
|
|
16365
16392
|
let flakyMdPath;
|
|
16393
|
+
let flakyMdWriteError;
|
|
16366
16394
|
if ((options.writeFlakyMd ?? true) && report.totalTests > 0) {
|
|
16367
16395
|
const base = stat.isFile() ? dirname(target) : target;
|
|
16368
16396
|
flakyMdPath = join(base, "FLAKY.md");
|
|
16369
16397
|
try {
|
|
16370
16398
|
writeFileAtomic(flakyMdPath, renderFlakyMd(report));
|
|
16371
|
-
} catch {
|
|
16399
|
+
} catch (e) {
|
|
16372
16400
|
flakyMdPath = void 0;
|
|
16401
|
+
flakyMdWriteError = e instanceof Error ? e.message : String(e);
|
|
16373
16402
|
}
|
|
16374
16403
|
}
|
|
16375
16404
|
return {
|
|
@@ -16377,7 +16406,7 @@ function runForensics(target, options = {}) {
|
|
|
16377
16406
|
output: [
|
|
16378
16407
|
renderLeaderboard(report),
|
|
16379
16408
|
"",
|
|
16380
|
-
flakyMdPath !== void 0 ? renderFlakyMdHint() : report.totalTests === 0 && (report.totalNetworkObservations ?? 0) > 0 && options.writeFlakyMd !== false ? "FLAKY.md was not written — network observations do not establish test outcomes." : renderFlakyMdNotWritten(options)
|
|
16409
|
+
flakyMdPath !== void 0 ? renderFlakyMdHint() : report.totalTests === 0 && (report.totalNetworkObservations ?? 0) > 0 && options.writeFlakyMd !== false ? "FLAKY.md was not written — network observations do not establish test outcomes." : renderFlakyMdNotWritten(options, flakyMdWriteError)
|
|
16381
16410
|
].join("\n"),
|
|
16382
16411
|
flakyMdPath
|
|
16383
16412
|
};
|
|
@@ -16386,9 +16415,10 @@ function renderFlakyMdHint() {
|
|
|
16386
16415
|
return "Full details in FLAKY.md (committed artifact).";
|
|
16387
16416
|
}
|
|
16388
16417
|
/** Honest fallback for every case where FLAKY.md was NOT written (M2). */
|
|
16389
|
-
function renderFlakyMdNotWritten(options) {
|
|
16418
|
+
function renderFlakyMdNotWritten(options, writeError) {
|
|
16390
16419
|
if (options.writeFlakyMd === false) return "FLAKY.md not written (--no-flaky-md).";
|
|
16391
|
-
return
|
|
16420
|
+
if (writeError !== void 0) return `FLAKY.md could not be written: ${writeError}`;
|
|
16421
|
+
return "FLAKY.md was not written — nothing recognized to report.";
|
|
16392
16422
|
}
|
|
16393
16423
|
/**
|
|
16394
16424
|
* R5: the trace artifact's test identity. The per-test trace convention
|
|
@@ -17012,8 +17042,10 @@ function renderGateNotice(skipped) {
|
|
|
17012
17042
|
* module moved. Re-exports in cli.ts keep the historical import surface.
|
|
17013
17043
|
*/
|
|
17014
17044
|
var scan_pipeline_exports = /* @__PURE__ */ __exportAll({
|
|
17045
|
+
DEFAULT_MAX_DURATION_MS: () => DEFAULT_MAX_DURATION_MS$1,
|
|
17015
17046
|
EVIDENCE_OVERRIDES: () => EVIDENCE_OVERRIDES$1,
|
|
17016
17047
|
KNOWN_RULE_IDS: () => KNOWN_RULE_IDS$1,
|
|
17048
|
+
MAX_DURATION_MS: () => MAX_DURATION_MS$1,
|
|
17017
17049
|
OVERLAP_META_BY_RULE_ID: () => OVERLAP_META_BY_RULE_ID$1,
|
|
17018
17050
|
SUITE_INVALIDATING_RULE_IDS: () => SUITE_INVALIDATING_RULE_IDS$1,
|
|
17019
17051
|
applyPostScanProcessing: () => applyPostScanProcessing,
|
|
@@ -17030,8 +17062,13 @@ var scan_pipeline_exports = /* @__PURE__ */ __exportAll({
|
|
|
17030
17062
|
summarizeForensicVerdicts: () => summarizeForensicVerdicts
|
|
17031
17063
|
});
|
|
17032
17064
|
const UNIVERSAL_RULES = RULES.map(asUniversal);
|
|
17033
|
-
|
|
17034
|
-
|
|
17065
|
+
/**
|
|
17066
|
+
* The scan time budget: the default, and the ceiling every surface enforces.
|
|
17067
|
+
* Owned here, where the clamp is applied; `cli.ts` and the MCP server import
|
|
17068
|
+
* these rather than keeping copies that only agree by coincidence.
|
|
17069
|
+
*/
|
|
17070
|
+
const DEFAULT_MAX_DURATION_MS$1 = 6e5;
|
|
17071
|
+
const MAX_DURATION_MS$1 = 36e5;
|
|
17035
17072
|
/** Registered rule IDs — used to warn on unknown severityOverrides keys (M4). */
|
|
17036
17073
|
const KNOWN_RULE_IDS$1 = new Set(RULES.map((r) => r.id));
|
|
17037
17074
|
/**
|
|
@@ -17786,8 +17823,8 @@ async function runScan$1(args, hooks = {}) {
|
|
|
17786
17823
|
async function runScanInProject(args, hooks = {}) {
|
|
17787
17824
|
const started = Date.now();
|
|
17788
17825
|
const degradationWindow = beginDegradationWindow();
|
|
17789
|
-
const requestedDuration = Number.isFinite(args.maxDurationMs) ? args.maxDurationMs : DEFAULT_MAX_DURATION_MS$
|
|
17790
|
-
const deadline = started + Math.min(Math.max(1, requestedDuration), MAX_DURATION_MS$
|
|
17826
|
+
const requestedDuration = Number.isFinite(args.maxDurationMs) ? args.maxDurationMs : DEFAULT_MAX_DURATION_MS$1;
|
|
17827
|
+
const deadline = started + Math.min(Math.max(1, requestedDuration), MAX_DURATION_MS$1);
|
|
17791
17828
|
const discovered = discoverWorkspace(args.target);
|
|
17792
17829
|
const targetAbs = resolve(args.target);
|
|
17793
17830
|
const scanRoot = discovered && discovered.root !== targetAbs && targetAbs.startsWith(discovered.root + sep) ? {
|
|
@@ -18632,10 +18669,14 @@ function isKnownTemplate(content) {
|
|
|
18632
18669
|
/**
|
|
18633
18670
|
* A generated workflow with the parts that legitimately change between
|
|
18634
18671
|
* releases blanked: line endings, the pinned package version (in the npx
|
|
18635
|
-
* tarball and in the Action's `version:` input) and
|
|
18672
|
+
* tarball and in the Action's `version:` input) and every pinned action SHA
|
|
18673
|
+
* with its version comment. Dependabot moves the `actions/*` pins between
|
|
18674
|
+
* releases, so blanking only the Mjölnir Action's SHA made each of those bumps
|
|
18675
|
+
* turn every workflow the previous release generated into one that reads as
|
|
18676
|
+
* hand-customised.
|
|
18636
18677
|
*/
|
|
18637
18678
|
function canonicalWorkflow(text) {
|
|
18638
|
-
return text.replace(/\r\n/g, "\n").replace(/mjolnir-qa-[0-9A-Za-z.+-]+?\.tgz/g, "mjolnir-qa-<version>.tgz").replace(/(\n {10}version: )"[^"\n]*"/g, "$1\"<version>\"").replace(/
|
|
18679
|
+
return text.replace(/\r\n/g, "\n").replace(/mjolnir-qa-[0-9A-Za-z.+-]+?\.tgz/g, "mjolnir-qa-<version>.tgz").replace(/(\n {10}version: )"[^"\n]*"/g, "$1\"<version>\"").replace(/(uses: [^\s@]+)@[0-9a-f]{40}[^\n]*/g, "$1@<sha>");
|
|
18639
18680
|
}
|
|
18640
18681
|
/**
|
|
18641
18682
|
* The action template through 6.0.0, which passed no `version:` and so ran
|
|
@@ -18699,7 +18740,7 @@ jobs:
|
|
|
18699
18740
|
if: always()
|
|
18700
18741
|
continue-on-error: true
|
|
18701
18742
|
run: npx --yes https://registry.npmjs.org/mjolnir-qa/-/mjolnir-qa-${INSTALL_VERSION}.tgz mjolnir.json --format pr-comment > mjolnir-comment.md
|
|
18702
|
-
- uses: actions/upload-artifact@
|
|
18743
|
+
- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
|
18703
18744
|
if: always()
|
|
18704
18745
|
with:
|
|
18705
18746
|
name: mjolnir-report
|
|
@@ -18724,7 +18765,7 @@ ${indentBlock(gateScript(gate), 10)}
|
|
|
18724
18765
|
contents: read
|
|
18725
18766
|
pull-requests: write
|
|
18726
18767
|
steps:
|
|
18727
|
-
- uses: actions/download-artifact@
|
|
18768
|
+
- uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
|
|
18728
18769
|
with:
|
|
18729
18770
|
name: mjolnir-report
|
|
18730
18771
|
- uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
|
@@ -18846,7 +18887,7 @@ jobs:
|
|
|
18846
18887
|
pr-comment: "false"
|
|
18847
18888
|
trust-artifact: "false"
|
|
18848
18889
|
annotations: "true"
|
|
18849
|
-
- uses: actions/upload-artifact@
|
|
18890
|
+
- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
|
18850
18891
|
if: always()
|
|
18851
18892
|
with:
|
|
18852
18893
|
name: mjolnir-report
|
|
@@ -18877,7 +18918,7 @@ jobs:
|
|
|
18877
18918
|
contents: read
|
|
18878
18919
|
pull-requests: write
|
|
18879
18920
|
steps:
|
|
18880
|
-
- uses: actions/download-artifact@
|
|
18921
|
+
- uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
|
|
18881
18922
|
with:
|
|
18882
18923
|
name: mjolnir-report
|
|
18883
18924
|
- uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
|
@@ -20013,8 +20054,6 @@ function renderPwRunSummary(s) {
|
|
|
20013
20054
|
*/
|
|
20014
20055
|
/** Hard caps (§21 threat model: parameter size + resource bounds). */
|
|
20015
20056
|
const MAX_PARAM_BYTES = 65536;
|
|
20016
|
-
const MAX_DURATION_MS$1 = 36e5;
|
|
20017
|
-
const DEFAULT_MAX_DURATION_MS$1 = 6e5;
|
|
20018
20057
|
const PROTOCOL_VERSION = "2025-06-18";
|
|
20019
20058
|
/** The real path of `p`, or its lexical resolution when it does not exist. */
|
|
20020
20059
|
function realOrResolved(p) {
|
|
@@ -20078,7 +20117,7 @@ const MCP_TOOLS = [
|
|
|
20078
20117
|
},
|
|
20079
20118
|
fixturesRoot: {
|
|
20080
20119
|
type: "string",
|
|
20081
|
-
description: "Root directory for fixture lookups (defaults to cwd)."
|
|
20120
|
+
description: "Root directory for fixture lookups (defaults to <cwd>/tests/fixtures, as for `mjolnir explain`)."
|
|
20082
20121
|
}
|
|
20083
20122
|
},
|
|
20084
20123
|
required: ["ruleId"]
|
|
@@ -20158,7 +20197,7 @@ function validateParams(name, args) {
|
|
|
20158
20197
|
if (typeof args["path"] !== "string" || args["path"].length === 0) return "scan requires a non-empty string `path`";
|
|
20159
20198
|
if (args["maxDurationMs"] !== void 0) {
|
|
20160
20199
|
const duration = args["maxDurationMs"];
|
|
20161
|
-
if (typeof duration !== "number" || !Number.isFinite(duration) || duration <= 0 || duration >
|
|
20200
|
+
if (typeof duration !== "number" || !Number.isFinite(duration) || duration <= 0 || duration > 36e5) return "`maxDurationMs` must be a finite positive number within the server budget";
|
|
20162
20201
|
}
|
|
20163
20202
|
}
|
|
20164
20203
|
if (name === "explain") {
|
|
@@ -20262,7 +20301,7 @@ async function handleToolCall(call) {
|
|
|
20262
20301
|
message: "fixturesRoot must be a string"
|
|
20263
20302
|
}
|
|
20264
20303
|
};
|
|
20265
|
-
const explanation = explainRule(ruleId, (typeof rawFixturesRoot === "string" ? rawFixturesRoot : void 0) ?? process.cwd());
|
|
20304
|
+
const explanation = explainRule(ruleId, (typeof rawFixturesRoot === "string" ? rawFixturesRoot : void 0) ?? join(process.cwd(), "tests", "fixtures"));
|
|
20266
20305
|
if (!explanation.ok) return {
|
|
20267
20306
|
jsonrpc: "2.0",
|
|
20268
20307
|
id: call.id,
|
|
@@ -22252,6 +22291,24 @@ const MAX_FINDINGS_PER_GROUP = 25;
|
|
|
22252
22291
|
function escMd(s) {
|
|
22253
22292
|
return sanitizeData(s).replace(/([\\`*_{}[\]()#+!|<>])/g, "\\$1");
|
|
22254
22293
|
}
|
|
22294
|
+
/**
|
|
22295
|
+
* A Markdown code span for untrusted text.
|
|
22296
|
+
*
|
|
22297
|
+
* Backslash escapes are literal inside a code span, so `escMd` there rendered
|
|
22298
|
+
* `__snapshots__` as `\_\_snapshots\_\_` and `app/(auth)` as `app/\(auth\)`.
|
|
22299
|
+
* A span cannot be broken out of with formatting characters, only with a
|
|
22300
|
+
* backtick run, so it is fenced with one backtick more than the longest run
|
|
22301
|
+
* inside (CommonMark 6.1) and padded where the text would otherwise touch the
|
|
22302
|
+
* fence. Kept to one line, because a newline would end the list item.
|
|
22303
|
+
*/
|
|
22304
|
+
function mdCodeSpan(raw) {
|
|
22305
|
+
const text = sanitizeData(raw).replace(/[\r\n]+/g, " ");
|
|
22306
|
+
if (text.trim() === "") return "` `";
|
|
22307
|
+
const longest = Math.max(0, ...[...text.matchAll(/`+/g)].map((m) => m[0].length));
|
|
22308
|
+
const fence = "`".repeat(longest + 1);
|
|
22309
|
+
const pad = text.startsWith("`") || text.endsWith("`") || text.startsWith(" ") && text.endsWith(" ") ? " " : "";
|
|
22310
|
+
return `${fence}${pad}${text}${pad}${fence}`;
|
|
22311
|
+
}
|
|
22255
22312
|
function looksLikeCode(s) {
|
|
22256
22313
|
return /[(;={]|await |expect\(/.test(s);
|
|
22257
22314
|
}
|
|
@@ -22266,10 +22323,8 @@ function evidenceTagLine(f) {
|
|
|
22266
22323
|
}
|
|
22267
22324
|
function findingLineMd(f) {
|
|
22268
22325
|
const icon = f.severity === "error" ? "🔴" : f.severity === "warning" ? "🟡" : "🔵";
|
|
22269
|
-
const
|
|
22270
|
-
|
|
22271
|
-
const safePath = escMd(f.file).replace(/[\r\n]+/g, " ");
|
|
22272
|
-
return `${icon} **${escMd(f.ruleId)}** \`${safePath}:${f.line}\` — ${escMd(f.message)} ${escMd(`[${evidenceTagLine(f)}]`)}\n Fix: ${fixBody}`;
|
|
22326
|
+
const fixBody = looksLikeCode(f.fix) ? mdCodeSpan(f.fix) : `_${escMd(f.fix)}_`;
|
|
22327
|
+
return `${icon} **${escMd(f.ruleId)}** ${mdCodeSpan(`${f.file}:${f.line}`)} — ${escMd(f.message)} ${escMd(`[${evidenceTagLine(f)}]`)}\n Fix: ${fixBody}`;
|
|
22273
22328
|
}
|
|
22274
22329
|
/**
|
|
22275
22330
|
* Reader-facing sentences for the `analysisStatus.reasons` tokens.
|
|
@@ -22300,7 +22355,8 @@ const REASON_PHRASES = [
|
|
|
22300
22355
|
["runtime-incomplete", "the runtime evidence set is incomplete"],
|
|
22301
22356
|
["identity-incomplete", "the run could not be bound to the tree it analysed"],
|
|
22302
22357
|
["degraded:", "capability was lost to a swallowed error"],
|
|
22303
|
-
["truncated:", "the scan was cut short"]
|
|
22358
|
+
["truncated:", "the scan was cut short"],
|
|
22359
|
+
["coverage:quarantine:", "quarantine-tier rule(s) withheld, as in every default scan (`--include-warn` runs them)"]
|
|
22304
22360
|
];
|
|
22305
22361
|
/**
|
|
22306
22362
|
* `scope-ignored:1` -> "1 path was ignored by the scope filter".
|
|
@@ -22447,7 +22503,9 @@ function renderUnifiedReport(result, options = {}) {
|
|
|
22447
22503
|
provisionalRuleIds: [],
|
|
22448
22504
|
ceilingReasons: []
|
|
22449
22505
|
};
|
|
22450
|
-
const
|
|
22506
|
+
const analysisIncomplete = isIncompleteResult(result) || !result.trustSummary;
|
|
22507
|
+
const noEvidence = summary.level === "L0" && summary.evidenceCoverage === 0;
|
|
22508
|
+
const incomplete = analysisIncomplete || noEvidence;
|
|
22451
22509
|
lines.push(`## <img width="16" height="16" alt="" src="${MARK_URL}"/> Mjölnir Verification Report`);
|
|
22452
22510
|
lines.push("");
|
|
22453
22511
|
lines.push("> Tests tell you what passed. Mjölnir tells you what you can trust.");
|
|
@@ -22458,8 +22516,9 @@ function renderUnifiedReport(result, options = {}) {
|
|
|
22458
22516
|
}
|
|
22459
22517
|
if (incomplete) {
|
|
22460
22518
|
const reasons = result.analysisStatus?.reasons ?? [];
|
|
22519
|
+
const explained = [...reasons.length > 0 ? [humanizeReasons(reasons)] : [], ...!analysisIncomplete && noEvidence ? ["no runtime evidence corroborates this run, so an empty result is an observation, not a verdict"] : []];
|
|
22461
22520
|
lines.push(`> [!WARNING]
|
|
22462
|
-
> **Analysis status: INCOMPLETE.** ${
|
|
22521
|
+
> **Analysis status: INCOMPLETE.** ${explained.length > 0 ? explained.join("; ") : "The scan did not complete a trustworthy analysis."}`);
|
|
22463
22522
|
lines.push("");
|
|
22464
22523
|
}
|
|
22465
22524
|
const diff = options.diff;
|
|
@@ -23173,7 +23232,7 @@ function renderStepSummary(result, options = {}) {
|
|
|
23173
23232
|
lines.push(`<details${open}>`, `<summary>${icons[sev]} ${list.length} ${sev}${list.length === 1 ? "" : "s"}</summary>`, "");
|
|
23174
23233
|
for (const f of list.slice(0, DETAILS_PER_SEVERITY_CAP)) {
|
|
23175
23234
|
const path = options.pathPrefix ? `${options.pathPrefix.replace(/\/$/, "")}/${f.file}` : f.file;
|
|
23176
|
-
lines.push(`- **${escapeMarkdown(f.ruleId)}**
|
|
23235
|
+
lines.push(`- **${escapeMarkdown(f.ruleId)}** ${mdCodeSpan(`${path}:${f.line}`)} — ${escapeMarkdown(stripAnsiForSummary(f.message))}`);
|
|
23177
23236
|
lines.push(` - Fix: ${escapeMarkdown(stripAnsiForSummary(f.fix))}`);
|
|
23178
23237
|
}
|
|
23179
23238
|
if (list.length > DETAILS_PER_SEVERITY_CAP) lines.push(`- … and ${list.length - DETAILS_PER_SEVERITY_CAP} more — see the full JSON artifact.`);
|
|
@@ -28218,6 +28277,19 @@ function verbFailure(error, io, debug) {
|
|
|
28218
28277
|
return 20;
|
|
28219
28278
|
}
|
|
28220
28279
|
/**
|
|
28280
|
+
* The notices a scan reports about itself, sent to the verb's own stderr.
|
|
28281
|
+
*
|
|
28282
|
+
* `fix`, `ci verify` and `handover` called `runScan` with no hooks, so a
|
|
28283
|
+
* config warning (an unknown key, an expired suppression) never reached the
|
|
28284
|
+
* user on those verbs, and the plugin gate notice bypassed `io.err`.
|
|
28285
|
+
*/
|
|
28286
|
+
function scanNotices(io) {
|
|
28287
|
+
return {
|
|
28288
|
+
onConfigWarning: (message) => io.err(message),
|
|
28289
|
+
onGateNotice: (notice) => io.err(notice)
|
|
28290
|
+
};
|
|
28291
|
+
}
|
|
28292
|
+
/**
|
|
28221
28293
|
* The one place a run's gate level is resolved from `--blocking`, then
|
|
28222
28294
|
* `mjolnir.config.json`, then the shipped default.
|
|
28223
28295
|
*
|
|
@@ -28441,9 +28513,8 @@ async function runScanCommand(argv, io = {
|
|
|
28441
28513
|
...args,
|
|
28442
28514
|
target
|
|
28443
28515
|
}, {
|
|
28444
|
-
|
|
28516
|
+
...scanNotices(io),
|
|
28445
28517
|
onProgress: (e) => progress.onEvent(e),
|
|
28446
|
-
onGateNotice: (notice) => io.err(notice),
|
|
28447
28518
|
...args.debug ? { onRuleCrash: (ruleId, file, error) => {
|
|
28448
28519
|
const detail = error instanceof Error ? [error.message, error.stack].filter((value) => Boolean(value)).join("\n") : String(error);
|
|
28449
28520
|
crashLog.push(`${ruleId} crashed on ${file}: ${detail}`);
|
|
@@ -28469,7 +28540,7 @@ async function runScanCommand(argv, io = {
|
|
|
28469
28540
|
for (const line of crashLog.slice(0, 50)) io.err(` ${line}`);
|
|
28470
28541
|
if (crashLog.length > 50) io.err(` … and ${crashLog.length - 50} more`);
|
|
28471
28542
|
}
|
|
28472
|
-
const { config
|
|
28543
|
+
const { config } = loadConfig(target, { knownRuleIds: KNOWN_RULE_IDS$1 });
|
|
28473
28544
|
for (const d of args.deprecatedFlags ?? []) io.err(`warning: ${d.flag} is deprecated and will be removed in 7.0 — use ${d.replacement}. It still does exactly what it did; nothing about your scan changes this release.`);
|
|
28474
28545
|
const gate = resolveGateLevel(args.blocking, config);
|
|
28475
28546
|
if (args.scoreOnly) {
|
|
@@ -28496,7 +28567,6 @@ async function runScanCommand(argv, io = {
|
|
|
28496
28567
|
}
|
|
28497
28568
|
}
|
|
28498
28569
|
if (result.partial) return 2;
|
|
28499
|
-
for (const w of warnings) io.err(w);
|
|
28500
28570
|
if (baselineFailed) return 2;
|
|
28501
28571
|
return scanExitCode({
|
|
28502
28572
|
partial: false,
|
|
@@ -28525,7 +28595,7 @@ async function runFixCommand(argv, io = {
|
|
|
28525
28595
|
...args,
|
|
28526
28596
|
target,
|
|
28527
28597
|
strict: true
|
|
28528
|
-
});
|
|
28598
|
+
}, scanNotices(io));
|
|
28529
28599
|
const fixes = planAndApplyFixes(result, target, { dryRun });
|
|
28530
28600
|
io.out(renderFixReport(fixes, dryRun));
|
|
28531
28601
|
if (result.partial) return 2;
|
|
@@ -28547,7 +28617,7 @@ async function runVerifyCommand(argv, io = {
|
|
|
28547
28617
|
const result = await runScan$1({
|
|
28548
28618
|
...args,
|
|
28549
28619
|
target
|
|
28550
|
-
});
|
|
28620
|
+
}, scanNotices(io));
|
|
28551
28621
|
const baseline = loadBaseline(join(target, DEFAULT_BASELINE_PATH), (w) => io.err(w));
|
|
28552
28622
|
const digest = buildVerifyDigest(result, baseline);
|
|
28553
28623
|
io.out(renderVerifyDigest(digest));
|
|
@@ -28646,7 +28716,7 @@ async function runHandoverCommand(argv, io = {
|
|
|
28646
28716
|
const result = await runScan$1({
|
|
28647
28717
|
...args,
|
|
28648
28718
|
target
|
|
28649
|
-
});
|
|
28719
|
+
}, scanNotices(io));
|
|
28650
28720
|
let forensics = null;
|
|
28651
28721
|
let forensicsFailed = false;
|
|
28652
28722
|
const resultsDir = join(target, "test-results");
|
|
@@ -30394,7 +30464,7 @@ function verificationBlock(version) {
|
|
|
30394
30464
|
/** The per-rule remediation copy block (fenced, self-contained). */
|
|
30395
30465
|
function ruleCopyBlock(group, version) {
|
|
30396
30466
|
const f = group.findings[0];
|
|
30397
|
-
const occurrences = group.findings.slice(0, OCCURRENCE_CAP).map((x) => `-
|
|
30467
|
+
const occurrences = group.findings.slice(0, OCCURRENCE_CAP).map((x) => `- ${mdCodeSpan(`${x.file}:${x.line}`)}`).join("\n");
|
|
30398
30468
|
const overflow = group.findings.length > OCCURRENCE_CAP ? `\n- … and ${group.findings.length - OCCURRENCE_CAP} more — see the JSON report.` : "";
|
|
30399
30469
|
return [
|
|
30400
30470
|
"```text",
|
|
@@ -30520,7 +30590,7 @@ function renderHandoff(result, options = {}, version = ENGINE_VERSION) {
|
|
|
30520
30590
|
lines.push("");
|
|
30521
30591
|
lines.push(`**Occurrences (${g.findings.length}):**`);
|
|
30522
30592
|
lines.push("");
|
|
30523
|
-
for (const f of g.findings.slice(0, OCCURRENCE_CAP)) lines.push(`-
|
|
30593
|
+
for (const f of g.findings.slice(0, OCCURRENCE_CAP)) lines.push(`- ${mdCodeSpan(`${f.file}:${f.line}`)} — ${escapeMarkdown(f.message)}`);
|
|
30524
30594
|
if (g.findings.length > OCCURRENCE_CAP) lines.push(`- … and ${g.findings.length - OCCURRENCE_CAP} more — see the JSON report.`);
|
|
30525
30595
|
lines.push("");
|
|
30526
30596
|
lines.push(ruleCopyBlock(g, version));
|
|
@@ -32848,9 +32918,7 @@ async function runTrustTrendCommand(argv, io = {
|
|
|
32848
32918
|
* identity can carry it without a cli.ts import cycle; this re-export
|
|
32849
32919
|
* keeps every existing consumer stable.
|
|
32850
32920
|
*/
|
|
32851
|
-
const { runScan, buildUniversalRules, fallbackWorkspace, pathMatchesGlob, isValidFindingRecord, discoverAndParseRuntimeReport, KNOWN_RULE_IDS, OVERLAP_META_BY_RULE_ID, EVIDENCE_OVERRIDES, SUITE_INVALIDATING_RULE_IDS } = scan_pipeline_exports;
|
|
32852
|
-
const DEFAULT_MAX_DURATION_MS = 6e5;
|
|
32853
|
-
const MAX_DURATION_MS = 36e5;
|
|
32921
|
+
const { runScan, buildUniversalRules, fallbackWorkspace, pathMatchesGlob, isValidFindingRecord, discoverAndParseRuntimeReport, KNOWN_RULE_IDS, OVERLAP_META_BY_RULE_ID, EVIDENCE_OVERRIDES, SUITE_INVALIDATING_RULE_IDS, DEFAULT_MAX_DURATION_MS, MAX_DURATION_MS } = scan_pipeline_exports;
|
|
32854
32922
|
function parseArgs(argv, onError) {
|
|
32855
32923
|
const args = {
|
|
32856
32924
|
target: ".",
|
|
@@ -32909,7 +32977,7 @@ function parseArgs(argv, onError) {
|
|
|
32909
32977
|
args.base = ref;
|
|
32910
32978
|
} else if (a === "--max-duration") {
|
|
32911
32979
|
const v = Number(argv[++i]);
|
|
32912
|
-
if (!Number.isFinite(v) || v <= 0 || v * 1e3 >
|
|
32980
|
+
if (!Number.isFinite(v) || v <= 0 || v * 1e3 > MAX_DURATION_MS) return reject({
|
|
32913
32981
|
flag: "--max-duration",
|
|
32914
32982
|
token: argv[i]
|
|
32915
32983
|
});
|
|
@@ -32968,11 +33036,22 @@ function parseArgs(argv, onError) {
|
|
|
32968
33036
|
}
|
|
32969
33037
|
return args;
|
|
32970
33038
|
}
|
|
32971
|
-
/**
|
|
33039
|
+
/**
|
|
33040
|
+
* Scan flags that exist — the "did you mean" candidate pool.
|
|
33041
|
+
*
|
|
33042
|
+
* Every flag `parseArgs` accepts, and nothing it rejects. The pool had
|
|
33043
|
+
* drifted both ways: ten accepted flags were missing, and `--dry-run` and
|
|
33044
|
+
* `--version` were in it although a scan rejects them, so `mjolnir scan
|
|
33045
|
+
* --dry-run` answered `unknown flag "--dry-run"` and then suggested it.
|
|
33046
|
+
* tests/cli/help.spec.ts reads `parseArgs` to hold the two lists together.
|
|
33047
|
+
*/
|
|
32972
33048
|
const KNOWN_SCAN_FLAGS = [
|
|
32973
33049
|
"--json",
|
|
32974
33050
|
"--format",
|
|
32975
33051
|
"--verbose",
|
|
33052
|
+
"--suppressions",
|
|
33053
|
+
"--suppression-gate",
|
|
33054
|
+
"--policy",
|
|
32976
33055
|
"--scope",
|
|
32977
33056
|
"--base",
|
|
32978
33057
|
"--max-duration",
|
|
@@ -32987,12 +33066,16 @@ const KNOWN_SCAN_FLAGS = [
|
|
|
32987
33066
|
"--record-milestones",
|
|
32988
33067
|
"--save-baseline",
|
|
32989
33068
|
"--cache",
|
|
33069
|
+
"--no-progress",
|
|
33070
|
+
"--category",
|
|
33071
|
+
"--score",
|
|
33072
|
+
"--require-full-coverage",
|
|
33073
|
+
"--staged",
|
|
33074
|
+
"--blocking",
|
|
33075
|
+
"--enable-plugins",
|
|
32990
33076
|
"--monorepo",
|
|
32991
33077
|
"--help",
|
|
32992
|
-
"-h"
|
|
32993
|
-
"--version",
|
|
32994
|
-
"-v",
|
|
32995
|
-
"--dry-run"
|
|
33078
|
+
"-h"
|
|
32996
33079
|
];
|
|
32997
33080
|
/** Hand-rolled Levenshtein distance (plan M2: no new dependencies). */
|
|
32998
33081
|
function levenshtein(a, b) {
|
|
@@ -33249,13 +33332,8 @@ async function main(argv = process$1.argv.slice(2), io = {
|
|
|
33249
33332
|
io.out(renderCiSubcommandHelp());
|
|
33250
33333
|
return 10;
|
|
33251
33334
|
}
|
|
33252
|
-
if (argv[0] === "ci" && argv[1] === "install") return runCiInstall(argv.slice(2), io);
|
|
33253
33335
|
if (argv[0] === "ci") {
|
|
33254
33336
|
const sub = argv[1];
|
|
33255
|
-
if (sub === void 0 || sub === "--help" || sub === "-h") {
|
|
33256
|
-
io.out(renderCiSubcommandHelp());
|
|
33257
|
-
return sub === void 0 ? 10 : 0;
|
|
33258
|
-
}
|
|
33259
33337
|
const entry = CI_SUBCOMMANDS[sub];
|
|
33260
33338
|
if (entry === void 0) {
|
|
33261
33339
|
io.err(`mjolnir ci: unknown subcommand "${sub}".`);
|
|
@@ -33347,6 +33425,6 @@ if (isEntryPoint()) {
|
|
|
33347
33425
|
await flushSentry();
|
|
33348
33426
|
}
|
|
33349
33427
|
}
|
|
33350
|
-
export { ENGINE_VERSION as CLI_VERSION, DEFAULT_MAX_DURATION_MS, EVIDENCE_OVERRIDES, EXPLAIN_ARMS, KNOWN_RULE_IDS, MAX_DURATION_MS, OVERLAP_META_BY_RULE_ID, SUITE_INVALIDATING_RULE_IDS, buildUniversalRules, discoverAndParseRuntimeReport, err, exitForFindings, fallbackWorkspace, internalErrorMessage, isEntryPoint, isValidFindingRecord, levenshtein, main, nearestFlags, out, parseArgs, parseArgsOrUsage, pathMatchesGlob, printUsage, renderCiSubcommandHelp, renderScanOutput, runAnalyzeCommand, runCIIntegrityCommand, runCiAdapterCommand, runCiInstall, runContractVerifyCommand, runCrossFileCommand, runDoctorCommand, runDoctorPlaywright, runEvidenceArm, runEvidenceGraphCommand, runExplainCommand, runFixCommand, runFrameworkMaturityCommand, runHandoffCommand, runHandoverCommand, runHelpCommand, runInstallCommand, runPolicyCommand, runPrCommentCommand, runPwReportCommand, runReleaseTrustCommand, runRulesCommand, runScan, runScanCommand, runStatsCommand, runSuppressionGateCommand, runSuppressions, runTrustTrendCommand, runVerifyCommand, runWhyCommand, usageErrorMessage, validateScanTarget };
|
|
33428
|
+
export { ENGINE_VERSION as CLI_VERSION, DEFAULT_MAX_DURATION_MS, EVIDENCE_OVERRIDES, EXPLAIN_ARMS, KNOWN_RULE_IDS, KNOWN_SCAN_FLAGS, MAX_DURATION_MS, OVERLAP_META_BY_RULE_ID, SUITE_INVALIDATING_RULE_IDS, buildUniversalRules, discoverAndParseRuntimeReport, err, exitForFindings, fallbackWorkspace, internalErrorMessage, isEntryPoint, isValidFindingRecord, levenshtein, main, nearestFlags, out, parseArgs, parseArgsOrUsage, pathMatchesGlob, printUsage, renderCiSubcommandHelp, renderScanOutput, runAnalyzeCommand, runCIIntegrityCommand, runCiAdapterCommand, runCiInstall, runContractVerifyCommand, runCrossFileCommand, runDoctorCommand, runDoctorPlaywright, runEvidenceArm, runEvidenceGraphCommand, runExplainCommand, runFixCommand, runFrameworkMaturityCommand, runHandoffCommand, runHandoverCommand, runHelpCommand, runInstallCommand, runPolicyCommand, runPrCommentCommand, runPwReportCommand, runReleaseTrustCommand, runRulesCommand, runScan, runScanCommand, runStatsCommand, runSuppressionGateCommand, runSuppressions, runTrustTrendCommand, runVerifyCommand, runWhyCommand, usageErrorMessage, validateScanTarget };
|
|
33351
33429
|
|
|
33352
33430
|
//# sourceMappingURL=cli.mjs.map
|
package/dist/mcp/stdio.mjs
CHANGED
|
@@ -193,7 +193,8 @@ function stampRuntimeCorroboration(findings, report, scanRoot) {
|
|
|
193
193
|
let n = p.replace(/\\/g, "/");
|
|
194
194
|
if (scanRoot) {
|
|
195
195
|
const root = scanRoot.replace(/\\/g, "/").replace(/\/?$/, "/");
|
|
196
|
-
|
|
196
|
+
const head = n.slice(0, root.length);
|
|
197
|
+
if (/^[a-z]:\//i.test(root) ? head.toLowerCase() === root.toLowerCase() : head === root) n = n.slice(root.length);
|
|
197
198
|
}
|
|
198
199
|
return n.replace(/^\.\//, "").replace(/^\/+/, "");
|
|
199
200
|
};
|
|
@@ -1423,7 +1424,7 @@ function readCandidateBinding(root, manifestPath = CANDIDATE_MANIFEST_PATH) {
|
|
|
1423
1424
|
* guarded by the version-consistency
|
|
1424
1425
|
* spec. cli.ts re-exports this as CLI_VERSION.
|
|
1425
1426
|
*/
|
|
1426
|
-
const ENGINE_VERSION = "6.0
|
|
1427
|
+
const ENGINE_VERSION = "6.1.0";
|
|
1427
1428
|
resolveBuildId();
|
|
1428
1429
|
/**
|
|
1429
1430
|
* The decision, with the process boundary already behind `runGit`.
|
|
@@ -1729,7 +1730,11 @@ function validateConfigSchema(config) {
|
|
|
1729
1730
|
}
|
|
1730
1731
|
}
|
|
1731
1732
|
if (cfg["plugins"] !== void 0) {
|
|
1732
|
-
if (cfg["plugins"]
|
|
1733
|
+
if (!Array.isArray(cfg["plugins"])) errors.push(`plugins must be an array of package names or { "package": "<name>" } objects, got ${typeof cfg["plugins"]}`);
|
|
1734
|
+
else for (let i = 0; i < cfg["plugins"].length; i++) {
|
|
1735
|
+
const entry = cfg["plugins"][i];
|
|
1736
|
+
if (!(typeof entry === "string" || entry !== null && typeof entry === "object" && !Array.isArray(entry) && typeof entry["package"] === "string")) errors.push(`plugins[${i}] must be a package name or { "package": "<name>" }, got ${JSON.stringify(entry)}`);
|
|
1737
|
+
}
|
|
1733
1738
|
}
|
|
1734
1739
|
return {
|
|
1735
1740
|
valid: errors.length === 0,
|
|
@@ -1794,7 +1799,7 @@ function loadConfig(root, options = {}) {
|
|
|
1794
1799
|
return {
|
|
1795
1800
|
config: parsed,
|
|
1796
1801
|
path: p,
|
|
1797
|
-
warnings: validate(parsed, options.knownRuleIds)
|
|
1802
|
+
warnings: validate(parsed, options.knownRuleIds, options.now, name)
|
|
1798
1803
|
};
|
|
1799
1804
|
} catch (err) {
|
|
1800
1805
|
const msg = err.message;
|
|
@@ -1807,7 +1812,7 @@ function loadConfig(root, options = {}) {
|
|
|
1807
1812
|
warnings: []
|
|
1808
1813
|
};
|
|
1809
1814
|
}
|
|
1810
|
-
function validate(cfg, knownRuleIds) {
|
|
1815
|
+
function validate(cfg, knownRuleIds, now = /* @__PURE__ */ new Date(), fileName = "mjolnir.config.json") {
|
|
1811
1816
|
const warnings = [];
|
|
1812
1817
|
const schemaResult = validateConfigSchema(cfg);
|
|
1813
1818
|
if (!schemaResult.valid) throw new Error(schemaResult.errors.join("; "));
|
|
@@ -1827,10 +1832,10 @@ function validate(cfg, knownRuleIds) {
|
|
|
1827
1832
|
if (!Array.isArray(ign.files)) throw new Error(`ignore for ${ign.ruleId}: "files" must be an array of glob strings, got ${typeof ign.files}`);
|
|
1828
1833
|
for (const g of ign.files) if (typeof g !== "string") throw new Error(`ignore for ${ign.ruleId}: "files" entries must be strings, got ${typeof g} (${JSON.stringify(g)})`);
|
|
1829
1834
|
}
|
|
1830
|
-
if (ign.expires !== void 0 && Number.isNaN(
|
|
1831
|
-
if (ign.expires !== void 0 && !
|
|
1835
|
+
if (ign.expires !== void 0 && Number.isNaN(utcMidnight(ign.expires))) throw new Error(`ignore for ${ign.ruleId}: "expires" must be an ISO date (YYYY-MM-DD), got "${ign.expires}"`);
|
|
1836
|
+
if (ign.expires !== void 0 && !isSuppressionActive(ign, now)) warnings.push(`warning: ignore for ${ign.ruleId} has expired (expires "${ign.expires}") — the entry is kept but currently inactive.`);
|
|
1832
1837
|
}
|
|
1833
|
-
for (const key of Object.keys(cfg)) if (!KNOWN_CONFIG_KEYS.has(key)) warnings.push(`warning:
|
|
1838
|
+
for (const key of Object.keys(cfg)) if (!KNOWN_CONFIG_KEYS.has(key)) warnings.push(`warning: ${fileName} has an unknown top-level key "${key}" — ignored (typo? see README §Configuration).`);
|
|
1834
1839
|
if (cfg.exclude !== void 0) {
|
|
1835
1840
|
if (!Array.isArray(cfg.exclude)) throw new Error(`exclude must be an array of strings, got ${typeof cfg.exclude}`);
|
|
1836
1841
|
for (const p of cfg.exclude) if (typeof p !== "string") throw new Error(`exclude entries must be strings, got ${typeof p} (${JSON.stringify(p)})`);
|
|
@@ -1852,12 +1857,22 @@ function isSuppressionActive(ign, now = /* @__PURE__ */ new Date()) {
|
|
|
1852
1857
|
if (!ign.expires) return true;
|
|
1853
1858
|
return utcMidnight(ign.expires) > utcMidnightOf(now);
|
|
1854
1859
|
}
|
|
1855
|
-
/**
|
|
1860
|
+
/**
|
|
1861
|
+
* UTC-midnight timestamp of an ISO calendar date (YYYY-MM-DD), or NaN.
|
|
1862
|
+
*
|
|
1863
|
+
* NaN for anything that is not a real calendar day, so a load-time check and
|
|
1864
|
+
* the active/expired verdict share one parser and cannot disagree.
|
|
1865
|
+
*/
|
|
1856
1866
|
function utcMidnight(isoDate) {
|
|
1857
|
-
if (!/^\d{4}-\d{2}-\d{2}$/.test(isoDate)) return NaN;
|
|
1858
1867
|
const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(isoDate);
|
|
1859
1868
|
if (!m) return NaN;
|
|
1860
|
-
|
|
1869
|
+
const year = Number(m[1]);
|
|
1870
|
+
const month = Number(m[2]) - 1;
|
|
1871
|
+
const day = Number(m[3]);
|
|
1872
|
+
const t = Date.UTC(year, month, day);
|
|
1873
|
+
const back = new Date(t);
|
|
1874
|
+
if (back.getUTCFullYear() !== year || back.getUTCMonth() !== month || back.getUTCDate() !== day) return NaN;
|
|
1875
|
+
return t;
|
|
1861
1876
|
}
|
|
1862
1877
|
/** UTC-midnight timestamp of a Date (calendar day, timezone-independent). */
|
|
1863
1878
|
function utcMidnightOf(d) {
|
|
@@ -3580,7 +3595,9 @@ const javaAdapter = {
|
|
|
3580
3595
|
const artifact = /<artifactId>([^<]+)<\/artifactId>/.exec(block[0])?.[1] ?? "";
|
|
3581
3596
|
for (const { re, tag } of JAVA_ARTIFACT_TAGS) if (re.test(artifact)) frameworks.add(tag);
|
|
3582
3597
|
}
|
|
3583
|
-
} catch {
|
|
3598
|
+
} catch {
|
|
3599
|
+
recordDegradation("java-manifest-unreadable");
|
|
3600
|
+
}
|
|
3584
3601
|
for (const gradle of ["build.gradle", "build.gradle.kts"]) {
|
|
3585
3602
|
const path = join(root, gradle);
|
|
3586
3603
|
if (!existsSync(path)) continue;
|
|
@@ -3590,7 +3607,9 @@ const javaAdapter = {
|
|
|
3590
3607
|
const artifact = m[1].split(":")[1];
|
|
3591
3608
|
for (const { re, tag } of JAVA_ARTIFACT_TAGS) if (artifact !== void 0 && re.test(artifact)) frameworks.add(tag);
|
|
3592
3609
|
}
|
|
3593
|
-
} catch {
|
|
3610
|
+
} catch {
|
|
3611
|
+
recordDegradation("java-manifest-unreadable");
|
|
3612
|
+
}
|
|
3594
3613
|
}
|
|
3595
3614
|
if (frameworks.size === 0) return {
|
|
3596
3615
|
frameworks: [],
|
|
@@ -3748,9 +3767,13 @@ const csharpAdapter = {
|
|
|
3748
3767
|
const pkg = m[1];
|
|
3749
3768
|
for (const { re, tag } of CS_PACKAGE_TAGS) if (re.test(pkg)) frameworks.add(tag);
|
|
3750
3769
|
}
|
|
3751
|
-
} catch {
|
|
3770
|
+
} catch {
|
|
3771
|
+
recordDegradation("csharp-project-unreadable");
|
|
3772
|
+
}
|
|
3752
3773
|
}
|
|
3753
|
-
} catch {
|
|
3774
|
+
} catch {
|
|
3775
|
+
recordDegradation("csharp-project-unreadable");
|
|
3776
|
+
}
|
|
3754
3777
|
if (frameworks.size === 0) return {
|
|
3755
3778
|
frameworks: [],
|
|
3756
3779
|
unknown: true
|
|
@@ -4664,6 +4687,7 @@ function computeChangedScope(root, baseBranch) {
|
|
|
4664
4687
|
...untrackedSet
|
|
4665
4688
|
])].sort();
|
|
4666
4689
|
const changed = {};
|
|
4690
|
+
const fullyChanged = /* @__PURE__ */ new Set();
|
|
4667
4691
|
const batchDiff = (args, files) => {
|
|
4668
4692
|
const perFile = /* @__PURE__ */ new Map();
|
|
4669
4693
|
if (files.length === 0) return perFile;
|
|
@@ -4715,7 +4739,8 @@ function computeChangedScope(root, baseBranch) {
|
|
|
4715
4739
|
if (untrackedSet.has(file)) {
|
|
4716
4740
|
const all = allLinesOf(root, file);
|
|
4717
4741
|
if (all === null) {
|
|
4718
|
-
|
|
4742
|
+
fullyChanged.add(file);
|
|
4743
|
+
changed[file] = lines;
|
|
4719
4744
|
continue;
|
|
4720
4745
|
}
|
|
4721
4746
|
for (const l of all) lines.add(l);
|
|
@@ -4724,11 +4749,10 @@ function computeChangedScope(root, baseBranch) {
|
|
|
4724
4749
|
}
|
|
4725
4750
|
return {
|
|
4726
4751
|
changed,
|
|
4752
|
+
...fullyChanged.size > 0 ? { fullyChanged } : {},
|
|
4727
4753
|
degraded: false
|
|
4728
4754
|
};
|
|
4729
4755
|
}
|
|
4730
|
-
/** Sentinel size for "fully changed" — bounded, mirrors LIMITS.maxFileBytes scale. */
|
|
4731
|
-
const LIMITS_MAX_LINES = 1e6;
|
|
4732
4756
|
/** Every 1-based line number of an untracked file (it is all new). */
|
|
4733
4757
|
function allLinesOf(root, file) {
|
|
4734
4758
|
const full = join(root, file);
|
|
@@ -4778,6 +4802,7 @@ function parseChangedLines(diff) {
|
|
|
4778
4802
|
function filterToChanged(findings, diff) {
|
|
4779
4803
|
if (diff.degraded) return findings;
|
|
4780
4804
|
return findings.filter((f) => {
|
|
4805
|
+
if (diff.fullyChanged?.has(f.file)) return true;
|
|
4781
4806
|
const lines = diff.changed[f.file];
|
|
4782
4807
|
if (!lines) return false;
|
|
4783
4808
|
for (let l = Math.max(1, f.line - 3); l <= f.line; l++) if (lines.has(l)) return true;
|
|
@@ -13468,7 +13493,9 @@ function hashDir(dir, hash, depth) {
|
|
|
13468
13493
|
else if (entry.isFile() && /\.tsx?$/.test(entry.name)) try {
|
|
13469
13494
|
hash.update(entry.name);
|
|
13470
13495
|
hash.update(readFileSync(full));
|
|
13471
|
-
} catch {
|
|
13496
|
+
} catch {
|
|
13497
|
+
recordDegradation("rules-tree-file-unreadable");
|
|
13498
|
+
}
|
|
13472
13499
|
}
|
|
13473
13500
|
}
|
|
13474
13501
|
/**
|
|
@@ -14903,13 +14930,15 @@ function runForensics(target, options = {}) {
|
|
|
14903
14930
|
report.incompleteReasons = [.../* @__PURE__ */ new Set([...report.incompleteReasons, ...incompleteReasons])];
|
|
14904
14931
|
}
|
|
14905
14932
|
let flakyMdPath;
|
|
14933
|
+
let flakyMdWriteError;
|
|
14906
14934
|
if ((options.writeFlakyMd ?? true) && report.totalTests > 0) {
|
|
14907
14935
|
const base = stat.isFile() ? dirname(target) : target;
|
|
14908
14936
|
flakyMdPath = join(base, "FLAKY.md");
|
|
14909
14937
|
try {
|
|
14910
14938
|
writeFileAtomic(flakyMdPath, renderFlakyMd(report));
|
|
14911
|
-
} catch {
|
|
14939
|
+
} catch (e) {
|
|
14912
14940
|
flakyMdPath = void 0;
|
|
14941
|
+
flakyMdWriteError = e instanceof Error ? e.message : String(e);
|
|
14913
14942
|
}
|
|
14914
14943
|
}
|
|
14915
14944
|
return {
|
|
@@ -14917,7 +14946,7 @@ function runForensics(target, options = {}) {
|
|
|
14917
14946
|
output: [
|
|
14918
14947
|
renderLeaderboard(report),
|
|
14919
14948
|
"",
|
|
14920
|
-
flakyMdPath !== void 0 ? renderFlakyMdHint() : report.totalTests === 0 && (report.totalNetworkObservations ?? 0) > 0 && options.writeFlakyMd !== false ? "FLAKY.md was not written — network observations do not establish test outcomes." : renderFlakyMdNotWritten(options)
|
|
14949
|
+
flakyMdPath !== void 0 ? renderFlakyMdHint() : report.totalTests === 0 && (report.totalNetworkObservations ?? 0) > 0 && options.writeFlakyMd !== false ? "FLAKY.md was not written — network observations do not establish test outcomes." : renderFlakyMdNotWritten(options, flakyMdWriteError)
|
|
14921
14950
|
].join("\n"),
|
|
14922
14951
|
flakyMdPath
|
|
14923
14952
|
};
|
|
@@ -14926,9 +14955,10 @@ function renderFlakyMdHint() {
|
|
|
14926
14955
|
return "Full details in FLAKY.md (committed artifact).";
|
|
14927
14956
|
}
|
|
14928
14957
|
/** Honest fallback for every case where FLAKY.md was NOT written (M2). */
|
|
14929
|
-
function renderFlakyMdNotWritten(options) {
|
|
14958
|
+
function renderFlakyMdNotWritten(options, writeError) {
|
|
14930
14959
|
if (options.writeFlakyMd === false) return "FLAKY.md not written (--no-flaky-md).";
|
|
14931
|
-
return
|
|
14960
|
+
if (writeError !== void 0) return `FLAKY.md could not be written: ${writeError}`;
|
|
14961
|
+
return "FLAKY.md was not written — nothing recognized to report.";
|
|
14932
14962
|
}
|
|
14933
14963
|
/**
|
|
14934
14964
|
* R5: the trace artifact's test identity. The per-test trace convention
|
|
@@ -15529,8 +15559,13 @@ function renderGateNotice(skipped) {
|
|
|
15529
15559
|
* module moved. Re-exports in cli.ts keep the historical import surface.
|
|
15530
15560
|
*/
|
|
15531
15561
|
const UNIVERSAL_RULES = RULES.map(asUniversal);
|
|
15532
|
-
|
|
15533
|
-
|
|
15562
|
+
/**
|
|
15563
|
+
* The scan time budget: the default, and the ceiling every surface enforces.
|
|
15564
|
+
* Owned here, where the clamp is applied; `cli.ts` and the MCP server import
|
|
15565
|
+
* these rather than keeping copies that only agree by coincidence.
|
|
15566
|
+
*/
|
|
15567
|
+
const DEFAULT_MAX_DURATION_MS = 6e5;
|
|
15568
|
+
const MAX_DURATION_MS = 36e5;
|
|
15534
15569
|
/** Registered rule IDs — used to warn on unknown severityOverrides keys (M4). */
|
|
15535
15570
|
const KNOWN_RULE_IDS = new Set(RULES.map((r) => r.id));
|
|
15536
15571
|
/**
|
|
@@ -16285,8 +16320,8 @@ async function runScan(args, hooks = {}) {
|
|
|
16285
16320
|
async function runScanInProject(args, hooks = {}) {
|
|
16286
16321
|
const started = Date.now();
|
|
16287
16322
|
const degradationWindow = beginDegradationWindow();
|
|
16288
|
-
const requestedDuration = Number.isFinite(args.maxDurationMs) ? args.maxDurationMs : DEFAULT_MAX_DURATION_MS
|
|
16289
|
-
const deadline = started + Math.min(Math.max(1, requestedDuration), MAX_DURATION_MS
|
|
16323
|
+
const requestedDuration = Number.isFinite(args.maxDurationMs) ? args.maxDurationMs : DEFAULT_MAX_DURATION_MS;
|
|
16324
|
+
const deadline = started + Math.min(Math.max(1, requestedDuration), MAX_DURATION_MS);
|
|
16290
16325
|
const discovered = discoverWorkspace(args.target);
|
|
16291
16326
|
const targetAbs = resolve(args.target);
|
|
16292
16327
|
const scanRoot = discovered && discovered.root !== targetAbs && targetAbs.startsWith(discovered.root + sep) ? {
|
|
@@ -17047,8 +17082,6 @@ function renderPwRunSummary(s) {
|
|
|
17047
17082
|
*/
|
|
17048
17083
|
/** Hard caps (§21 threat model: parameter size + resource bounds). */
|
|
17049
17084
|
const MAX_PARAM_BYTES = 65536;
|
|
17050
|
-
const MAX_DURATION_MS = 36e5;
|
|
17051
|
-
const DEFAULT_MAX_DURATION_MS = 6e5;
|
|
17052
17085
|
const PROTOCOL_VERSION = "2025-06-18";
|
|
17053
17086
|
/** The real path of `p`, or its lexical resolution when it does not exist. */
|
|
17054
17087
|
function realOrResolved(p) {
|
|
@@ -17112,7 +17145,7 @@ const MCP_TOOLS = [
|
|
|
17112
17145
|
},
|
|
17113
17146
|
fixturesRoot: {
|
|
17114
17147
|
type: "string",
|
|
17115
|
-
description: "Root directory for fixture lookups (defaults to cwd)."
|
|
17148
|
+
description: "Root directory for fixture lookups (defaults to <cwd>/tests/fixtures, as for `mjolnir explain`)."
|
|
17116
17149
|
}
|
|
17117
17150
|
},
|
|
17118
17151
|
required: ["ruleId"]
|
|
@@ -17192,7 +17225,7 @@ function validateParams(name, args) {
|
|
|
17192
17225
|
if (typeof args["path"] !== "string" || args["path"].length === 0) return "scan requires a non-empty string `path`";
|
|
17193
17226
|
if (args["maxDurationMs"] !== void 0) {
|
|
17194
17227
|
const duration = args["maxDurationMs"];
|
|
17195
|
-
if (typeof duration !== "number" || !Number.isFinite(duration) || duration <= 0 || duration >
|
|
17228
|
+
if (typeof duration !== "number" || !Number.isFinite(duration) || duration <= 0 || duration > 36e5) return "`maxDurationMs` must be a finite positive number within the server budget";
|
|
17196
17229
|
}
|
|
17197
17230
|
}
|
|
17198
17231
|
if (name === "explain") {
|
|
@@ -17296,7 +17329,7 @@ async function handleToolCall(call) {
|
|
|
17296
17329
|
message: "fixturesRoot must be a string"
|
|
17297
17330
|
}
|
|
17298
17331
|
};
|
|
17299
|
-
const explanation = explainRule(ruleId, (typeof rawFixturesRoot === "string" ? rawFixturesRoot : void 0) ?? process.cwd());
|
|
17332
|
+
const explanation = explainRule(ruleId, (typeof rawFixturesRoot === "string" ? rawFixturesRoot : void 0) ?? join(process.cwd(), "tests", "fixtures"));
|
|
17300
17333
|
if (!explanation.ok) return {
|
|
17301
17334
|
jsonrpc: "2.0",
|
|
17302
17335
|
id: call.id,
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mjolnir-qa",
|
|
3
|
-
"version": "6.0
|
|
4
|
-
"publishedStable": "6.0
|
|
3
|
+
"version": "6.1.0",
|
|
4
|
+
"publishedStable": "6.1.0",
|
|
5
5
|
"description": "Mjölnir — the Verification Trust Engine for QA. Audits test suites and CI pipelines, reports a worthiness score and prioritized findings.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"engines": {
|