mjolnir-qa 0.5.4 → 0.5.6

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 CHANGED
@@ -9,7 +9,7 @@ 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.4] — 2026-09-06
12
+ # [0.5.4] — 2026-09-06
13
13
 
14
14
  ### Agent Handoff + Minimized Reporting (plan 1788599400000)
15
15
 
@@ -74,6 +74,93 @@ scan`. Unknown categories are a usage error (exit 10).
74
74
  - help registry gained `why`, `handoff`, `install` and the new flags;
75
75
  site/reference/cli.md documents the handoff trust model.
76
76
 
77
+ ## [0.5.6] — 2026-09-06
78
+
79
+ ### Changes since 0.5.5
80
+
81
+ - Merge pull request #39 from Sergey-Bar/eng/verification-trust-2.0
82
+ - docs: regenerate readme SVGs under the revision-2 CI-rule measurements
83
+ - docs: regenerate rule-doc occurrence tables (yarnpkg-berry corpus lane)
84
+ - fix: eslint pragma for the file-wide consumer fallback regex
85
+ - fix: rev-2 enforcement arm in QA-CI-008 was dead code + coverage arms for CI rules
86
+ - ci-family trust repair: M2 detector re-adjudication, revision 2 re-measurement, workflow corpus lane
87
+
88
+ ## [0.5.5] — 2026-09-06
89
+
90
+ ### Audit Remediation 1.0 (engine correctness + trust boundary)
91
+
92
+ ### Changed — plugin execution gate (audit C2, contract-visible, pre-1.0)
93
+
94
+ - **`--enable-plugins` / `MJOLNIR_ENABLE_PLUGINS=1`** — npm plugins and
95
+ JS-module external rules (`mjolnir-rules/*.mjs`) now load and execute
96
+ only behind an explicit opt-in (default OFF). Declared-but-gated
97
+ sources are listed on a loud stderr notice; they are never imported.
98
+ JSON rule manifests execute no code and load without the gate. See
99
+ SECURITY.md (trust model) and docs/VERSIONING.md (gate contract).
100
+
101
+ ### Fixed — engine correctness (audit M1)
102
+
103
+ - **C1/W9 (`--cache`)**: the cache key folds in the repo-relative path,
104
+ adapter id, and parse mode — byte-identical files no longer share
105
+ verdicts (findings carried the first-scanned file's path), and
106
+ regex-fallback verdicts are never served as AST ones. `CACHE_VERSION`
107
+ bumped to 2: one-time invalidation, first post-upgrade scan cold.
108
+ - **C3**: the default console sinks are variadic —
109
+ "mjolnir internal error:" now carries the actual cause (the message
110
+ used to be dropped).
111
+ - **C5**: a partial (truncated) scan never writes the
112
+ first-clean-scan milestone; `mjolnir diff` on a truncated head
113
+ returns exit 2 and folds no resolved findings into stats.
114
+ - **W1**: the Java/C# maskers keep code after a closed block comment
115
+ live (`/*x*/y` used to blank `y`).
116
+ - **W2/W3**: parse-semaphore re-check (cap holds under fan-out) and
117
+ parser-creation retry on rejection (a transient WASM load failure no
118
+ longer disables the AST path for the process).
119
+ - **W4**: unchecked grammar-shape casts removed in the QA-model
120
+ extractor; `parseTsFile`'s undefined contract honored.
121
+ - **W8**: runtime corroboration claims "test" level only when the
122
+ finding's line falls inside the verdict's span; lineless reports
123
+ degrade to file level.
124
+ - **W10**: malformed rule records at the rule→Finding boundary are
125
+ rejected with a diagnostic (crash channel), never silently scored.
126
+ - **S5 (QA-CI-001, detectorRevision 2)**: step-level line resolution
127
+ falls back to the anchor line instead of crashing — the rule no
128
+ longer vanishes into crash isolation on workflows where the raw
129
+ `continue-on-error: true` literal sits outside the search window.
130
+ Behavior changes from crash-dropped to reported; sidecar +
131
+ measured-FP regenerated per policy.
132
+ - **masking.ts**: outer-delimiter search fixes misclassification of
133
+ string values ending in a nested quote.
134
+ - **changed.ts**: hunk-content lines that look like diff headers no
135
+ longer abandon the hunk; one unreadable untracked file degrades only
136
+ itself (treated fully changed), never the whole scope.
137
+ - **shared-walk.ts**: unreadable directories and skipped symlinks are
138
+ counted skips with reasons; deadline/cap checks run inside the entry
139
+ loop.
140
+ - **workflow-parser.ts**: the documented nesting-depth cap is enforced;
141
+ `jobs` builds on a null-prototype object; step `with` is copied.
142
+
143
+ ### Changed — suppressions expiry (audit S4, contract-visible, pre-1.0)
144
+
145
+ - The 90-day default for hand-authored `ignore` entries is no longer
146
+ anchored at the config file's mtime — any config edit used to reset
147
+ every suppression window. Expiry is the entry's explicit `expires`
148
+ date; entries without one stay active and are labeled accordingly in
149
+ `mjolnir suppressions`.
150
+
151
+ ### Hardened — trust boundary (audits S1/S2/S3/S7)
152
+
153
+ - **S1**: git resolves to an absolute path from PATH (never the scanned
154
+ CWD) — a planted `git.exe`/`git.bat` cannot hijack Mjölnir's git
155
+ calls on Windows.
156
+ - **S2**: ignore/glob patterns and external JSON-rule regexes are
157
+ length/wildcard-capped at compile time; `**/` compiles segment-aware.
158
+ - **S3**: config/ignore/plugins resolve from the explicit scan target's
159
+ project; the resolved anchor prints in verbose mode.
160
+ - **S7**: `ignore[].files` must be `string[]` (exit 10 on typo);
161
+ unknown top-level config keys warn; baseline `schemaVersion` is
162
+ checked (future versions degrade to "no baseline" with a warning).
163
+
77
164
  ## [0.5.3] — 2026-09-05
78
165
 
79
166
  ### Terminal + CI UX Overhaul (plan 1788579907109)
@@ -224,6 +311,8 @@ rules… → Scoring…`) fed by the new additive `ScanHooks.onProgress`.
224
311
 
