mjolnir-qa 0.5.3 → 0.5.5

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,6 +9,147 @@ 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
13
+
14
+ ### Agent Handoff + Minimized Reporting (plan 1788599400000)
15
+
16
+ ### Added
17
+
18
+ - **`mjolnir why <file>:<line>`** — occurrence-level evidence query
19
+ (informational, NOT a gate): exact file+line match, severity icon,
20
+ message/why/fix, evidence level, trust level, measured FP rate
21
+ (or the honest "ships on assumption"), runtime corroboration when
22
+ present, and the suppression contract (reason required, 90-day
23
+ expiry). Saved-report mode (`--json <mjolnir.json>`) is
24
+ authoritative; live scan runs otherwise. Exit 0 match / 1 no match.
25
+ - **`mjolnir handoff [mjolnir.json]`** — the deterministic fix-handoff
26
+ artifact: per-rule remediation sections (what is wrong / why
27
+ Mjölnir believes it / evidence boundary by level / occurrences
28
+ capped at 25 / fix / constraints / occurrences list), a per-rule
29
+ fenced copy block and a one-shot handoff prompt, and the formal
30
+ verification contract (TARGET_RESOLVED / TARGET_REMAINS /
31
+ NEW_FINDINGS_INTRODUCED / VERIFICATION_NOT_RUN, correlated by the
32
+ fingerprint ruleId+file+message; the standing caveat that a clean
33
+ `--scope changed` run verifies the changed surface only). Generated
34
+ solely from Mjölnir's own rule metadata — offline, deterministic,
35
+ escapeMarkdown'd. Zero findings → exit 0, non-actionable clean
36
+ artifact with no prompt. `--category`/`--rules` are presentation
37
+ filters.
38
+ - **`mjolnir install`** — installs the agent instruction surfaces
39
+ (`.claude/commands/mjolnir.md`, `.kilo/command/mjolnir.md`,
40
+ `.cursor/rules/mjolnir.mdc`, marker-appended `AGENTS.md`): the
41
+ version-pinned trust loop brief (scan `--scope changed` before
42
+ finishing, never suppress to green, report files changed and checks
43
+ not run). `--staged-hook` adds a NON-BLOCKING pre-commit hook
44
+ (`mjolnir --staged --blocking warning`, reusing `.husky`/
45
+ `core.hooksPath` when present). Marker-based idempotency;
46
+ `--dry-run` writes nothing; refusal (exit 10) before overwriting
47
+ any non-Mjölnir file; `--force` overwrites only Mjölnir-marked
48
+ files; never @latest.
49
+ - **`--score`** — prints only the numeric score (`unknown` when no
50
+ tests exist — never a fake 0); pure rendering flag, exit code
51
+ unchanged; stderr note when --json was also requested.
52
+ - **`--category <cat>`** (repeatable) — presentation filter on the
53
+ terminal findings display (and handoff/why): NEVER filters the
54
+ scan, the JSON/SARIF output, or the score; the terminal prints
55
+ `filtered view: N of M findings shown; score reflects the full
56
+ scan`. Unknown categories are a usage error (exit 10).
57
+ - **`--staged`** — scan-surface restriction: intersects discovered
58
+ test files with the git staged list; score reflects the staged
59
+ surface and is labeled as such (`staged surface: N file(s)`); not a
60
+ git repo → honest degraded fallback; empty staged set → exit 0.
61
+ - **`--blocking error|warning|none`** — exit-status override only:
62
+ maps onto the existing gate model (none→advisory, error→errors
63
+ block, warning→errors+warnings block). Detection and rendering are
64
+ identical under all three values; E0 findings never block; partial
65
+ scans stay exit 2.
66
+ - **`fixGroupId`** (additive JSON field): the stable semantic identity
67
+ of a remediation group — intentionally distinct from `ruleId`
68
+ (which identifies the detector). Current strategy: one rule = one
69
+ group, so fixGroupId equals ruleId today; consumers must not rely
70
+ on that permanently.
71
+
72
+ ### Changed
73
+
74
+ - help registry gained `why`, `handoff`, `install` and the new flags;
75
+ site/reference/cli.md documents the handoff trust model.
76
+
77
+ ## [0.5.5] — 2026-09-06
78
+
79
+ ### Audit Remediation 1.0 (engine correctness + trust boundary)
80
+
81
+ ### Changed — plugin execution gate (audit C2, contract-visible, pre-1.0)
82
+
83
+ - **`--enable-plugins` / `MJOLNIR_ENABLE_PLUGINS=1`** — npm plugins and
84
+ JS-module external rules (`mjolnir-rules/*.mjs`) now load and execute
85
+ only behind an explicit opt-in (default OFF). Declared-but-gated
86
+ sources are listed on a loud stderr notice; they are never imported.
87
+ JSON rule manifests execute no code and load without the gate. See
88
+ SECURITY.md (trust model) and docs/VERSIONING.md (gate contract).
89
+
90
+ ### Fixed — engine correctness (audit M1)
91
+
92
+ - **C1/W9 (`--cache`)**: the cache key folds in the repo-relative path,
93
+ adapter id, and parse mode — byte-identical files no longer share
94
+ verdicts (findings carried the first-scanned file's path), and
95
+ regex-fallback verdicts are never served as AST ones. `CACHE_VERSION`
96
+ bumped to 2: one-time invalidation, first post-upgrade scan cold.
97
+ - **C3**: the default console sinks are variadic —
98
+ "mjolnir internal error:" now carries the actual cause (the message
99
+ used to be dropped).
100
+ - **C5**: a partial (truncated) scan never writes the
101
+ first-clean-scan milestone; `mjolnir diff` on a truncated head
102
+ returns exit 2 and folds no resolved findings into stats.
103
+ - **W1**: the Java/C# maskers keep code after a closed block comment
104
+ live (`/*x*/y` used to blank `y`).
105
+ - **W2/W3**: parse-semaphore re-check (cap holds under fan-out) and
106
+ parser-creation retry on rejection (a transient WASM load failure no
107
+ longer disables the AST path for the process).
108
+ - **W4**: unchecked grammar-shape casts removed in the QA-model
109
+ extractor; `parseTsFile`'s undefined contract honored.
110
+ - **W8**: runtime corroboration claims "test" level only when the
111
+ finding's line falls inside the verdict's span; lineless reports
112
+ degrade to file level.
113
+ - **W10**: malformed rule records at the rule→Finding boundary are
114
+ rejected with a diagnostic (crash channel), never silently scored.
115
+ - **S5 (QA-CI-001, detectorRevision 2)**: step-level line resolution
116
+ falls back to the anchor line instead of crashing — the rule no
117
+ longer vanishes into crash isolation on workflows where the raw
118
+ `continue-on-error: true` literal sits outside the search window.
119
+ Behavior changes from crash-dropped to reported; sidecar +
120
+ measured-FP regenerated per policy.
121
+ - **masking.ts**: outer-delimiter search fixes misclassification of
122
+ string values ending in a nested quote.
123
+ - **changed.ts**: hunk-content lines that look like diff headers no
124
+ longer abandon the hunk; one unreadable untracked file degrades only
125
+ itself (treated fully changed), never the whole scope.
126
+ - **shared-walk.ts**: unreadable directories and skipped symlinks are
127
+ counted skips with reasons; deadline/cap checks run inside the entry
128
+ loop.
129
+ - **workflow-parser.ts**: the documented nesting-depth cap is enforced;
130
+ `jobs` builds on a null-prototype object; step `with` is copied.
131
+
132
+ ### Changed — suppressions expiry (audit S4, contract-visible, pre-1.0)
133
+
134
+ - The 90-day default for hand-authored `ignore` entries is no longer
135
+ anchored at the config file's mtime — any config edit used to reset
136
+ every suppression window. Expiry is the entry's explicit `expires`
137
+ date; entries without one stay active and are labeled accordingly in
138
+ `mjolnir suppressions`.
139
+
140
+ ### Hardened — trust boundary (audits S1/S2/S3/S7)
141
+
142
+ - **S1**: git resolves to an absolute path from PATH (never the scanned
143
+ CWD) — a planted `git.exe`/`git.bat` cannot hijack Mjölnir's git
144
+ calls on Windows.
145
+ - **S2**: ignore/glob patterns and external JSON-rule regexes are
146
+ length/wildcard-capped at compile time; `**/` compiles segment-aware.
147
+ - **S3**: config/ignore/plugins resolve from the explicit scan target's
148
+ project; the resolved anchor prints in verbose mode.
149
+ - **S7**: `ignore[].files` must be `string[]` (exit 10 on typo);
150
+ unknown top-level config keys warn; baseline `schemaVersion` is
151
+ checked (future versions degrade to "no baseline" with a warning).
152
+
12
153
  ## [0.5.3] — 2026-09-05
13
154
 
14
155
  ### Terminal + CI UX Overhaul (plan 1788579907109)
@@ -159,6 +300,8 @@ rules… → Scoring…`) fed by the new additive `ScanHooks.onProgress`.
159
300
 
160
301
  ### Added — folder-based external rules, zero network
161
302
 
303
+ ### Added — folder-based external rules, zero network
304
+
162
305
  - **`mjolnir-rules/` contract** (`src/plugins/local-rules.ts`): a
163
306
  workspace directory loaded from the scan target root alongside npm
164
307
  plugins. Two file kinds: **JSON rule manifests** (declarative regex
package/README.md CHANGED
@@ -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
@@ -128,6 +128,21 @@ interface Finding {
128
128
  /** How to fix — concrete action. */
129
129
  fix: string;
130
130
  docsUrl?: string;
131
+ /**
132
+ * Stable semantic identity of a remediation group: findings that can
133
+ * be reasoned about and potentially remediated as one root-cause unit
134
+ * (agent-handoff plan §5.1). This is intentionally a DIFFERENT
135
+ * concept from `ruleId` (which identifies the detector/rule).
136
+ *
137
+ * Current implementation strategy: fixGroupId = ruleId, because each
138
+ * rule currently represents one remediation group. Future rules may
139
+ * emit multiple findings belonging to one fix group, or multiple
140
+ * remediation groups. Consumers MUST NOT rely on
141
+ * fixGroupId === ruleId permanently.
142
+ *
143
+ * Additive within schemaVersion 1.
144
+ */
145
+ fixGroupId?: string;
131
146
  }
132
147
  interface DimensionScore {
133
148
  category: RuleCategory;
@@ -147,6 +162,16 @@ interface ScanResult {
147
162
  /** Present when --scope changed was requested. */
148
163
  scope?: "all" | "changed";
149
164
  scopeDegraded?: string;
165
+ /**
166
+ * Present when --staged was requested (agent-handoff plan §5.7):
167
+ * the scan surface was restricted to git staged files, and `files`
168
+ * is how many survived the intersection. The score reflects THAT
169
+ * surface — never present it as a full-repo score. Additive within
170
+ * schemaVersion 1.
171
+ */
172
+ staged?: {
173
+ files: number;
174
+ };
150
175
  /** Detected test frameworks (0.2). Empty + unknown=true when undetectable. */
151
176
  frameworks: string[];
152
177
  frameworkDetectionUnknown: boolean;
@@ -223,6 +248,20 @@ interface ScanResult {
223
248
  };
224
249
  }
225
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
226
265
  //#region src/discovery/workspace.d.ts
227
266
  /**
228
267
  * Repository discovery (Sprint-Plan W1-03).
@@ -238,9 +277,6 @@ interface Workspace {
238
277
  workspaceGlobs: string[];
239
278
  }
240
279
  //#endregion
241
- //#region src/engine/tier-policy.d.ts
242
- type Tier = "core" | "extended" | "quarantine";
243
- //#endregion
244
280
  //#region src/rules/rule.d.ts
245
281
  /**
246
282
  * How the rule's primary detection decision is made (Verification Trust
@@ -480,19 +516,13 @@ interface UniversalRule {
480
516
  run(file: ParsedFile): Array<Omit<Finding, "ruleId" | "category">>;
481
517
  }
482
518
  //#endregion
483
- //#region src/cli.d.ts
484
- /**
485
- * Tool version for `mjolnir --version`.
486
- *
487
- * A literal, not a package.json read: the shipped artifact is a single
488
- * bundled `dist/cli.mjs`, so resolving package.json at runtime depends on
489
- * where the file happens to sit after install. This follows the same
490
- * discipline as SARIF's `driver.version` — kept in sync by
491
- * `scripts/sync-sarif-version.cjs` on release and guarded by
492
- * `tests/version-consistency.spec.ts` locally.
493
- */
494
- declare const CLI_VERSION = "0.5.3";
495
- 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<{
496
526
  rules: UniversalRule[];
497
527
  pluginErrors: string[];
498
528
  tierByRuleId: Map<string, Tier>;
@@ -536,25 +566,41 @@ interface CliArgs {
536
566
  * and auto-disabled in CI/machine formats; this flag is the manual off.
537
567
  */
538
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";
583
+ /**
584
+ * --category <cat> (repeatable): presentation filter — narrows the
585
+ * TERMINAL findings display (and handoff/why output) to the given
586
+ * rule categories. NEVER filters the scan, the JSON/SARIF output,
587
+ * or the score (agent-handoff plan §5.5).
588
+ */
589
+ categories?: RuleCategory[];
590
+ /**
591
+ * --score: print only the numeric score (or `unknown` when the repo
592
+ * has no tests) instead of the full report. Pure rendering flag:
593
+ * scan semantics and exit codes are unchanged (plan §5.6).
594
+ */
595
+ scoreOnly?: boolean;
596
+ /**
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).
601
+ */
602
+ enablePlugins?: boolean;
539
603
  }
540
- /** A usage-error detail: the offending token, when one exists. */
541
- interface UsageErrorDetail {
542
- /** The unknown flag or rejected value (e.g. `--nope`, `loud`). */
543
- token?: string | undefined;
544
- /** The flag whose value was rejected (`--tone` for `--tone loud`). */
545
- flag?: string | undefined;
546
- }
547
- declare function parseArgs(argv: string[], onError?: (detail: UsageErrorDetail) => void): CliArgs | null;
548
- /** Hand-rolled Levenshtein distance (plan M2: no new dependencies). */
549
- declare function levenshtein(a: string, b: string): number;
550
- /** Nearest known flags within distance ≤ 2, nearest first. */
551
- declare function nearestFlags(flag: string, max?: number): string[];
552
- /**
553
- * Friendly usage error (plan M2, exit 10 preserved): nearest-flag
554
- * suggestion, the valid neighbors, and the exact help command. Printed
555
- * to stderr; findings/usage stay on their documented streams.
556
- */
557
- declare function usageErrorMessage(detail: UsageErrorDetail): string;
558
604
  interface ScanHooks {
559
605
  /** Invoked when a rule throws on a file (audit R-9). */
560
606
  onRuleCrash?: (ruleId: string, file: string, error: unknown) => void;
@@ -572,6 +618,13 @@ interface ScanHooks {
572
618
  total?: number | undefined;
573
619
  detail?: string | undefined;
574
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;
575
628
  }
576
629
  /**
577
630
  * Workspace fallback for targets with no discoverable project root
@@ -579,7 +632,40 @@ interface ScanHooks {
579
632
  * root-path degenerate case (`C:\` → basename "") is testable without
580
633
  * scanning a filesystem root.
581
634
  */
582
- 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">;
583
669
  /**
584
670
  * Testable default scan path core. `hooks` lets callers observe
585
671
  * normally-invisible events (swallowed rule crashes) without changing
@@ -590,9 +676,42 @@ declare function fallbackWorkspace(targetAbs: string): Workspace;
590
676
  * inherently async); `runRules` and every rule stay synchronous and
591
677
  * consume `ParsedFile.ast`. Callers await the returned promise.
592
678
  */
593
- 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.5";
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;
594
712
  type Output = (...parts: unknown[]) => void;
595
- declare function pathMatchesGlob(path: string, glob: string): boolean;
713
+ declare const out: Output;
714
+ declare const err: Output;
596
715
  /** Testable `ci install` handler. Returns the process exit code. */
597
716
  declare function runCiInstall(argv: string[], io?: {
598
717
  out: Output;
@@ -735,4 +854,4 @@ declare function runHelpCommand(argv: string[], io?: {
735
854
  declare function internalErrorMessage(err: unknown, emit: (s: string) => void, debug: boolean): void;
736
855
  declare function isEntryPoint(): boolean;
737
856
  //#endregion
738
- 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 };