mjolnir-qa 0.5.32 → 0.5.34
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/README.md +21 -3
- package/dist/cli.d.mts +10 -1
- package/dist/cli.mjs +202 -10
- package/dist/mcp/stdio.mjs +202 -10
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -9,6 +9,18 @@ Rule behavior changes (new rules, FP-rate changes against the corpus,
|
|
|
9
9
|
severity changes) are first-class entries here — rule IDs are immutable
|
|
10
10
|
once shipped, so this file is the record of what changed between versions.
|
|
11
11
|
|
|
12
|
+
## [0.5.34] — 2026-09-08
|
|
13
|
+
|
|
14
|
+
### Changes since 0
|
|
15
|
+
|
|
16
|
+
- p2: structural anti-dilution — deduction-mass ceilings close the padding vector (plan 1788853205786 P2, decision 4) (#64)
|
|
17
|
+
|
|
18
|
+
## [0.5.33] — 2026-09-08
|
|
19
|
+
|
|
20
|
+
### Changes since 0.5.32
|
|
21
|
+
|
|
22
|
+
- P1: distribution — root action, moving v1 tag, action-based ci install (#63)
|
|
23
|
+
|
|
12
24
|
## [0.5.32] — 2026-09-08
|
|
13
25
|
|
|
14
26
|
### Changes since 0.5.31
|
package/README.md
CHANGED
|
@@ -188,14 +188,16 @@ not drown a first PR:
|
|
|
188
188
|
npx mjolnir-qa@latest --scope changed
|
|
189
189
|
```
|
|
190
190
|
|
|
191
|
-
`mjolnir ci install` writes that as a GitHub Actions workflow —
|
|
192
|
-
|
|
191
|
+
`mjolnir ci install` writes that as a GitHub Actions workflow — the
|
|
192
|
+
[action](https://github.com/Sergey-Bar/Mjolnir#readme) (Marketplace-grade,
|
|
193
|
+
pinned to the `v1` major tag) by default, or plain `npx` with
|
|
194
|
+
`--no-action`. Advisory by default, never blocking until you say so.
|
|
193
195
|
|
|
194
196
|
| Command | What it does |
|
|
195
197
|
| ----------------------------------- | ------------------------------------------------ |
|
|
196
198
|
| `mjolnir` | Full-repo scan + worthiness score |
|
|
197
199
|
| `mjolnir --scope changed` | Only what your branch introduced — the CI form |
|
|
198
|
-
| `mjolnir ci install` | Generate the advisory PR workflow
|
|
200
|
+
| `mjolnir ci install` | Generate the advisory PR workflow (action-based) |
|
|
199
201
|
| `mjolnir explain QA-CI-001` | What / why / fix + measured FP rate for one rule |
|
|
200
202
|
| `mjolnir why src/a.spec.ts:42` | Why this exact line was flagged — never a gate |
|
|
201
203
|
| `mjolnir forensics ./test-results/` | Runtime evidence from a real run |
|
|
@@ -236,6 +238,9 @@ of them.
|
|
|
236
238
|
|
|
237
239
|
Requires **Node.js ≥ 22.18**. Runs on Windows, macOS and Linux. Install
|
|
238
240
|
globally with `npm i -g mjolnir-qa` if you prefer it over `npx`.
|
|
241
|
+
(Why ≥ 22.18? The build toolchain sets the floor — tsdown targets it and
|
|
242
|
+
the release pipeline smoke-tests against it; the runtime dependencies
|
|
243
|
+
have no such requirement.)
|
|
239
244
|
|
|
240
245
|
---
|
|
241
246
|
|
|
@@ -538,6 +543,19 @@ One command generates the PR workflow — advisory by default:
|
|
|
538
543
|
mjolnir ci install
|
|
539
544
|
```
|
|
540
545
|
|
|
546
|
+
Prefer the Marketplace action over a generated workflow? It is one line:
|
|
547
|
+
|
|
548
|
+
```yaml
|
|
549
|
+
- uses: Sergey-Bar/Mjolnir@v1
|
|
550
|
+
with:
|
|
551
|
+
scope: changed
|
|
552
|
+
fail-on: error
|
|
553
|
+
```
|
|
554
|
+
|
|
555
|
+
Pin `@v1` to follow the major line or an exact tag (`@v0.5.32`) for a
|
|
556
|
+
reproducible gate — [docs/DISTRIBUTION-KIT.md](docs/DISTRIBUTION-KIT.md)
|
|
557
|
+
covers Marketplace, Smithery and the MCP registries.
|
|
558
|
+
|
|
541
559
|
Or wire it into GitHub Code Scanning natively via SARIF:
|
|
542
560
|
|
|
543
561
|
```yaml
|
package/dist/cli.d.mts
CHANGED
|
@@ -201,6 +201,15 @@ interface ScanResult {
|
|
|
201
201
|
testDeclarationCount?: number;
|
|
202
202
|
/** Raw deduction total before normalization (Phase 5 — transparency). */
|
|
203
203
|
rawDeductions?: number;
|
|
204
|
+
/**
|
|
205
|
+
* The evidence-discounted deduction mass the P2 anti-dilution ceiling
|
|
206
|
+
* caps against (equals rawDeductions today — E0 charges 0, E1 halves;
|
|
207
|
+
* the ceiling input is deliberately the full discount surface).
|
|
208
|
+
* Additive within schemaVersion 1; present so consumers recompute the
|
|
209
|
+
* ceiling from docs/SCORING.md formula v2 without re-deriving
|
|
210
|
+
* evidence levels.
|
|
211
|
+
*/
|
|
212
|
+
effectiveDeductions?: number;
|
|
204
213
|
/** Number of findings suppressed by active config entries (suppression transparency). */
|
|
205
214
|
suppressionCount?: number;
|
|
206
215
|
/**
|
|
@@ -728,7 +737,7 @@ declare const runScan: typeof runScan$1, buildUniversalRules: typeof buildUniver
|
|
|
728
737
|
* `scripts/sync-sarif-version.cjs` on release and guarded by
|
|
729
738
|
* `tests/version-consistency.spec.ts` locally.
|
|
730
739
|
*/
|
|
731
|
-
declare const CLI_VERSION = "0.5.
|
|
740
|
+
declare const CLI_VERSION = "0.5.34";
|
|
732
741
|
/** A usage-error detail: the offending token, when one exists. */
|
|
733
742
|
interface UsageErrorDetail {
|
|
734
743
|
/** The unknown flag or rejected value (e.g. `--nope`, `loud`). */
|
package/dist/cli.mjs
CHANGED
|
@@ -10074,6 +10074,58 @@ function computeDimensions(findings) {
|
|
|
10074
10074
|
for (const dim of byCategory.values()) dim.score = Math.max(0, 100 - deductions.get(dim.category));
|
|
10075
10075
|
return [...byCategory.values()].sort((a, b) => a.category.localeCompare(b.category));
|
|
10076
10076
|
}
|
|
10077
|
+
/**
|
|
10078
|
+
* Deduction-mass ceilings (product-gap-remediation master plan P2,
|
|
10079
|
+
* plan 1788853205786 — structural anti-dilution, decision 4).
|
|
10080
|
+
*
|
|
10081
|
+
* The Goodhart vector the density formula permits: fixed deduction mass
|
|
10082
|
+
* ÷ padded denominator → score → 99. A repo with 80 warning-points of
|
|
10083
|
+
* real findings and 10,000 test declarations reads 99+ ("one minor
|
|
10084
|
+
* issue") because the RATE is tiny — the padding is free. Density stays
|
|
10085
|
+
* as the legitimate differentiator for small masses (a lone warning in
|
|
10086
|
+
* a 10k suite must still read ≥ 95), but it can no longer dilute a
|
|
10087
|
+
* large mass: each absolute deduction-mass band caps the score. Padding
|
|
10088
|
+
* cannot move a ceiling — the vector is structurally closed. 80
|
|
10089
|
+
* warning-pts read ≤ 75 in ANY suite size.
|
|
10090
|
+
*
|
|
10091
|
+
* The error-severity floor (95) is the ≥ 8 band seen from the other
|
|
10092
|
+
* side: 1 error ≥ 8 pts, so it is subsumed and left standing as a named
|
|
10093
|
+
* special case for prose continuity. The > 0 band is the honesty guard
|
|
10094
|
+
* (clampWithFindings) — subsumed as well.
|
|
10095
|
+
*
|
|
10096
|
+
* Calibration (P2.4): these boundaries were chosen to hold the three
|
|
10097
|
+
* known data points (self 100, golden 49 via suiteVoided, demo 67) and
|
|
10098
|
+
* are then FROZEN with a changelog entry. Never tune them to flatter a
|
|
10099
|
+
* repo (docs/SCORING.md Correction section is the precedent).
|
|
10100
|
+
*/
|
|
10101
|
+
const DEDUCTION_MASS_CEILINGS = [
|
|
10102
|
+
{
|
|
10103
|
+
minMass: 160,
|
|
10104
|
+
ceiling: 65
|
|
10105
|
+
},
|
|
10106
|
+
{
|
|
10107
|
+
minMass: 80,
|
|
10108
|
+
ceiling: 75
|
|
10109
|
+
},
|
|
10110
|
+
{
|
|
10111
|
+
minMass: 40,
|
|
10112
|
+
ceiling: 85
|
|
10113
|
+
},
|
|
10114
|
+
{
|
|
10115
|
+
minMass: 8,
|
|
10116
|
+
ceiling: 95
|
|
10117
|
+
}
|
|
10118
|
+
];
|
|
10119
|
+
/**
|
|
10120
|
+
* The score ceiling for a given evidence-discounted deduction mass.
|
|
10121
|
+
* 0 → no ceiling (the 100 honesty guard lives in clampWithFindings);
|
|
10122
|
+
* below the smallest band → no ceiling beyond the named guards.
|
|
10123
|
+
* Exported for the JSON contract (effectiveDeductions) and the tests.
|
|
10124
|
+
*/
|
|
10125
|
+
function massCeiling(totalDeduction) {
|
|
10126
|
+
for (const band of DEDUCTION_MASS_CEILINGS) if (totalDeduction >= band.minMass) return band.ceiling;
|
|
10127
|
+
return null;
|
|
10128
|
+
}
|
|
10077
10129
|
function computeTotal(dimensions, findings, exposure) {
|
|
10078
10130
|
if (findings.length === 0) return 100;
|
|
10079
10131
|
let totalDeduction = 0;
|
|
@@ -10086,6 +10138,8 @@ function computeTotal(dimensions, findings, exposure) {
|
|
|
10086
10138
|
const rate = totalDeduction / (declarations + 1);
|
|
10087
10139
|
score = clampWithFindings(100 - Math.min(100, rate * 5), totalDeduction);
|
|
10088
10140
|
} else score = clampWithFindings(100 - totalDeduction, totalDeduction);
|
|
10141
|
+
const ceiling = massCeiling(totalDeduction);
|
|
10142
|
+
if (ceiling !== null && score > ceiling) score = ceiling;
|
|
10089
10143
|
if (findings.some((f) => f.severity === "error" && deductionFor(f) > 0) && score > 95) score = 95;
|
|
10090
10144
|
return suiteVoided ? Math.min(score, 49) : score;
|
|
10091
10145
|
}
|
|
@@ -12322,6 +12376,7 @@ async function runScan$1(args, hooks = {}) {
|
|
|
12322
12376
|
});
|
|
12323
12377
|
const dimensions = computeDimensions(findings);
|
|
12324
12378
|
const rawDeductions = findings.reduce((sum, f) => sum + deductionFor(f), 0);
|
|
12379
|
+
const effectiveDeductions = rawDeductions;
|
|
12325
12380
|
const total = computeTotal(dimensions, findings, {
|
|
12326
12381
|
testDeclarations: testDeclarationCount,
|
|
12327
12382
|
testFileCount,
|
|
@@ -12346,6 +12401,7 @@ async function runScan$1(args, hooks = {}) {
|
|
|
12346
12401
|
testFileCount,
|
|
12347
12402
|
testDeclarationCount,
|
|
12348
12403
|
rawDeductions,
|
|
12404
|
+
effectiveDeductions,
|
|
12349
12405
|
suppressionCount,
|
|
12350
12406
|
...pluginsLoaded.length > 0 ? { plugins: pluginsLoaded } : {},
|
|
12351
12407
|
agenticProfile: computeAgenticProfile(fileProvenance, findings),
|
|
@@ -12900,6 +12956,10 @@ function appendScoreSection(lines, result, p, width, ascii) {
|
|
|
12900
12956
|
lines.push(` ${scoreGauge(result.score, p, gaugeWidth, ascii)}`);
|
|
12901
12957
|
lines.push(` ${p.dim(headlineFor(state, result.findings.length))}`);
|
|
12902
12958
|
if (result.rawDeductions !== void 0 && result.testDeclarationCount) lines.push(` ${p.dim(`(${result.rawDeductions} raw pts / ${result.testDeclarationCount} test declarations — normalized)`)}`);
|
|
12959
|
+
if (result.effectiveDeductions !== void 0 && result.score !== null) {
|
|
12960
|
+
const ceiling = massCeiling(result.effectiveDeductions);
|
|
12961
|
+
if (ceiling !== null && result.score <= ceiling && result.rawDeductions !== void 0) lines.push(` ${p.dim(`(capped: deduction mass ${result.effectiveDeductions} pts — absolute ceiling ${ceiling})`)}`);
|
|
12962
|
+
}
|
|
12903
12963
|
if (result.suppressionCount && result.suppressionCount > 0) lines.push(` ${p.dim(`(${result.suppressionCount} finding(s) suppressed by config)`)}`);
|
|
12904
12964
|
lines.push("");
|
|
12905
12965
|
}
|
|
@@ -13290,7 +13350,7 @@ function renderSarif(result, repoRootUri) {
|
|
|
13290
13350
|
tool: { driver: {
|
|
13291
13351
|
name: "Mjölnir",
|
|
13292
13352
|
informationUri: "https://github.com/Sergey-Bar/Mjolnir",
|
|
13293
|
-
version: "0.5.
|
|
13353
|
+
version: "0.5.34",
|
|
13294
13354
|
rules: [...rules.values()].map((r) => {
|
|
13295
13355
|
const meta = RULES.find((x) => x.id === r.id);
|
|
13296
13356
|
return {
|
|
@@ -15741,7 +15801,7 @@ const SUMMARY_SCRIPT_V1 = [
|
|
|
15741
15801
|
* the script via indentBlock(…, 10) inside the `run: |` scalar, so the
|
|
15742
15802
|
* raw unindented substring never appears in a real v1 file. */
|
|
15743
15803
|
function isKnownTemplate(content) {
|
|
15744
|
-
return GATES.some((g) => content === TEMPLATE(g)) || content.includes(indentBlock(SUMMARY_SCRIPT_V1, 10));
|
|
15804
|
+
return GATES.some((g) => content === TEMPLATE(g)) || GATES.some((g) => content === ACTION_TEMPLATE(g)) || content.includes(indentBlock(SUMMARY_SCRIPT_V1, 10));
|
|
15745
15805
|
}
|
|
15746
15806
|
/** Indents an embedded script so it sits inside a YAML `run: |` block scalar. */
|
|
15747
15807
|
function indentBlock(text, spaces) {
|
|
@@ -15854,6 +15914,127 @@ const GATES = [
|
|
|
15854
15914
|
"error",
|
|
15855
15915
|
"warning"
|
|
15856
15916
|
];
|
|
15917
|
+
/**
|
|
15918
|
+
* The action-ref the action-based template pins (P1: distribution).
|
|
15919
|
+
* `v1` is the major moving tag release.yml's action-tags job maintains on
|
|
15920
|
+
* every stable release — Marketplace convention. The generated workflow
|
|
15921
|
+
* pins the major, never @latest: a new release must not change gate
|
|
15922
|
+
* semantics without a commit of the consumer's.
|
|
15923
|
+
*/
|
|
15924
|
+
const ACTION_REF = "Sergey-Bar/Mjolnir@v1";
|
|
15925
|
+
/**
|
|
15926
|
+
* The action-based workflow for one gate level (P1.3): the root
|
|
15927
|
+
* action.yml does checkout-independent scanning — setup-node, the scan
|
|
15928
|
+
* itself (writing mjolnir.json for the reporting steps), and the gate
|
|
15929
|
+
* via the action's `fail-on` input. The action owns the gate: with
|
|
15930
|
+
* fail-on error/warning its step exits 1 on findings at the gate, so the
|
|
15931
|
+
* workflow needs no separate gate step; partial scans never block (the
|
|
15932
|
+
* action downgrades exit 2 to a loud warning, per the frozen exit-code
|
|
15933
|
+
* contract). Reporting steps run `if: always()` exactly like the npx
|
|
15934
|
+
* template. Advisory mode adds an explicit advisory note as the last
|
|
15935
|
+
* step so the job summary says "never blocking" in plain words.
|
|
15936
|
+
*/
|
|
15937
|
+
const ACTION_TEMPLATE = (gate) => `name: Mjölnir
|
|
15938
|
+
|
|
15939
|
+
on:
|
|
15940
|
+
pull_request:
|
|
15941
|
+
|
|
15942
|
+
concurrency:
|
|
15943
|
+
group: mjolnir-\${{ github.ref }}
|
|
15944
|
+
cancel-in-progress: true
|
|
15945
|
+
|
|
15946
|
+
permissions:
|
|
15947
|
+
contents: read
|
|
15948
|
+
pull-requests: write
|
|
15949
|
+
|
|
15950
|
+
jobs:
|
|
15951
|
+
scan:
|
|
15952
|
+
runs-on: ubuntu-latest
|
|
15953
|
+
# A hung scan must not sit for the 6-hour default.
|
|
15954
|
+
timeout-minutes: 10
|
|
15955
|
+
steps:
|
|
15956
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
15957
|
+
with:
|
|
15958
|
+
fetch-depth: 0 # needed for --scope changed merge-base
|
|
15959
|
+
# The action scans with the published mjolnir-qa package, pinned to
|
|
15960
|
+
# the EXACT version that generated this workflow — never a floating
|
|
15961
|
+
# tag: a new release must not change your gate semantics with no
|
|
15962
|
+
# commit of yours (same rule as the npx template). The fail-on
|
|
15963
|
+
# input is the gate: it fails the job on findings at the gate and
|
|
15964
|
+
# never on a partial scan (exit 2 downgrades to a warning — the
|
|
15965
|
+
# frozen exit-code contract). Advisory mode reports, never blocks.
|
|
15966
|
+
- name: Mjölnir verification trust scan
|
|
15967
|
+
id: mjolnir
|
|
15968
|
+
if: always()
|
|
15969
|
+
continue-on-error: ${gate === "advisory" ? "true" : "false"}
|
|
15970
|
+
uses: ${ACTION_REF}
|
|
15971
|
+
with:
|
|
15972
|
+
scope: changed
|
|
15973
|
+
format: json
|
|
15974
|
+
fail-on: ${gate === "advisory" ? "none" : gate}
|
|
15975
|
+
version: ${CLI_VERSION}
|
|
15976
|
+
# Reporting, not gating: runs even when the scan/gate failed, from
|
|
15977
|
+
# the same mjolnir.json the action wrote.
|
|
15978
|
+
- name: Annotations + Job Summary
|
|
15979
|
+
if: always()
|
|
15980
|
+
continue-on-error: true
|
|
15981
|
+
run: npx --yes mjolnir-qa@${CLI_VERSION} summary mjolnir.json
|
|
15982
|
+
- name: Render PR comment
|
|
15983
|
+
if: always()
|
|
15984
|
+
continue-on-error: true
|
|
15985
|
+
run: npx --yes mjolnir-qa@${CLI_VERSION} pr-comment . > mjolnir-comment.md
|
|
15986
|
+
# Best-effort: on a pull_request event from a fork the GITHUB_TOKEN is
|
|
15987
|
+
# read-only and this step will 403 for every external contributor. The
|
|
15988
|
+
# Job Summary above is the fallback that always renders.
|
|
15989
|
+
# (pull_request_target would fix the token but is a code-execution
|
|
15990
|
+
# risk — deliberately NOT used.)
|
|
15991
|
+
- name: Post or update PR comment
|
|
15992
|
+
if: always()
|
|
15993
|
+
continue-on-error: true
|
|
15994
|
+
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
|
15995
|
+
with:
|
|
15996
|
+
script: |
|
|
15997
|
+
const fs = require('fs');
|
|
15998
|
+
let body = '';
|
|
15999
|
+
try { body = fs.readFileSync('mjolnir-comment.md', 'utf8'); } catch (e) {}
|
|
16000
|
+
if (!body.trim()) {
|
|
16001
|
+
console.log('mjolnir-comment.md is empty or missing — nothing to post.');
|
|
16002
|
+
return;
|
|
16003
|
+
}
|
|
16004
|
+
const marker = '<!-- mjolnir-pr-comment -->';
|
|
16005
|
+
const listed = await github.rest.issues.listComments({
|
|
16006
|
+
owner: context.repo.owner,
|
|
16007
|
+
repo: context.repo.repo,
|
|
16008
|
+
issue_number: context.issue.number,
|
|
16009
|
+
});
|
|
16010
|
+
const existing = listed.data.find((c) => c.body?.startsWith(marker));
|
|
16011
|
+
if (existing) {
|
|
16012
|
+
await github.rest.issues.updateComment({
|
|
16013
|
+
owner: context.repo.owner,
|
|
16014
|
+
repo: context.repo.repo,
|
|
16015
|
+
comment_id: existing.id,
|
|
16016
|
+
body,
|
|
16017
|
+
});
|
|
16018
|
+
} else {
|
|
16019
|
+
await github.rest.issues.createComment({
|
|
16020
|
+
owner: context.repo.owner,
|
|
16021
|
+
repo: context.repo.repo,
|
|
16022
|
+
issue_number: context.issue.number,
|
|
16023
|
+
body,
|
|
16024
|
+
});
|
|
16025
|
+
}
|
|
16026
|
+
${gate === "advisory" ? ` # Advisory mode: findings are reported in the Job Summary,
|
|
16027
|
+
# never blocking — the action ran with fail-on: none and
|
|
16028
|
+
# continue-on-error, so even a crashed scan cannot fail this job.
|
|
16029
|
+
- name: Gate (advisory)
|
|
16030
|
+
if: always()
|
|
16031
|
+
run: echo "Advisory mode — findings reported, never blocking."` : ` # Gate enforcement: the action step above IS the gate — fail-on
|
|
16032
|
+
# ${gate} exits 1 on findings at the gate and the step is
|
|
16033
|
+
# continue-on-error: false, so its failure fails the job. A partial
|
|
16034
|
+
# scan never blocks (the action downgrades exit 2 to a warning per
|
|
16035
|
+
# the frozen exit-code contract). The reporting steps above ran
|
|
16036
|
+
# first (if: always()), so a red gate never suppresses the report.`}
|
|
16037
|
+
`;
|
|
15857
16038
|
/** Multiset line diff — counts only, for the refusal message. */
|
|
15858
16039
|
function summarizeContentDiff(existing, incoming) {
|
|
15859
16040
|
const remaining = /* @__PURE__ */ new Map();
|
|
@@ -15873,16 +16054,17 @@ function ciInstall(root, gate = "advisory", options = {}) {
|
|
|
15873
16054
|
const target = join(wfDir, "mjolnir.yml");
|
|
15874
16055
|
if (!existsSync(wfDir)) mkdirSync(wfDir, { recursive: true });
|
|
15875
16056
|
const existed = existsSync(target);
|
|
16057
|
+
const template = options.action === false ? TEMPLATE(gate) : ACTION_TEMPLATE(gate);
|
|
15876
16058
|
if (existed) {
|
|
15877
16059
|
const current = readFileSync(target, "utf8");
|
|
15878
16060
|
if (!isKnownTemplate(current) && !(options.force ?? false)) return {
|
|
15879
16061
|
written: target,
|
|
15880
16062
|
existed,
|
|
15881
16063
|
refused: true,
|
|
15882
|
-
diffSummary: summarizeContentDiff(current,
|
|
16064
|
+
diffSummary: summarizeContentDiff(current, template)
|
|
15883
16065
|
};
|
|
15884
16066
|
}
|
|
15885
|
-
writeFileSync(target,
|
|
16067
|
+
writeFileSync(target, template);
|
|
15886
16068
|
return {
|
|
15887
16069
|
written: target,
|
|
15888
16070
|
existed,
|
|
@@ -16037,9 +16219,13 @@ function writeBadge(result, options) {
|
|
|
16037
16219
|
const HELP_ENTRIES = [
|
|
16038
16220
|
{
|
|
16039
16221
|
verb: "ci install",
|
|
16040
|
-
summary: "generate the PR workflow (scan + annotations + gate)",
|
|
16041
|
-
usage: "mjolnir ci install [--gate advisory|error|warning] [--force]",
|
|
16042
|
-
examples: [
|
|
16222
|
+
summary: "generate the PR workflow (action-based by default; scan + annotations + gate)",
|
|
16223
|
+
usage: "mjolnir ci install [--gate advisory|error|warning] [--no-action] [--force]",
|
|
16224
|
+
examples: [
|
|
16225
|
+
"mjolnir ci install",
|
|
16226
|
+
"mjolnir ci install --gate error",
|
|
16227
|
+
"mjolnir ci install --no-action --gate error --force"
|
|
16228
|
+
],
|
|
16043
16229
|
next: "mjolnir --scope changed"
|
|
16044
16230
|
},
|
|
16045
16231
|
{
|
|
@@ -18662,7 +18848,7 @@ const { runScan, buildUniversalRules, fallbackWorkspace, pathMatchesGlob, isVali
|
|
|
18662
18848
|
* `scripts/sync-sarif-version.cjs` on release and guarded by
|
|
18663
18849
|
* `tests/version-consistency.spec.ts` locally.
|
|
18664
18850
|
*/
|
|
18665
|
-
const CLI_VERSION = "0.5.
|
|
18851
|
+
const CLI_VERSION = "0.5.34";
|
|
18666
18852
|
function parseArgs(argv, onError) {
|
|
18667
18853
|
const args = {
|
|
18668
18854
|
target: ".",
|
|
@@ -18835,9 +19021,11 @@ function runCiInstall(argv, io = {
|
|
|
18835
19021
|
let gateArg;
|
|
18836
19022
|
let gateSeen = false;
|
|
18837
19023
|
let force = false;
|
|
19024
|
+
let noAction = false;
|
|
18838
19025
|
const unknown = [];
|
|
18839
19026
|
for (const arg of argv) if (arg === "--gate") gateSeen = true;
|
|
18840
19027
|
else if (arg === "--force") force = true;
|
|
19028
|
+
else if (arg === "--no-action") noAction = true;
|
|
18841
19029
|
else if (gateSeen && gateArg === void 0 && !arg.startsWith("--")) gateArg = arg;
|
|
18842
19030
|
else unknown.push(arg);
|
|
18843
19031
|
if (gateSeen && gateArg === void 0) {
|
|
@@ -18856,7 +19044,10 @@ function runCiInstall(argv, io = {
|
|
|
18856
19044
|
io.err("Unknown gate level. Use: advisory | error | warning");
|
|
18857
19045
|
return 10;
|
|
18858
19046
|
}
|
|
18859
|
-
const result = ciInstall(resolve("."), gateArg ?? "advisory", {
|
|
19047
|
+
const result = ciInstall(resolve("."), gateArg ?? "advisory", {
|
|
19048
|
+
force,
|
|
19049
|
+
action: !noAction
|
|
19050
|
+
});
|
|
18860
19051
|
if (result.refused) {
|
|
18861
19052
|
io.err(`Refusing to overwrite the customized workflow at ${result.written}.`);
|
|
18862
19053
|
io.err("The file differs from the template Mjölnir would write:");
|
|
@@ -18865,8 +19056,9 @@ function runCiInstall(argv, io = {
|
|
|
18865
19056
|
return 10;
|
|
18866
19057
|
}
|
|
18867
19058
|
io.out(`${result.existed ? "Updated" : "Created"} ${result.written}`);
|
|
18868
|
-
io.out("Default mode: advisory — findings reported, never blocking.");
|
|
19059
|
+
io.out(noAction ? "Plain-npx template (—no-action). Default mode: advisory — findings reported, never blocking." : "Action-based template: uses Sergey-Bar/Mjolnir@v1 (major moving tag).");
|
|
18869
19060
|
io.out("Change with: mjolnir ci install --gate error|warning|advisory");
|
|
19061
|
+
if (!noAction) io.out("Prefer the plain-npx workflow? Re-run with --no-action.");
|
|
18870
19062
|
return 0;
|
|
18871
19063
|
}
|
|
18872
19064
|
/** Testable `suppressions` handler. */
|
package/dist/mcp/stdio.mjs
CHANGED
|
@@ -10073,6 +10073,58 @@ function computeDimensions(findings) {
|
|
|
10073
10073
|
for (const dim of byCategory.values()) dim.score = Math.max(0, 100 - deductions.get(dim.category));
|
|
10074
10074
|
return [...byCategory.values()].sort((a, b) => a.category.localeCompare(b.category));
|
|
10075
10075
|
}
|
|
10076
|
+
/**
|
|
10077
|
+
* Deduction-mass ceilings (product-gap-remediation master plan P2,
|
|
10078
|
+
* plan 1788853205786 — structural anti-dilution, decision 4).
|
|
10079
|
+
*
|
|
10080
|
+
* The Goodhart vector the density formula permits: fixed deduction mass
|
|
10081
|
+
* ÷ padded denominator → score → 99. A repo with 80 warning-points of
|
|
10082
|
+
* real findings and 10,000 test declarations reads 99+ ("one minor
|
|
10083
|
+
* issue") because the RATE is tiny — the padding is free. Density stays
|
|
10084
|
+
* as the legitimate differentiator for small masses (a lone warning in
|
|
10085
|
+
* a 10k suite must still read ≥ 95), but it can no longer dilute a
|
|
10086
|
+
* large mass: each absolute deduction-mass band caps the score. Padding
|
|
10087
|
+
* cannot move a ceiling — the vector is structurally closed. 80
|
|
10088
|
+
* warning-pts read ≤ 75 in ANY suite size.
|
|
10089
|
+
*
|
|
10090
|
+
* The error-severity floor (95) is the ≥ 8 band seen from the other
|
|
10091
|
+
* side: 1 error ≥ 8 pts, so it is subsumed and left standing as a named
|
|
10092
|
+
* special case for prose continuity. The > 0 band is the honesty guard
|
|
10093
|
+
* (clampWithFindings) — subsumed as well.
|
|
10094
|
+
*
|
|
10095
|
+
* Calibration (P2.4): these boundaries were chosen to hold the three
|
|
10096
|
+
* known data points (self 100, golden 49 via suiteVoided, demo 67) and
|
|
10097
|
+
* are then FROZEN with a changelog entry. Never tune them to flatter a
|
|
10098
|
+
* repo (docs/SCORING.md Correction section is the precedent).
|
|
10099
|
+
*/
|
|
10100
|
+
const DEDUCTION_MASS_CEILINGS = [
|
|
10101
|
+
{
|
|
10102
|
+
minMass: 160,
|
|
10103
|
+
ceiling: 65
|
|
10104
|
+
},
|
|
10105
|
+
{
|
|
10106
|
+
minMass: 80,
|
|
10107
|
+
ceiling: 75
|
|
10108
|
+
},
|
|
10109
|
+
{
|
|
10110
|
+
minMass: 40,
|
|
10111
|
+
ceiling: 85
|
|
10112
|
+
},
|
|
10113
|
+
{
|
|
10114
|
+
minMass: 8,
|
|
10115
|
+
ceiling: 95
|
|
10116
|
+
}
|
|
10117
|
+
];
|
|
10118
|
+
/**
|
|
10119
|
+
* The score ceiling for a given evidence-discounted deduction mass.
|
|
10120
|
+
* 0 → no ceiling (the 100 honesty guard lives in clampWithFindings);
|
|
10121
|
+
* below the smallest band → no ceiling beyond the named guards.
|
|
10122
|
+
* Exported for the JSON contract (effectiveDeductions) and the tests.
|
|
10123
|
+
*/
|
|
10124
|
+
function massCeiling(totalDeduction) {
|
|
10125
|
+
for (const band of DEDUCTION_MASS_CEILINGS) if (totalDeduction >= band.minMass) return band.ceiling;
|
|
10126
|
+
return null;
|
|
10127
|
+
}
|
|
10076
10128
|
function computeTotal(dimensions, findings, exposure) {
|
|
10077
10129
|
if (findings.length === 0) return 100;
|
|
10078
10130
|
let totalDeduction = 0;
|
|
@@ -10085,6 +10137,8 @@ function computeTotal(dimensions, findings, exposure) {
|
|
|
10085
10137
|
const rate = totalDeduction / (declarations + 1);
|
|
10086
10138
|
score = clampWithFindings(100 - Math.min(100, rate * 5), totalDeduction);
|
|
10087
10139
|
} else score = clampWithFindings(100 - totalDeduction, totalDeduction);
|
|
10140
|
+
const ceiling = massCeiling(totalDeduction);
|
|
10141
|
+
if (ceiling !== null && score > ceiling) score = ceiling;
|
|
10088
10142
|
if (findings.some((f) => f.severity === "error" && deductionFor(f) > 0) && score > 95) score = 95;
|
|
10089
10143
|
return suiteVoided ? Math.min(score, 49) : score;
|
|
10090
10144
|
}
|
|
@@ -12321,6 +12375,7 @@ async function runScan$1(args, hooks = {}) {
|
|
|
12321
12375
|
});
|
|
12322
12376
|
const dimensions = computeDimensions(findings);
|
|
12323
12377
|
const rawDeductions = findings.reduce((sum, f) => sum + deductionFor(f), 0);
|
|
12378
|
+
const effectiveDeductions = rawDeductions;
|
|
12324
12379
|
const total = computeTotal(dimensions, findings, {
|
|
12325
12380
|
testDeclarations: testDeclarationCount,
|
|
12326
12381
|
testFileCount,
|
|
@@ -12345,6 +12400,7 @@ async function runScan$1(args, hooks = {}) {
|
|
|
12345
12400
|
testFileCount,
|
|
12346
12401
|
testDeclarationCount,
|
|
12347
12402
|
rawDeductions,
|
|
12403
|
+
effectiveDeductions,
|
|
12348
12404
|
suppressionCount,
|
|
12349
12405
|
...pluginsLoaded.length > 0 ? { plugins: pluginsLoaded } : {},
|
|
12350
12406
|
agenticProfile: computeAgenticProfile(fileProvenance, findings),
|
|
@@ -12899,6 +12955,10 @@ function appendScoreSection(lines, result, p, width, ascii) {
|
|
|
12899
12955
|
lines.push(` ${scoreGauge(result.score, p, gaugeWidth, ascii)}`);
|
|
12900
12956
|
lines.push(` ${p.dim(headlineFor(state, result.findings.length))}`);
|
|
12901
12957
|
if (result.rawDeductions !== void 0 && result.testDeclarationCount) lines.push(` ${p.dim(`(${result.rawDeductions} raw pts / ${result.testDeclarationCount} test declarations — normalized)`)}`);
|
|
12958
|
+
if (result.effectiveDeductions !== void 0 && result.score !== null) {
|
|
12959
|
+
const ceiling = massCeiling(result.effectiveDeductions);
|
|
12960
|
+
if (ceiling !== null && result.score <= ceiling && result.rawDeductions !== void 0) lines.push(` ${p.dim(`(capped: deduction mass ${result.effectiveDeductions} pts — absolute ceiling ${ceiling})`)}`);
|
|
12961
|
+
}
|
|
12902
12962
|
if (result.suppressionCount && result.suppressionCount > 0) lines.push(` ${p.dim(`(${result.suppressionCount} finding(s) suppressed by config)`)}`);
|
|
12903
12963
|
lines.push("");
|
|
12904
12964
|
}
|
|
@@ -13289,7 +13349,7 @@ function renderSarif(result, repoRootUri) {
|
|
|
13289
13349
|
tool: { driver: {
|
|
13290
13350
|
name: "Mjölnir",
|
|
13291
13351
|
informationUri: "https://github.com/Sergey-Bar/Mjolnir",
|
|
13292
|
-
version: "0.5.
|
|
13352
|
+
version: "0.5.34",
|
|
13293
13353
|
rules: [...rules.values()].map((r) => {
|
|
13294
13354
|
const meta = RULES.find((x) => x.id === r.id);
|
|
13295
13355
|
return {
|
|
@@ -14838,7 +14898,7 @@ const SUMMARY_SCRIPT_V1 = [
|
|
|
14838
14898
|
* the script via indentBlock(…, 10) inside the `run: |` scalar, so the
|
|
14839
14899
|
* raw unindented substring never appears in a real v1 file. */
|
|
14840
14900
|
function isKnownTemplate(content) {
|
|
14841
|
-
return GATES.some((g) => content === TEMPLATE(g)) || content.includes(indentBlock(SUMMARY_SCRIPT_V1, 10));
|
|
14901
|
+
return GATES.some((g) => content === TEMPLATE(g)) || GATES.some((g) => content === ACTION_TEMPLATE(g)) || content.includes(indentBlock(SUMMARY_SCRIPT_V1, 10));
|
|
14842
14902
|
}
|
|
14843
14903
|
/** Indents an embedded script so it sits inside a YAML `run: |` block scalar. */
|
|
14844
14904
|
function indentBlock(text, spaces) {
|
|
@@ -14951,6 +15011,127 @@ const GATES = [
|
|
|
14951
15011
|
"error",
|
|
14952
15012
|
"warning"
|
|
14953
15013
|
];
|
|
15014
|
+
/**
|
|
15015
|
+
* The action-ref the action-based template pins (P1: distribution).
|
|
15016
|
+
* `v1` is the major moving tag release.yml's action-tags job maintains on
|
|
15017
|
+
* every stable release — Marketplace convention. The generated workflow
|
|
15018
|
+
* pins the major, never @latest: a new release must not change gate
|
|
15019
|
+
* semantics without a commit of the consumer's.
|
|
15020
|
+
*/
|
|
15021
|
+
const ACTION_REF = "Sergey-Bar/Mjolnir@v1";
|
|
15022
|
+
/**
|
|
15023
|
+
* The action-based workflow for one gate level (P1.3): the root
|
|
15024
|
+
* action.yml does checkout-independent scanning — setup-node, the scan
|
|
15025
|
+
* itself (writing mjolnir.json for the reporting steps), and the gate
|
|
15026
|
+
* via the action's `fail-on` input. The action owns the gate: with
|
|
15027
|
+
* fail-on error/warning its step exits 1 on findings at the gate, so the
|
|
15028
|
+
* workflow needs no separate gate step; partial scans never block (the
|
|
15029
|
+
* action downgrades exit 2 to a loud warning, per the frozen exit-code
|
|
15030
|
+
* contract). Reporting steps run `if: always()` exactly like the npx
|
|
15031
|
+
* template. Advisory mode adds an explicit advisory note as the last
|
|
15032
|
+
* step so the job summary says "never blocking" in plain words.
|
|
15033
|
+
*/
|
|
15034
|
+
const ACTION_TEMPLATE = (gate) => `name: Mjölnir
|
|
15035
|
+
|
|
15036
|
+
on:
|
|
15037
|
+
pull_request:
|
|
15038
|
+
|
|
15039
|
+
concurrency:
|
|
15040
|
+
group: mjolnir-\${{ github.ref }}
|
|
15041
|
+
cancel-in-progress: true
|
|
15042
|
+
|
|
15043
|
+
permissions:
|
|
15044
|
+
contents: read
|
|
15045
|
+
pull-requests: write
|
|
15046
|
+
|
|
15047
|
+
jobs:
|
|
15048
|
+
scan:
|
|
15049
|
+
runs-on: ubuntu-latest
|
|
15050
|
+
# A hung scan must not sit for the 6-hour default.
|
|
15051
|
+
timeout-minutes: 10
|
|
15052
|
+
steps:
|
|
15053
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
15054
|
+
with:
|
|
15055
|
+
fetch-depth: 0 # needed for --scope changed merge-base
|
|
15056
|
+
# The action scans with the published mjolnir-qa package, pinned to
|
|
15057
|
+
# the EXACT version that generated this workflow — never a floating
|
|
15058
|
+
# tag: a new release must not change your gate semantics with no
|
|
15059
|
+
# commit of yours (same rule as the npx template). The fail-on
|
|
15060
|
+
# input is the gate: it fails the job on findings at the gate and
|
|
15061
|
+
# never on a partial scan (exit 2 downgrades to a warning — the
|
|
15062
|
+
# frozen exit-code contract). Advisory mode reports, never blocks.
|
|
15063
|
+
- name: Mjölnir verification trust scan
|
|
15064
|
+
id: mjolnir
|
|
15065
|
+
if: always()
|
|
15066
|
+
continue-on-error: ${gate === "advisory" ? "true" : "false"}
|
|
15067
|
+
uses: ${ACTION_REF}
|
|
15068
|
+
with:
|
|
15069
|
+
scope: changed
|
|
15070
|
+
format: json
|
|
15071
|
+
fail-on: ${gate === "advisory" ? "none" : gate}
|
|
15072
|
+
version: ${CLI_VERSION}
|
|
15073
|
+
# Reporting, not gating: runs even when the scan/gate failed, from
|
|
15074
|
+
# the same mjolnir.json the action wrote.
|
|
15075
|
+
- name: Annotations + Job Summary
|
|
15076
|
+
if: always()
|
|
15077
|
+
continue-on-error: true
|
|
15078
|
+
run: npx --yes mjolnir-qa@${CLI_VERSION} summary mjolnir.json
|
|
15079
|
+
- name: Render PR comment
|
|
15080
|
+
if: always()
|
|
15081
|
+
continue-on-error: true
|
|
15082
|
+
run: npx --yes mjolnir-qa@${CLI_VERSION} pr-comment . > mjolnir-comment.md
|
|
15083
|
+
# Best-effort: on a pull_request event from a fork the GITHUB_TOKEN is
|
|
15084
|
+
# read-only and this step will 403 for every external contributor. The
|
|
15085
|
+
# Job Summary above is the fallback that always renders.
|
|
15086
|
+
# (pull_request_target would fix the token but is a code-execution
|
|
15087
|
+
# risk — deliberately NOT used.)
|
|
15088
|
+
- name: Post or update PR comment
|
|
15089
|
+
if: always()
|
|
15090
|
+
continue-on-error: true
|
|
15091
|
+
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
|
15092
|
+
with:
|
|
15093
|
+
script: |
|
|
15094
|
+
const fs = require('fs');
|
|
15095
|
+
let body = '';
|
|
15096
|
+
try { body = fs.readFileSync('mjolnir-comment.md', 'utf8'); } catch (e) {}
|
|
15097
|
+
if (!body.trim()) {
|
|
15098
|
+
console.log('mjolnir-comment.md is empty or missing — nothing to post.');
|
|
15099
|
+
return;
|
|
15100
|
+
}
|
|
15101
|
+
const marker = '<!-- mjolnir-pr-comment -->';
|
|
15102
|
+
const listed = await github.rest.issues.listComments({
|
|
15103
|
+
owner: context.repo.owner,
|
|
15104
|
+
repo: context.repo.repo,
|
|
15105
|
+
issue_number: context.issue.number,
|
|
15106
|
+
});
|
|
15107
|
+
const existing = listed.data.find((c) => c.body?.startsWith(marker));
|
|
15108
|
+
if (existing) {
|
|
15109
|
+
await github.rest.issues.updateComment({
|
|
15110
|
+
owner: context.repo.owner,
|
|
15111
|
+
repo: context.repo.repo,
|
|
15112
|
+
comment_id: existing.id,
|
|
15113
|
+
body,
|
|
15114
|
+
});
|
|
15115
|
+
} else {
|
|
15116
|
+
await github.rest.issues.createComment({
|
|
15117
|
+
owner: context.repo.owner,
|
|
15118
|
+
repo: context.repo.repo,
|
|
15119
|
+
issue_number: context.issue.number,
|
|
15120
|
+
body,
|
|
15121
|
+
});
|
|
15122
|
+
}
|
|
15123
|
+
${gate === "advisory" ? ` # Advisory mode: findings are reported in the Job Summary,
|
|
15124
|
+
# never blocking — the action ran with fail-on: none and
|
|
15125
|
+
# continue-on-error, so even a crashed scan cannot fail this job.
|
|
15126
|
+
- name: Gate (advisory)
|
|
15127
|
+
if: always()
|
|
15128
|
+
run: echo "Advisory mode — findings reported, never blocking."` : ` # Gate enforcement: the action step above IS the gate — fail-on
|
|
15129
|
+
# ${gate} exits 1 on findings at the gate and the step is
|
|
15130
|
+
# continue-on-error: false, so its failure fails the job. A partial
|
|
15131
|
+
# scan never blocks (the action downgrades exit 2 to a warning per
|
|
15132
|
+
# the frozen exit-code contract). The reporting steps above ran
|
|
15133
|
+
# first (if: always()), so a red gate never suppresses the report.`}
|
|
15134
|
+
`;
|
|
14954
15135
|
/** Multiset line diff — counts only, for the refusal message. */
|
|
14955
15136
|
function summarizeContentDiff(existing, incoming) {
|
|
14956
15137
|
const remaining = /* @__PURE__ */ new Map();
|
|
@@ -14970,16 +15151,17 @@ function ciInstall(root, gate = "advisory", options = {}) {
|
|
|
14970
15151
|
const target = join(wfDir, "mjolnir.yml");
|
|
14971
15152
|
if (!existsSync(wfDir)) mkdirSync(wfDir, { recursive: true });
|
|
14972
15153
|
const existed = existsSync(target);
|
|
15154
|
+
const template = options.action === false ? TEMPLATE(gate) : ACTION_TEMPLATE(gate);
|
|
14973
15155
|
if (existed) {
|
|
14974
15156
|
const current = readFileSync(target, "utf8");
|
|
14975
15157
|
if (!isKnownTemplate(current) && !(options.force ?? false)) return {
|
|
14976
15158
|
written: target,
|
|
14977
15159
|
existed,
|
|
14978
15160
|
refused: true,
|
|
14979
|
-
diffSummary: summarizeContentDiff(current,
|
|
15161
|
+
diffSummary: summarizeContentDiff(current, template)
|
|
14980
15162
|
};
|
|
14981
15163
|
}
|
|
14982
|
-
writeFileSync(target,
|
|
15164
|
+
writeFileSync(target, template);
|
|
14983
15165
|
return {
|
|
14984
15166
|
written: target,
|
|
14985
15167
|
existed,
|
|
@@ -15228,9 +15410,13 @@ function writeBadge(result, options) {
|
|
|
15228
15410
|
const HELP_ENTRIES = [
|
|
15229
15411
|
{
|
|
15230
15412
|
verb: "ci install",
|
|
15231
|
-
summary: "generate the PR workflow (scan + annotations + gate)",
|
|
15232
|
-
usage: "mjolnir ci install [--gate advisory|error|warning] [--force]",
|
|
15233
|
-
examples: [
|
|
15413
|
+
summary: "generate the PR workflow (action-based by default; scan + annotations + gate)",
|
|
15414
|
+
usage: "mjolnir ci install [--gate advisory|error|warning] [--no-action] [--force]",
|
|
15415
|
+
examples: [
|
|
15416
|
+
"mjolnir ci install",
|
|
15417
|
+
"mjolnir ci install --gate error",
|
|
15418
|
+
"mjolnir ci install --no-action --gate error --force"
|
|
15419
|
+
],
|
|
15234
15420
|
next: "mjolnir --scope changed"
|
|
15235
15421
|
},
|
|
15236
15422
|
{
|
|
@@ -18298,7 +18484,7 @@ const { runScan, buildUniversalRules, fallbackWorkspace, pathMatchesGlob, isVali
|
|
|
18298
18484
|
* `scripts/sync-sarif-version.cjs` on release and guarded by
|
|
18299
18485
|
* `tests/version-consistency.spec.ts` locally.
|
|
18300
18486
|
*/
|
|
18301
|
-
const CLI_VERSION = "0.5.
|
|
18487
|
+
const CLI_VERSION = "0.5.34";
|
|
18302
18488
|
function parseArgs(argv, onError) {
|
|
18303
18489
|
const args = {
|
|
18304
18490
|
target: ".",
|
|
@@ -18471,9 +18657,11 @@ function runCiInstall(argv, io = {
|
|
|
18471
18657
|
let gateArg;
|
|
18472
18658
|
let gateSeen = false;
|
|
18473
18659
|
let force = false;
|
|
18660
|
+
let noAction = false;
|
|
18474
18661
|
const unknown = [];
|
|
18475
18662
|
for (const arg of argv) if (arg === "--gate") gateSeen = true;
|
|
18476
18663
|
else if (arg === "--force") force = true;
|
|
18664
|
+
else if (arg === "--no-action") noAction = true;
|
|
18477
18665
|
else if (gateSeen && gateArg === void 0 && !arg.startsWith("--")) gateArg = arg;
|
|
18478
18666
|
else unknown.push(arg);
|
|
18479
18667
|
if (gateSeen && gateArg === void 0) {
|
|
@@ -18492,7 +18680,10 @@ function runCiInstall(argv, io = {
|
|
|
18492
18680
|
io.err("Unknown gate level. Use: advisory | error | warning");
|
|
18493
18681
|
return 10;
|
|
18494
18682
|
}
|
|
18495
|
-
const result = ciInstall(resolve("."), gateArg ?? "advisory", {
|
|
18683
|
+
const result = ciInstall(resolve("."), gateArg ?? "advisory", {
|
|
18684
|
+
force,
|
|
18685
|
+
action: !noAction
|
|
18686
|
+
});
|
|
18496
18687
|
if (result.refused) {
|
|
18497
18688
|
io.err(`Refusing to overwrite the customized workflow at ${result.written}.`);
|
|
18498
18689
|
io.err("The file differs from the template Mjölnir would write:");
|
|
@@ -18501,8 +18692,9 @@ function runCiInstall(argv, io = {
|
|
|
18501
18692
|
return 10;
|
|
18502
18693
|
}
|
|
18503
18694
|
io.out(`${result.existed ? "Updated" : "Created"} ${result.written}`);
|
|
18504
|
-
io.out("Default mode: advisory — findings reported, never blocking.");
|
|
18695
|
+
io.out(noAction ? "Plain-npx template (—no-action). Default mode: advisory — findings reported, never blocking." : "Action-based template: uses Sergey-Bar/Mjolnir@v1 (major moving tag).");
|
|
18505
18696
|
io.out("Change with: mjolnir ci install --gate error|warning|advisory");
|
|
18697
|
+
if (!noAction) io.out("Prefer the plain-npx workflow? Re-run with --no-action.");
|
|
18506
18698
|
return 0;
|
|
18507
18699
|
}
|
|
18508
18700
|
/** Testable `suppressions` handler. */
|
package/package.json
CHANGED