mjolnir-qa 0.4.0 → 0.5.2

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/dist/cli.d.mts CHANGED
@@ -1,6 +1,6 @@
1
1
  //#region src/types.d.ts
2
2
  /**
3
- * QA Doctor — canonical types (JSON contract v1, schemaVersion 1).
3
+ * Mjolnir — canonical types (JSON contract v1, schemaVersion 1).
4
4
  *
5
5
  * STABILITY: This file is public API. Per Product-MVP.txt §24.2:
6
6
  * additive changes only within schemaVersion 1; removing or renaming
@@ -34,6 +34,52 @@ type EvidenceLevel = (typeof EVIDENCE_ORDER)[number];
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
36
  type RuleCategory = "QA-TEST" | "QA-TQUAL" | "QA-PW" | "QA-CI";
37
+ /**
38
+ * Trust levels (Verification Trust Evolution Plan §16): the OVERALL
39
+ * trust a consumer can place in one finding, combining the static
40
+ * evidence ladder (E0–E2) with RUNTIME corroboration from a real run
41
+ * report. Exposed honestly, never overclaimed:
42
+ * L0 — observation only (E0, no runtime evidence).
43
+ * L1 — heuristic static evidence (E1), no runtime evidence.
44
+ * L2 — deterministic static evidence (E2), no runtime evidence.
45
+ * L3 — RUNTIME: the file containing this finding appeared in a real
46
+ * run report (tests in that file executed).
47
+ * L4 — RUNTIME: the specific test containing this finding was
48
+ * identified in the report and executed (its outcome is known).
49
+ * L5 — RUNTIME: the run verdict directly corroborates the DEFECT
50
+ * class (e.g. a flake-risk finding whose test actually flaked,
51
+ * retried, or timed out in the report).
52
+ * INVARIANT (structurally enforced): L3–L5 require runtime
53
+ * corroboration — a static-only finding can never claim L4/L5.
54
+ */
55
+ declare const TRUST_ORDER: readonly ["L0", "L1", "L2", "L3", "L4", "L5"];
56
+ type TrustLevel = (typeof TRUST_ORDER)[number];
57
+ /**
58
+ * Runtime corroboration for one finding (plan §16): what a real run
59
+ * report says about the code this finding points at. Additive within
60
+ * schemaVersion 1; absent means "no runtime evidence" — never
61
+ * fabricated.
62
+ */
63
+ interface RuntimeCorroboration {
64
+ /** Granularity of what the runtime report could vouch for. */
65
+ level: "file" | "test" | "defect";
66
+ /** Report format the evidence came from. */
67
+ source: "playwright-json" | "junit-xml";
68
+ /** Number of tests executed in the finding's file (any level). */
69
+ testsExecuted: number;
70
+ /**
71
+ * The containing test's verdict, when the finding line falls inside a
72
+ * test the report identifies (level "test"/"defect").
73
+ */
74
+ matchedTest?: {
75
+ title: string;
76
+ finalStatus: string;
77
+ attempts: number;
78
+ passedOnRetry: boolean;
79
+ everFailed: boolean;
80
+ skipped: boolean;
81
+ };
82
+ }
37
83
  interface Finding {
38
84
  ruleId: string;
39
85
  category: RuleCategory;
@@ -48,6 +94,28 @@ interface Finding {
48
94
  * Optional in the JSON contract (additive within schemaVersion 1).
49
95
  */
50
96
  evidenceLevel?: EvidenceLevel;
97
+ /**
98
+ * Measured false-positive rate (0..1) for the rule that produced this
99
+ * finding, from hand-classified corpus verdicts — present only when the
100
+ * rule has ≥ 10 classified verdicts. Absent means the rule ships on
101
+ * assumption. Additive within schemaVersion 1.
102
+ */
103
+ measuredFpRate?: number;
104
+ /** Classified (TP+FP) verdicts behind `measuredFpRate`. */
105
+ measuredFpN?: number;
106
+ /**
107
+ * Runtime corroboration from a real run report (plan §16), stamped
108
+ * when a report was available and matched this finding's file/test.
109
+ * Absent means "no runtime evidence" — the static evidence ladder
110
+ * (E0–E2) is all the consumer has. Additive within schemaVersion 1.
111
+ */
112
+ runtimeCorroboration?: RuntimeCorroboration;
113
+ /**
114
+ * Overall trust level (plan §16, see TRUST_ORDER). Derived
115
+ * deterministically from evidenceLevel + runtimeCorroboration;
116
+ * stamped with the corroboration pass. Additive within schemaVersion 1.
117
+ */
118
+ trustLevel?: TrustLevel;
51
119
  /** Repo-relative path with forward slashes, regardless of OS. */
52
120
  file: string;
53
121
  /** 1-based. */
@@ -84,15 +152,356 @@ interface ScanResult {
84
152
  frameworkDetectionUnknown: boolean;
85
153
  dimensions: DimensionScore[];
86
154
  findings: Finding[];
155
+ /** Number of test files scanned (Phase 5 — reporting only). */
156
+ testFileCount?: number;
157
+ /** Test declarations found — the normalization denominator (Phase 5). */
158
+ testDeclarationCount?: number;
159
+ /** Raw deduction total before normalization (Phase 5 — transparency). */
160
+ rawDeductions?: number;
161
+ /** Number of findings suppressed by active config entries (suppression transparency). */
162
+ suppressionCount?: number;
163
+ /**
164
+ * Third-party plugin code that executed during this scan (audit S-8).
165
+ * Plugins run with full Node privileges by documented design — anyone
166
+ * reading a report must be able to tell whether they ran. Absent
167
+ * when no plugins are configured.
168
+ */
169
+ plugins?: Array<{
170
+ name: string;
171
+ rules: number;
172
+ }>;
173
+ /**
174
+ * Agentic Trust Profile (plan §17): per-scan provenance metadata —
175
+ * share of test files carrying detected generative markers and the
176
+ * findings split across those surfaces. PROVENANCE IS NOT TRUST: the
177
+ * profile never changes scoring, evidence levels, or tier behavior
178
+ * (§17.4 — the same evidence standard applies regardless of author).
179
+ * Additive within schemaVersion 1; present on every completed scan.
180
+ */
181
+ agenticProfile?: {
182
+ testFiles: number;
183
+ generatedMarkedFiles: number;
184
+ codegenLikeFiles: number;
185
+ /** generatedMarkedFiles / testFiles (0..1). */
186
+ shareMarkedGenerated: number;
187
+ findingsInGeneratedFiles: number;
188
+ findingsInUnmarkedFiles: number;
189
+ note: string;
190
+ };
87
191
  analysisStatus: {
88
192
  discovery: AnalysisStatus;
89
193
  rules: AnalysisStatus;
90
194
  skippedFiles: number;
91
195
  durationMs: number;
196
+ /**
197
+ * Named reasons the scan stopped early (audit H-8): "deadline",
198
+ * "file-cap:<adapter>", "rule-loop-deadline". Present only when
199
+ * truncation actually happened — absence means the scan is whole.
200
+ */
201
+ truncationReasons?: string[];
202
+ /**
203
+ * Rule executions that threw and were swallowed by crash isolation
204
+ * (audit R-9). 0 means no rule silently failed; absence means the
205
+ * producer predates the counter.
206
+ */
207
+ rulesCrashed?: number;
92
208
  };
209
+ /**
210
+ * Local incremental cache report (Beta-to-Stable plan, M5.2). Present
211
+ * only when the scan ran with `--cache`; additive within
212
+ * schemaVersion 1. The cache is content-addressed and local-only
213
+ * (plan A-2) — it never leaves the machine and never touches the
214
+ * network.
215
+ */
216
+ cache?: {
217
+ /** Files whose rule verdicts were reused from the cache. */
218
+ hits: number;
219
+ /** Files analyzed fresh this run (cache misses). */
220
+ misses: number;
221
+ /** Absolute path of the cache file — auditable, gitignored. */
222
+ file: string;
223
+ };
224
+ }
225
+ //#endregion
226
+ //#region src/discovery/workspace.d.ts
227
+ /**
228
+ * Repository discovery (Sprint-Plan W1-03).
229
+ * Finds project root, parses package.json, detects npm/yarn/pnpm workspaces.
230
+ * Monorepo depth beyond workspaces is a documented launch cut (§29.1).
231
+ */
232
+ interface Workspace {
233
+ /** Absolute path of the workspace/project root. */
234
+ root: string;
235
+ name: string;
236
+ packageJson: Record<string, unknown>;
237
+ /** Glob patterns from the root package.json workspaces field. */
238
+ workspaceGlobs: string[];
239
+ }
240
+ //#endregion
241
+ //#region src/engine/tier-policy.d.ts
242
+ type Tier = "core" | "extended" | "quarantine";
243
+ //#endregion
244
+ //#region src/rules/rule.d.ts
245
+ /**
246
+ * How the rule's primary detection decision is made (Verification Trust
247
+ * Evolution Plan §09.6/§12.1 — the enforced D6 enum, replacing free text):
248
+ * - "LEXICAL": pattern matching over source text (regex over `codeText`,
249
+ * masked text, YAML/manifest text, suite-wide absence sweeps).
250
+ * - "AST": structural analysis of a parsed syntax tree (ts-morph node
251
+ * walks, tree-sitter queries) is the core decision.
252
+ * - "SEMANTIC": name/type/symbol or call-graph reasoning beyond
253
+ * single-file syntax (reserved — no rule ships this yet).
254
+ * - "FRAMEWORK": framework configuration/manifest semantics drive the
255
+ * decision (CI workflow job/step structure, test-command gating).
256
+ * - "RUNTIME": execution evidence drives the decision (reserved —
257
+ * Phase 6).
258
+ */
259
+ type DetectionStrategy = "LEXICAL" | "AST" | "SEMANTIC" | "FRAMEWORK" | "RUNTIME";
260
+ interface RuleMeta {
261
+ /** Frozen public API — never reused (§18.4). */
262
+ id: string;
263
+ category: RuleCategory;
264
+ title: string;
265
+ severity: Severity;
266
+ confidence: Confidence;
267
+ findingType: FindingType;
268
+ /** QA-native impact framing (#21): what this means for the QA engineer. */
269
+ qaImpact: QaImpact;
270
+ /**
271
+ * Honesty Core: explicit evidence level. When omitted, findings derive
272
+ * it from findingType+confidence (deriveEvidenceLevel). Only set this
273
+ * when the rule's evidence is genuinely stronger/weaker than the
274
+ * default derivation implies.
275
+ */
276
+ evidenceLevel?: EvidenceLevel;
277
+ /** Rule IDs that can fire on the same root cause (dedup pass, R6). */
278
+ overlapWith?: string[];
279
+ /** Languages this rule applies to, e.g. ["typescript", "python"]. */
280
+ languages?: string[];
281
+ /** Frameworks the rule is meaningful for, e.g. ["jest", "vitest", "playwright"]. */
282
+ frameworks?: string[];
283
+ /**
284
+ * Declared false-positive risk of the rule as shipped. Part of the
285
+ * north-star contract: a rule that cannot honestly classify its FP risk
286
+ * should not be enforced.
287
+ */
288
+ falsePositiveRisk?: "low" | "medium" | "high";
289
+ /** Whether `mjolnir fix` (or a future autofix) can safely repair it. */
290
+ autofix?: boolean;
291
+ /**
292
+ * How detection works, as an enforced enum (plan §09.6/§12.1 — D6
293
+ * closed). Free-text declarations were migrated to the enum in
294
+ * Phase 2; the registry ratchet (tests/rules.registry.spec.ts) makes
295
+ * omission or a bad value a CI failure, so new rules must declare it.
296
+ */
297
+ detectionStrategy?: DetectionStrategy;
298
+ /**
299
+ * Verbatim legacy detection-strategy description preserved from the
300
+ * pre-enum free-text era (D6 migration). Optional; carries the nuance
301
+ * the enum alone cannot ("regex pattern + inside-string oracle", …).
302
+ * Rendered by the rule docs pages alongside the enum.
303
+ */
304
+ detectionNotes?: string;
305
+ /** First released version (semver). Immutable once set. */
306
+ introduced?: string;
307
+ /**
308
+ * Tier assignment (Phase 4 — Tempering Plan; measurement-dependent
309
+ * default per Verification Trust Evolution Plan §11.2 Step 2).
310
+ * - "core": ships in the default report (≤10% measured FP rate)
311
+ * - "extended": included by default, lower confidence (≤30% FP)
312
+ * - "quarantine": opt-in only via --strict (>30% FP or unmeasured)
313
+ * When omitted, the tier resolves measurement-dependently via
314
+ * `effectiveTier` (src/rules/measurement.ts): core for rules with a
315
+ * valid corpus measurement, extended (displayed PROVISIONAL)
316
+ * otherwise — an unmeasured rule can never default into core.
317
+ */
318
+ tier?: "core" | "extended" | "quarantine";
319
+ /**
320
+ * Detector implementation revision (Verification Trust Evolution Plan
321
+ * §07). Increment on ANY detection-logic change — pattern, scoping,
322
+ * AST adoption, rung change. A `MEASURED_FP` entry recorded against a
323
+ * different revision is stale: the measurement is invalidated,
324
+ * displayed as PROVISIONAL, and the rule cannot sit in effective core
325
+ * until re-measured (registry ratchet, §20.3). Default when omitted: 1
326
+ * (the current first-generation detectors).
327
+ */
328
+ detectorRevision?: number;
329
+ /**
330
+ * This finding proves the reported pass does not cover what it claims.
331
+ *
332
+ * `.only` makes the runner skip every other test; a masked CI gate means a
333
+ * failure was ignored. Either way the suite's green is not evidence, and no
334
+ * amount of density normalization should be able to average that away — a
335
+ * two-test repo with `.only` is as compromised as a two-thousand-test one.
336
+ *
337
+ * Findings marked here cap the score into the UNWORTHY band regardless of
338
+ * exposure. Reserved for mechanisms where the bypass is unambiguous, not for
339
+ * findings that merely weaken a single test.
340
+ */
341
+ suiteInvalidating?: boolean;
342
+ }
343
+ interface SourceFileContext {
344
+ /** Repo-relative path, forward slashes. */
345
+ path: string;
346
+ text: string;
347
+ /**
348
+ * Parsed AST provided by the engine — ts-morph SourceFile for
349
+ * TypeScript files, tree-sitter Tree for Java/C# (Phase 0.5 parse
350
+ * stage), workflow DOM for GitHub Actions. Typed as unknown here to
351
+ * keep the core rule contract decoupled; each language's helper
352
+ * narrows it (getTsSourceFile / getTreeSitterTree).
353
+ */
354
+ ast?: unknown;
355
+ /**
356
+ * Code-only text view: string literals and comments blanked to spaces,
357
+ * newlines preserved so line/column indices stay exact (Phase 1 FP
358
+ * firewall). Rules that must never fire on prose inside strings or
359
+ * comments use this instead of `text`. Falls back to `text` when
360
+ * unavailable.
361
+ */
362
+ codeText?: string;
363
+ }
364
+ type RuleFn = (ctx: SourceFileContext) => Omit<Finding, "ruleId" | "category">[];
365
+ /**
366
+ * Optional L2 structural-analysis hook (Verification Trust Evolution
367
+ * Plan §13.2). When the engine provides a parsed AST on the context
368
+ * (ts-morph SourceFile for TypeScript, tree-sitter Tree for Java/C#),
369
+ * the hook produces the findings; its regex path is the MANDATORY
370
+ * fallback, never optional — `undefined` return (or no `ctx.ast`)
371
+ * means "no AST — run the regex path", so fixture harnesses, grammar
372
+ * load failures, and degraded scans all keep working (ts-ast fallback
373
+ * discipline, QA-PW-002 pattern). This seam is what lets a rule
374
+ * declare `detectionStrategy: "AST"` honestly: the structural path is
375
+ * the decision when a tree exists, and the regex path is documented
376
+ * degraded detection, not a second source of truth.
377
+ */
378
+ type AstQueryHook = (ctx: SourceFileContext) => Omit<Finding, "ruleId" | "category">[] | undefined;
379
+ type AppliesTo = "test-files" | "ci-workflows" | "python" | "java" | "csharp" | "all";
380
+ interface QADoctorRule extends RuleMeta {
381
+ /** Which file kinds this rule applies to. */
382
+ appliesTo: AppliesTo;
383
+ /**
384
+ * Config-hygiene rules: the engine only feeds these rules config
385
+ * files (and never feeds them test files), and never feeds test-file
386
+ * rules a config. Set on rules whose detection gates on a config
387
+ * filename (QA-PW-121/122/141/143/144). Without this flag the
388
+ * generic test rules would fire nonsense on configs (e.g. QA-TEST-003
389
+ * "no assertions" on every playwright.config.ts).
390
+ */
391
+ configRule?: boolean;
392
+ /**
393
+ * Config filename patterns (regex SOURCE strings) this config rule
394
+ * gates on (plan §15.2): the adapter matches these against the file's
395
+ * basename, replacing the hard-coded playwright.config.* regex that
396
+ * used to live in the adapter AND duplicated inside each config rule.
397
+ * The internal regex gate in `run` stays as belt-and-suspenders for
398
+ * direct harness invocation.
399
+ */
400
+ configFiles?: string[];
401
+ /**
402
+ * Framework opt-in (plan §15.1, defect D7): when declared, the rule
403
+ * runs on a file only if the file's own framework tags (its
404
+ * imports/usings) intersect it. Files without tags are always
405
+ * analyzed (open-when-unknown) — the dimension narrows, it never
406
+ * silently drops evidence. Mirror of UniversalRule.frameworks,
407
+ * threaded through asUniversal.
408
+ */
409
+ frameworksOverride?: string[];
410
+ /**
411
+ * L2 structural-analysis path (§13.2): runs when the engine supplies
412
+ * a parsed AST for the file. MUST be paired with a regex fallback in
413
+ * `run` (mandatory fallback discipline) — see AstQueryHook.
414
+ */
415
+ astQuery?: AstQueryHook;
416
+ run: RuleFn;
417
+ }
418
+ //#endregion
419
+ //#region src/engine/adapter.d.ts
420
+ /** Semantic operations a parsed file exposes to rules. */
421
+ interface ParsedFile {
422
+ path: string;
423
+ text: string;
424
+ /** Adapter-specific AST; typed loosely until tree-sitter unifies it. */
425
+ ast?: unknown;
426
+ /**
427
+ * Code-only text view: string literals and comments blanked to spaces,
428
+ * newlines preserved so line/column indices stay exact. Regex rules
429
+ * that must never fire on prose inside strings or comments use this
430
+ * instead of `text`. Computed lazily per adapter.
431
+ */
432
+ codeText?: string;
433
+ /**
434
+ * Per-file framework tags (Verification Trust Evolution Plan §15.1,
435
+ * defect D7): derived from the file's OWN imports/usings/imports-lines
436
+ * by the adapter ("playwright", "jest", "cypress", "junit", "testng",
437
+ * "selenium", "nunit", "xunit", "mstest", "pytest", …). EMPTY/absent
438
+ * means "no per-file evidence" — framework filtering is then OPEN (a
439
+ * rule declaring `frameworks` still runs), never a silent skip.
440
+ */
441
+ frameworkTags?: readonly string[];
442
+ }
443
+ /**
444
+ * A rule that declares which adapters it supports. Backward compatible:
445
+ * legacy 'test-files' maps to ['typescript'], 'ci-workflows' to
446
+ * ['github-actions'].
447
+ */
448
+ interface UniversalRule {
449
+ id: string;
450
+ category: string;
451
+ appliesTo: readonly string[];
452
+ /**
453
+ * Config-hygiene rule (see QADoctorRule.configRule): the adapter runs
454
+ * these ONLY on the config files named in `configFiles`, and runs
455
+ * every other rule only on test files. Keeps config rules measurable
456
+ * in real scans without letting generic test rules fire nonsense on
457
+ * configs.
458
+ */
459
+ configOnly?: boolean;
460
+ /**
461
+ * Config filename patterns (regex sources) this config rule gates on
462
+ * (plan §15.2 — replaces the hard-coded playwright.config.* regex
463
+ * that used to live in the TS adapter AND duplicated inside each
464
+ * config rule). Empty/absent + configOnly=true falls back to the
465
+ * adapter's built-in config list.
466
+ */
467
+ configFiles?: readonly string[];
468
+ /**
469
+ * Framework opt-in (plan §15.1, defect D7): when declared, the rule
470
+ * runs on a file only if the file's own `frameworkTags` intersect it.
471
+ * Files without tags are always analyzed (open-when-unknown).
472
+ */
473
+ frameworks?: readonly string[];
474
+ /**
475
+ * Detector implementation revision (§07), threaded through asUniversal
476
+ * so the M5.2 cache digest can fold it in; the stale-measurement
477
+ * machinery reads it from the registry, the cache from this field.
478
+ */
479
+ detectorRevision?: number;
480
+ run(file: ParsedFile): Array<Omit<Finding, "ruleId" | "category">>;
93
481
  }
94
482
  //#endregion
95
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.2";
495
+ declare function buildUniversalRules(root: string, strict?: boolean): Promise<{
496
+ rules: UniversalRule[];
497
+ pluginErrors: string[];
498
+ tierByRuleId: Map<string, Tier>;
499
+ pluginMeta: Array<{
500
+ name: string;
501
+ rules: number;
502
+ }>;
503
+ externalRules: QADoctorRule[];
504
+ }>;
96
505
  interface CliArgs {
97
506
  target: string;
98
507
  json: boolean;
@@ -106,17 +515,48 @@ interface CliArgs {
106
515
  ascii?: boolean;
107
516
  /** --tone blunt: opt-in blunter messages (Sprint 9 Task 40). */
108
517
  tone?: "blunt";
518
+ /** --strict: include quarantine-tier rules in the scan (Phase 4). */
519
+ strict?: boolean;
520
+ /** --base <ref>: base ref for --scope changed (audit H-10). */
521
+ base?: string;
522
+ /** --debug: print errors swallowed by crash isolation (audit R-9). */
523
+ debug?: boolean;
524
+ /** --record-milestones: let a scan write .mjolnir/stats.json (audit R-1). */
525
+ recordMilestones?: boolean;
526
+ /**
527
+ * --cache: reuse per-file rule verdicts from the local content-addressed
528
+ * cache (M5.2). Post-loop processing always re-runs; the cache only
529
+ * short-circuits the read+parse+rule loop for byte-identical files
530
+ * under an unchanged rule set. Local-only, never leaves the machine.
531
+ */
532
+ cache?: boolean;
109
533
  }
110
534
  declare function parseArgs(argv: string[]): CliArgs | null;
111
- declare function runScan(args: CliArgs): ScanResult;
112
- type Output = (...parts: unknown[]) => void;
535
+ interface ScanHooks {
536
+ /** Invoked when a rule throws on a file (audit R-9). */
537
+ onRuleCrash?: (ruleId: string, file: string, error: unknown) => void;
538
+ /** Invoked for non-fatal config warnings (bug-audit M4). */
539
+ onConfigWarning?: (message: string) => void;
540
+ }
113
541
  /**
114
- * Minimal glob match for suppression `files` patterns. Supports:
115
- * "tests/**" — everything under tests/
116
- * "**‍/*.spec.ts" any depth ending pattern
117
- * "tests/foo.spec.ts" exact path
118
- * Forward slashes only (findings always use normalized paths).
542
+ * Workspace fallback for targets with no discoverable project root
543
+ * (package.json-less repos, Python/Java/C# trees). Exported pure so the
544
+ * root-path degenerate case (`C:\` → basename "") is testable without
545
+ * scanning a filesystem root.
119
546
  */
547
+ declare function fallbackWorkspace(targetAbs: string): Workspace;
548
+ /**
549
+ * Testable default scan path core. `hooks` lets callers observe
550
+ * normally-invisible events (swallowed rule crashes) without changing
551
+ * the ScanResult contract beyond the rulesCrashed counter.
552
+ *
553
+ * Async since the Verification Trust Evolution Plan Phase 0.5 (§10): the
554
+ * per-file loop awaits the adapter parse stage (WASM grammar load is
555
+ * inherently async); `runRules` and every rule stay synchronous and
556
+ * consume `ParsedFile.ast`. Callers await the returned promise.
557
+ */
558
+ declare function runScan(args: CliArgs, hooks?: ScanHooks): Promise<ScanResult>;
559
+ type Output = (...parts: unknown[]) => void;
120
560
  declare function pathMatchesGlob(path: string, glob: string): boolean;
121
561
  /** Testable `ci install` handler. Returns the process exit code. */
122
562
  declare function runCiInstall(argv: string[], io?: {
@@ -126,6 +566,7 @@ declare function runCiInstall(argv: string[], io?: {
126
566
  /** Testable `suppressions` handler. */
127
567
  declare function runSuppressions(io?: {
128
568
  out: Output;
569
+ err?: Output;
129
570
  }): number;
130
571
  /** Testable `forensics` handler. */
131
572
  declare function runForensicsCommand(argv: string[], io?: {
@@ -135,7 +576,8 @@ declare function runForensicsCommand(argv: string[], io?: {
135
576
  /** Testable `doctor:playwright` handler. */
136
577
  declare function runDoctorPlaywright(argv: string[], io?: {
137
578
  out: Output;
138
- }): number;
579
+ err?: Output;
580
+ }): Promise<number>;
139
581
  /** Testable `doctor` handler — self-audit of Mjölnir's own rule base. */
140
582
  declare function runDoctorCommand(argv: string[], io?: {
141
583
  out: Output;
@@ -145,7 +587,7 @@ declare function runDoctorCommand(argv: string[], io?: {
145
587
  declare function runRulesCommand(argv: string[], io?: {
146
588
  out: Output;
147
589
  err: Output;
148
- }): number;
590
+ }): Promise<number>;
149
591
  /**
150
592
  * Testable `explain <RULE-ID>` handler (Plan.md Sprint 1.3,
151
593
  * Master-Stabilization-Plan Sprint 5 Task 19). Metadata always renders
@@ -158,11 +600,16 @@ declare function runExplainCommand(argv: string[], io?: {
158
600
  out: Output;
159
601
  err: Output;
160
602
  }): number;
161
- /** Testable default scan path. */
162
603
  declare function runScanCommand(argv: string[], io?: {
163
604
  out: Output;
164
605
  err: Output;
165
- }): number;
606
+ }): Promise<number>;
607
+ /**
608
+ * Exit-code decision for a finished scan under the given gate level
609
+ * (audit H-7): the previously-dead config.gate field now selects which
610
+ * severities block. Advisory (E0) findings never gate at any level.
611
+ */
612
+ declare function exitForFindings(findings: readonly Finding[], gate: "advisory" | "error" | "warning"): number;
166
613
  /** Testable `triage` handler (Tier 5 #22). */
167
614
  declare function runTriageCommand(argv: string[], io?: {
168
615
  out: Output;
@@ -172,17 +619,17 @@ declare function runTriageCommand(argv: string[], io?: {
172
619
  declare function runBadgeCommand(argv: string[], io?: {
173
620
  out: Output;
174
621
  err: Output;
175
- }): number;
622
+ }): Promise<number>;
176
623
  /** Testable `debt` handler (Tier 5 #27). */
177
624
  declare function runDebtCommand(argv: string[], io?: {
178
625
  out: Output;
179
626
  err: Output;
180
- }): number;
627
+ }): Promise<number>;
181
628
  /** Testable `fix` handler (Tier 1 #3) — safe auto-fix with proof. */
182
629
  declare function runFixCommand(argv: string[], io?: {
183
630
  out: Output;
184
631
  err: Output;
185
- }): number;
632
+ }): Promise<number>;
186
633
  /** Testable `create-rule` handler (Tier 6 #34). */
187
634
  declare function runCreateRuleCommand(argv: string[], io?: {
188
635
  out: Output;
@@ -192,22 +639,22 @@ declare function runCreateRuleCommand(argv: string[], io?: {
192
639
  declare function runImpactCommand(argv: string[], io?: {
193
640
  out: Output;
194
641
  err: Output;
195
- }): number;
642
+ }): Promise<number>;
196
643
  /** Testable `baseline` handler (Sprint 6 Task 24). */
197
644
  declare function runBaselineCommand(argv: string[], io?: {
198
645
  out: Output;
199
646
  err: Output;
200
- }): number;
647
+ }): Promise<number>;
201
648
  /** Testable `diff` handler (Sprint 6 Task 24) — new/worsened debt only. */
202
649
  declare function runDiffCommand(argv: string[], io?: {
203
650
  out: Output;
204
651
  err: Output;
205
- }): number;
652
+ }): Promise<number>;
206
653
  /** Testable `pr-comment` handler (Sprint 6 Task 25). */
207
654
  declare function runPrCommentCommand(argv: string[], io?: {
208
655
  out: Output;
209
656
  err: Output;
210
- }): number;
657
+ }): Promise<number>;
211
658
  /** Testable `stats` handler (Sprint 6 Task 26). */
212
659
  declare function runStatsCommand(argv: string[], io?: {
213
660
  out: Output;
@@ -217,7 +664,7 @@ declare function runStatsCommand(argv: string[], io?: {
217
664
  declare function runHandoverCommand(argv: string[], io?: {
218
665
  out: Output;
219
666
  err: Output;
220
- }): number;
667
+ }): Promise<number>;
221
668
  /** Testable `init` handler (Tier 2 #10). */
222
669
  declare function runInitCommand(argv: string[], io?: {
223
670
  out: Output;
@@ -228,7 +675,7 @@ declare function runPwReportCommand(argv: string[], io?: {
228
675
  out: Output;
229
676
  err: Output;
230
677
  }): number;
231
- declare function main(argv?: string[]): number;
678
+ declare function main(argv?: string[]): Promise<number>;
232
679
  declare function isEntryPoint(): boolean;
233
680
  //#endregion
234
- export { Output, isEntryPoint, main, parseArgs, pathMatchesGlob, runBadgeCommand, runBaselineCommand, runCiInstall, runCreateRuleCommand, runDebtCommand, runDiffCommand, runDoctorCommand, runDoctorPlaywright, runExplainCommand, runFixCommand, runForensicsCommand, runHandoverCommand, runImpactCommand, runInitCommand, runPrCommentCommand, runPwReportCommand, runRulesCommand, runScan, runScanCommand, runStatsCommand, runSuppressions, runTriageCommand };
681
+ export { CLI_VERSION, Output, ScanHooks, buildUniversalRules, exitForFindings, fallbackWorkspace, isEntryPoint, main, parseArgs, pathMatchesGlob, runBadgeCommand, runBaselineCommand, runCiInstall, runCreateRuleCommand, runDebtCommand, runDiffCommand, runDoctorCommand, runDoctorPlaywright, runExplainCommand, runFixCommand, runForensicsCommand, runHandoverCommand, runImpactCommand, runInitCommand, runPrCommentCommand, runPwReportCommand, runRulesCommand, runScan, runScanCommand, runStatsCommand, runSuppressions, runTriageCommand };