225
312
  ### Added — folder-based external rules, zero network
226
313
 
314
+ ### Added — folder-based external rules, zero network
315
+
227
316
  - **`mjolnir-rules/` contract** (`src/plugins/local-rules.ts`): a
228
317
  workspace directory loaded from the scan target root alongside npm
229
318
  plugins. Two file kinds: **JSON rule manifests** (declarative regex
package/README.md CHANGED
@@ -284,13 +284,13 @@ firewall.
284
284
 
285
285
  ### How much of this is measured
286
286
 
287
- **74 of 99 rules carry a false-positive rate measured against real OSS code** (≥ 10 hand-classified findings each; see
288
- [docs/FP-AUDIT.md](docs/FP-AUDIT.md)). The other 19 ship on the author's
287
+ **76 of 99 rules carry a false-positive rate measured against real OSS code** (≥ 10 hand-classified findings each; see
288
+ [docs/FP-AUDIT.md](docs/FP-AUDIT.md)). The other 23 ship on the author's
289
289
  estimate. Every scan footer tells you how many of the rules that _fired_
290
290
  are measured; `mjolnir rules --unmeasured` lists the ones that aren't;
291
291
  every rule's `mjolnir explain` page states its status. We publish the rate
292
292
  even when it's ugly — QA-CS-103 audits at 95% and is quarantined for it.
293
- Growing that 78 is the project's continuing work.
293
+ Growing that number is the project's continuing work.
294
294
 
295
295
  ### Rule tiers and language maturity
296
296
 
@@ -558,10 +558,17 @@ are immutable once shipped and never reused.
558
558
  real OSS code ship in the headline tiers (see
559
559
  [How much of this is measured](#how-much-of-this-is-measured)); the scan
560
560
  footer and `mjolnir rules --unmeasured` tell you which is which.
561
- - **Plugin trust** — plugins are npm packages declared under `"plugins"`.
562
- There is **no sandbox**: plugin code runs with full Node privileges, the
563
- same trust model as ESLint or Vitest plugins. Core rule-ID prefixes are
564
- reserved and rejected from plugins to prevent spoofing.
561
+ - **Plugin trust & execution gate** — plugins are npm packages declared
562
+ under `"plugins"`; JS modules live in `mjolnir-rules/*.mjs`. There is
563
+ **no sandbox**: plugin code runs with full Node privileges, the same
564
+ trust model as ESLint or Vitest plugins. Because of that, code execution
565
+ is **opt-in at every scan**: pass `--enable-plugins` (or set
566
+ `MJOLNIR_ENABLE_PLUGINS=1`) or the sources are NOT loaded — a loud
567
+ stderr notice lists exactly what was skipped. Scanning untrusted code
568
+ never executes it. JSON rule manifests (`mjolnir-rules/*.json`) are
569
+ unaffected: they declare regex patterns and execute no code by design.
570
+ Core rule-ID prefixes are reserved and rejected from plugins and
571
+ external rules to prevent spoofing.
565
572
  - **Workspace-local external rules** (folder-based, zero network) — a
566
573
  `mjolnir-rules/` directory next to the scan target loads custom rules:
567
574
  JSON files declare regex patterns (no code executed), `.mjs`/`.js`
package/dist/cli.d.mts CHANGED
@@ -33,7 +33,7 @@ type EvidenceLevel = (typeof EVIDENCE_ORDER)[number];
33
33
  */
34
34
  type QaImpact = "BLOCKS-RELEASE" | "FLAKY-RISK" | "FALSE-GREEN" | "HYGIENE";
35
35
  /** Rule namespaces are frozen public API (§18.4). IDs are never reused. */
36
- type RuleCategory = "QA-TEST" | "QA-TQUAL" | "QA-PW" | "QA-CI";
36
+ type RuleCategory = "QA-TEST" | "QA-TQUAL" | "QA-PW" | "QA-CI" | "QA-PY" | "QA-ENV" | "QA-JV" | "QA-CS" | "QA-CYP" | "QA-SE" | "QA-WDIO" | "QA-PPTR" | "QA-APM";
37
37
  /**
38
38
  * Trust levels (Verification Trust Evolution Plan §16): the OVERALL
39
39
  * trust a consumer can place in one finding, combining the static
@@ -248,6 +248,20 @@ interface ScanResult {
248
248
  };
249
249
  }
250
250
  //#endregion
251
+ //#region src/engine/tier-policy.d.ts
252
+ type Tier = "core" | "extended" | "quarantine";
253
+ //#endregion
254
+ //#region src/engine/overlap-dedup.d.ts
255
+ /** Per-rule metadata the dedup pass needs, built from RULES. */
256
+ interface OverlapMeta {
257
+ /** Rule IDs this rule can double-report on the same root cause. */
258
+ overlapWith?: string[];
259
+ /** Tier rank for survivor resolution (missing = core). */
260
+ tier?: Tier;
261
+ /** RULES registry order for final tie-breaking. */
262
+ order?: number;
263
+ }
264
+ //#endregion
251
265
  //#region src/discovery/workspace.d.ts
252
266
  /**
253
267
  * Repository discovery (Sprint-Plan W1-03).
@@ -263,9 +277,6 @@ interface Workspace {
263
277
  workspaceGlobs: string[];
264
278
  }
265
279
  //#endregion
266
- //#region src/engine/tier-policy.d.ts
267
- type Tier = "core" | "extended" | "quarantine";
268
- //#endregion
269
280
  //#region src/rules/rule.d.ts
270
281
  /**
271
282
  * How the rule's primary detection decision is made (Verification Trust
@@ -505,19 +516,13 @@ interface UniversalRule {
505
516
  run(file: ParsedFile): Array<Omit<Finding, "ruleId" | "category">>;
506
517
  }
507
518
  //#endregion
508
- //#region src/cli.d.ts
509
- /**
510
- * Tool version for `mjolnir --version`.
511
- *
512
- * A literal, not a package.json read: the shipped artifact is a single
513
- * bundled `dist/cli.mjs`, so resolving package.json at runtime depends on
514
- * where the file happens to sit after install. This follows the same
515
- * discipline as SARIF's `driver.version` — kept in sync by
516
- * `scripts/sync-sarif-version.cjs` on release and guarded by
517
- * `tests/version-consistency.spec.ts` locally.
518
- */
519
- declare const CLI_VERSION = "0.5.4";
520
- declare function buildUniversalRules(root: string, strict?: boolean): Promise<{
519
+ //#region src/engine/scan-pipeline.d.ts
520
+ declare function buildUniversalRules$1(root: string, strict?: boolean, opts?: {
521
+ /** CLI trust-gate flag (`--enable-plugins`) for this scan. */
522
+ enablePlugins?: boolean;
523
+ /** Called with the gate notice when code sources were skipped. */
524
+ onGateNotice?: (notice: string) => void;
525
+ }): Promise<{
521
526
  rules: UniversalRule[];
522
527
  pluginErrors: string[];
523
528
  tierByRuleId: Map<string, Tier>;
@@ -561,6 +566,20 @@ interface CliArgs {
561
566
  * and auto-disabled in CI/machine formats; this flag is the manual off.
562
567
  */
563
568
  noProgress?: boolean;
569
+ /**
570
+ * --staged: scan-surface restriction (plan §5.7) — intersect the
571
+ * discovered test files with the git staged file list. Does NOT
572
+ * change rule semantics; score implications come only from the
573
+ * narrowed surface (labeled as such in the report).
574
+ */
575
+ staged?: boolean;
576
+ /**
577
+ * --blocking error|warning|none (plan §5.8): invocation-level gate
578
+ * override. Controls PROCESS EXIT BEHAVIOR ONLY via the existing
579
+ * exitForFindings mechanism — detection, rendering, JSON and score
580
+ * are identical under all three values. Overrides config.gate.
581
+ */
582
+ blocking?: "error" | "warning" | "none";
564
583
  /**
565
584
  * --category <cat> (repeatable): presentation filter — narrows the
566
585
  * TERMINAL findings display (and handoff/why output) to the given
@@ -575,38 +594,13 @@ interface CliArgs {
575
594
  */
576
595
  scoreOnly?: boolean;
577
596
  /**
578
- * --staged: scan-surface restriction (plan §5.7) intersect the
579
- * discovered test files with the git staged file list. Does NOT
580
- * change rule semantics; score implications come only from the
581
- * narrowed surface (labeled as such in the report).
582
- */
583
- staged?: boolean;
584
- /**
585
- * --blocking error|warning|none (plan §5.8): invocation-level gate
586
- * override. Controls PROCESS EXIT BEHAVIOR ONLY via the existing
587
- * exitForFindings mechanism — detection, rendering, JSON and score
588
- * are identical under all three values. Overrides config.gate.
597
+ * Audit C2: --enable-plugins opens the plugin trust gate for THIS
598
+ * invocation npm-plugin and JS-module rule sources may load (and
599
+ * execute). Default OFF; MJOLNIR_ENABLE_PLUGINS=1 is the env
600
+ * equivalent. JSON rule manifests are unaffected (no code by design).
589
601
  */
590
- blocking?: "error" | "warning" | "none";
602
+ enablePlugins?: boolean;
591
603
  }
592
- /** A usage-error detail: the offending token, when one exists. */
593
- interface UsageErrorDetail {
594
- /** The unknown flag or rejected value (e.g. `--nope`, `loud`). */
595
- token?: string | undefined;
596
- /** The flag whose value was rejected (`--tone` for `--tone loud`). */
597
- flag?: string | undefined;
598
- }
599
- declare function parseArgs(argv: string[], onError?: (detail: UsageErrorDetail) => void): CliArgs | null;
600
- /** Hand-rolled Levenshtein distance (plan M2: no new dependencies). */
601
- declare function levenshtein(a: string, b: string): number;
602
- /** Nearest known flags within distance ≤ 2, nearest first. */
603
- declare function nearestFlags(flag: string, max?: number): string[];
604
- /**
605
- * Friendly usage error (plan M2, exit 10 preserved): nearest-flag
606
- * suggestion, the valid neighbors, and the exact help command. Printed
607
- * to stderr; findings/usage stay on their documented streams.
608
- */
609
- declare function usageErrorMessage(detail: UsageErrorDetail): string;
610
604
  interface ScanHooks {
611
605
  /** Invoked when a rule throws on a file (audit R-9). */
612
606
  onRuleCrash?: (ruleId: string, file: string, error: unknown) => void;
@@ -624,6 +618,13 @@ interface ScanHooks {
624
618
  total?: number | undefined;
625
619
  detail?: string | undefined;
626
620
  }) => void;
621
+ /**
622
+ * Audit C2: invoked when code-executing rule sources were skipped
623
+ * because the plugin trust gate is closed. Default (no hook): the
624
+ * gate notice is written straight to stderr — loud in every verb,
625
+ * never on the stdout machine contracts.
626
+ */
627
+ onGateNotice?: (notice: string) => void;
627
628
  }
628
629
  /**
629
630
  * Workspace fallback for targets with no discoverable project root
@@ -631,7 +632,40 @@ interface ScanHooks {
631
632
  * root-path degenerate case (`C:\` → basename "") is testable without
632
633
  * scanning a filesystem root.
633
634
  */
634
- declare function fallbackWorkspace(targetAbs: string): Workspace;
635
+ declare function fallbackWorkspace$1(targetAbs: string): Workspace;
636
+ /**
637
+ * Minimal glob match for suppression `files` patterns, with gitignore
638
+ * `**` semantics (bug-audit M5). Supports:
639
+ * "tests/**" — everything inside tests/
640
+ * "tests" + "/**\/*.spec.ts" — any depth UNDER tests/ (including none) ending in .spec.ts
641
+ * "**" + "/*.spec.ts" — any depth including root-level files
642
+ * "tests/foo.spec.ts" — exact path
643
+ * "*" within a segment never crosses "/".
644
+ *
645
+ * Forward slashes only (findings always use normalized paths). `?`,
646
+ * character classes and `!` negation are not metacharacters here — same
647
+ * as before this rewrite.
648
+ */
649
+ declare function pathMatchesGlob$1(path: string, glob: string): boolean;
650
+ /**
651
+ * Plan §16: locate a runtime run report next to the scan target, using
652
+ * the exact conventions the forensics ingestion already accepts —
653
+ * `mjolnir.report.json` (the packages/playwright-reporter default
654
+ * output) or a `test-results/` directory. Returns the path for
655
+ * `runForensics`, or undefined when neither convention is present
656
+ * ("no runtime evidence" — never guessed).
657
+ */
658
+ declare function discoverRuntimeReport$1(scanRoot: string): string | undefined;
659
+ /**
660
+ * Audit W10 — runtime shape validation at the rule→Finding boundary.
661
+ * A finding record coming out of a rule (plugin/JSON-manifest rules are
662
+ * external data, not trusted internal code) must carry the fields the
663
+ * whole downstream pipeline indexes on: severity within the enum,
664
+ * integer line ≥ 1, non-empty message, and a file path. Malformed
665
+ * records are rejected (routed to the crash/plugin-error channel by the
666
+ * caller) — never silently scored.
667
+ */
668
+ declare function isValidFindingRecord$1(f: unknown): f is Omit<Finding, "ruleId" | "category">;
635
669
  /**
636
670
  * Testable default scan path core. `hooks` lets callers observe
637
671
  * normally-invisible events (swallowed rule crashes) without changing
@@ -642,9 +676,42 @@ declare function fallbackWorkspace(targetAbs: string): Workspace;
642
676
  * inherently async); `runRules` and every rule stay synchronous and
643
677
  * consume `ParsedFile.ast`. Callers await the returned promise.
644
678
  */
645
- declare function runScan(args: CliArgs, hooks?: ScanHooks): Promise<ScanResult>;
679
+ declare function runScan$1(args: CliArgs, hooks?: ScanHooks): Promise<ScanResult>;
680
+ //#endregion
681
+ //#region src/cli.d.ts
682
+ declare const runScan: typeof runScan$1, buildUniversalRules: typeof buildUniversalRules$1, fallbackWorkspace: typeof fallbackWorkspace$1, pathMatchesGlob: typeof pathMatchesGlob$1, isValidFindingRecord: typeof isValidFindingRecord$1, discoverRuntimeReport: typeof discoverRuntimeReport$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>;
683
+ /**
684
+ * Tool version for `mjolnir --version`.
685
+ *
686
+ * A literal, not a package.json read: the shipped artifact is a single
687
+ * bundled `dist/cli.mjs`, so resolving package.json at runtime depends on
688
+ * where the file happens to sit after install. This follows the same
689
+ * discipline as SARIF's `driver.version` — kept in sync by
690
+ * `scripts/sync-sarif-version.cjs` on release and guarded by
691
+ * `tests/version-consistency.spec.ts` locally.
692
+ */
693
+ declare const CLI_VERSION = "0.5.6";
694
+ /** A usage-error detail: the offending token, when one exists. */
695
+ interface UsageErrorDetail {
696
+ /** The unknown flag or rejected value (e.g. `--nope`, `loud`). */
697
+ token?: string | undefined;
698
+ /** The flag whose value was rejected (`--tone` for `--tone loud`). */
699
+ flag?: string | undefined;
700
+ }
701
+ declare function parseArgs(argv: string[], onError?: (detail: UsageErrorDetail) => void): CliArgs | null;
702
+ /** Hand-rolled Levenshtein distance (plan M2: no new dependencies). */
703
+ declare function levenshtein(a: string, b: string): number;
704
+ /** Nearest known flags within distance ≤ 2, nearest first. */
705
+ declare function nearestFlags(flag: string, max?: number): string[];
706
+ /**
707
+ * Friendly usage error (plan M2, exit 10 preserved): nearest-flag
708
+ * suggestion, the valid neighbors, and the exact help command. Printed
709
+ * to stderr; findings/usage stay on their documented streams.
710
+ */
711
+ declare function usageErrorMessage(detail: UsageErrorDetail): string;
646
712
  type Output = (...parts: unknown[]) => void;
647
- declare function pathMatchesGlob(path: string, glob: string): boolean;
713
+ declare const out: Output;
714
+ declare const err: Output;
648
715
  /** Testable `ci install` handler. Returns the process exit code. */
649
716
  declare function runCiInstall(argv: string[], io?: {
650
717
  out: Output;
@@ -787,4 +854,4 @@ declare function runHelpCommand(argv: string[], io?: {
787
854
  declare function internalErrorMessage(err: unknown, emit: (s: string) => void, debug: boolean): void;
788
855
  declare function isEntryPoint(): boolean;
789
856
  //#endregion
790
- export { CLI_VERSION, Output, ScanHooks, UsageErrorDetail, buildUniversalRules, exitForFindings, fallbackWorkspace, internalErrorMessage, isEntryPoint, levenshtein, main, nearestFlags, parseArgs, pathMatchesGlob, runBadgeCommand, runBaselineCommand, runCiInstall, runCreateRuleCommand, runDebtCommand, runDiffCommand, runDoctorCommand, runDoctorPlaywright, runExplainCommand, runFixCommand, runForensicsCommand, runHandoverCommand, runHelpCommand, runImpactCommand, runInitCommand, runPrCommentCommand, runPwReportCommand, runRulesCommand, runScan, runScanCommand, runStatsCommand, runSuppressions, runTriageCommand, usageErrorMessage };
857
+ export { CLI_VERSION, type CliArgs, EVIDENCE_OVERRIDES, KNOWN_RULE_IDS, OVERLAP_META_BY_RULE_ID, Output, SUITE_INVALIDATING_RULE_IDS, type ScanHooks, UsageErrorDetail, buildUniversalRules, discoverRuntimeReport, err, exitForFindings, fallbackWorkspace, internalErrorMessage, isEntryPoint, isValidFindingRecord, levenshtein, main, nearestFlags, out, parseArgs, pathMatchesGlob, runBadgeCommand, runBaselineCommand, runCiInstall, runCreateRuleCommand, runDebtCommand, runDiffCommand, runDoctorCommand, runDoctorPlaywright, runExplainCommand, runFixCommand, runForensicsCommand, runHandoverCommand, runHelpCommand, runImpactCommand, runInitCommand, runPrCommentCommand, runPwReportCommand, runRulesCommand, runScan, runScanCommand, runStatsCommand, runSuppressions, runTriageCommand, usageErrorMessage };