mjolnir-qa 6.0.0 → 6.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +508 -1
- package/README.md +314 -29
- package/dist/cli.d.mts +12 -480
- package/dist/cli.mjs +808 -620
- package/dist/mcp/stdio.mjs +318 -397
- package/package.json +24 -34
- package/dist/rolldown-runtime-8H4AJuhK.mjs +0 -14
package/dist/cli.mjs
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { t as __exportAll } from "./rolldown-runtime-8H4AJuhK.mjs";
|
|
3
2
|
import { createRequire } from "node:module";
|
|
4
|
-
import { accessSync, chmodSync, closeSync, constants, copyFileSync, existsSync, fstatSync, lstatSync, mkdirSync, mkdtempSync, openSync, readFileSync, readSync, readdirSync, realpathSync, renameSync, rmSync, statSync, unlinkSync, writeFileSync, writeSync } from "node:fs";
|
|
3
|
+
import { accessSync, appendFileSync, chmodSync, closeSync, constants, copyFileSync, existsSync, fstatSync, lstatSync, mkdirSync, mkdtempSync, openSync, readFileSync, readSync, readdirSync, realpathSync, renameSync, rmSync, statSync, unlinkSync, writeFileSync, writeSync } from "node:fs";
|
|
5
4
|
import { basename, delimiter, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
6
5
|
import process$1 from "node:process";
|
|
7
6
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
@@ -17,7 +16,18 @@ import { createInterface } from "node:readline";
|
|
|
17
16
|
import { Buffer as Buffer$1 } from "node:buffer";
|
|
18
17
|
import { isDeepStrictEqual } from "node:util";
|
|
19
18
|
import { tmpdir } from "node:os";
|
|
20
|
-
|
|
19
|
+
// \0rolldown/runtime.js
|
|
20
|
+
var __defProp = Object.defineProperty;
|
|
21
|
+
var __exportAll = (all, no_symbols) => {
|
|
22
|
+
let target = {};
|
|
23
|
+
for (var name in all) __defProp(target, name, {
|
|
24
|
+
get: all[name],
|
|
25
|
+
enumerable: true
|
|
26
|
+
});
|
|
27
|
+
if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
|
|
28
|
+
return target;
|
|
29
|
+
};
|
|
30
|
+
// src/types.ts
|
|
21
31
|
/** Severity ladder. Order matters for sorting and gating. */
|
|
22
32
|
const SEVERITY_ORDER$1 = [
|
|
23
33
|
"error",
|
|
@@ -146,8 +156,7 @@ const DEDUCTIONS = {
|
|
|
146
156
|
warning: 3,
|
|
147
157
|
info: 1
|
|
148
158
|
};
|
|
149
|
-
|
|
150
|
-
//#region src/claim-evidence.ts
|
|
159
|
+
// src/claim-evidence.ts
|
|
151
160
|
/**
|
|
152
161
|
* The one place a rendered surface decides whether it may claim "clean".
|
|
153
162
|
*
|
|
@@ -204,8 +213,7 @@ function scanExitCode(input) {
|
|
|
204
213
|
if (input.gate === "advisory") return 0;
|
|
205
214
|
return input.findings.some((f) => isAtGate(f, input.gate, input.isAdvisory)) ? 1 : 0;
|
|
206
215
|
}
|
|
207
|
-
|
|
208
|
-
//#region src/engine/completion.ts
|
|
216
|
+
// src/engine/completion.ts
|
|
209
217
|
function deriveCompletion(input) {
|
|
210
218
|
const truncationReasons = [...new Set(input.truncationReasons)].sort();
|
|
211
219
|
const degradations = [...input.degradations ?? []].sort((a, b) => a.reason < b.reason ? -1 : a.reason > b.reason ? 1 : 0);
|
|
@@ -247,8 +255,7 @@ function deriveCompletion(input) {
|
|
|
247
255
|
}
|
|
248
256
|
};
|
|
249
257
|
}
|
|
250
|
-
|
|
251
|
-
//#region src/engine/degradation-ledger.ts
|
|
258
|
+
// src/engine/degradation-ledger.ts
|
|
252
259
|
/**
|
|
253
260
|
* Append-only. Records are never edited and never removed by a recording
|
|
254
261
|
* site; a reader takes a window, and the window is defined by index, so
|
|
@@ -284,8 +291,7 @@ function summarizeDegradations(window) {
|
|
|
284
291
|
count
|
|
285
292
|
})).sort((a, b) => a.reason < b.reason ? -1 : a.reason > b.reason ? 1 : 0);
|
|
286
293
|
}
|
|
287
|
-
|
|
288
|
-
//#region src/engine/runtime-corroboration.ts
|
|
294
|
+
// src/engine/runtime-corroboration.ts
|
|
289
295
|
/**
|
|
290
296
|
* Stamp runtime corroboration + trust levels onto findings (mutates in
|
|
291
297
|
* place, the same contract as stampEvidenceLevels). Returns the number
|
|
@@ -301,7 +307,8 @@ function stampRuntimeCorroboration(findings, report, scanRoot) {
|
|
|
301
307
|
let n = p.replace(/\\/g, "/");
|
|
302
308
|
if (scanRoot) {
|
|
303
309
|
const root = scanRoot.replace(/\\/g, "/").replace(/\/?$/, "/");
|
|
304
|
-
|
|
310
|
+
const head = n.slice(0, root.length);
|
|
311
|
+
if (/^[a-z]:\//i.test(root) ? head.toLowerCase() === root.toLowerCase() : head === root) n = n.slice(root.length);
|
|
305
312
|
}
|
|
306
313
|
return n.replace(/^\.\//, "").replace(/^\/+/, "");
|
|
307
314
|
};
|
|
@@ -367,8 +374,7 @@ function deriveTrustLevel(finding, corroboration) {
|
|
|
367
374
|
if (corroboration.level === "test") return "L4";
|
|
368
375
|
return "L3";
|
|
369
376
|
}
|
|
370
|
-
|
|
371
|
-
//#region src/rules/measured-fp.generated.ts
|
|
377
|
+
// src/rules/measured-fp.generated.ts
|
|
372
378
|
const MEASURED_FP = {
|
|
373
379
|
"QA-CI-001": {
|
|
374
380
|
fpRate: .105,
|
|
@@ -1429,8 +1435,7 @@ const MEASURED_FP_RAW = {
|
|
|
1429
1435
|
ciHigh: .4343
|
|
1430
1436
|
}
|
|
1431
1437
|
};
|
|
1432
|
-
|
|
1433
|
-
//#region src/engine/trust-summary.ts
|
|
1438
|
+
// src/engine/trust-summary.ts
|
|
1434
1439
|
/**
|
|
1435
1440
|
* Trust level of the summary — the BEST trust level any finding in the
|
|
1436
1441
|
* scan reached. Static-only findings settle at L2 (deriveTrustLevel's
|
|
@@ -1554,8 +1559,7 @@ function buildTrustSummary(result, declarationsByFile) {
|
|
|
1554
1559
|
ceilingReasons: reasons
|
|
1555
1560
|
};
|
|
1556
1561
|
}
|
|
1557
|
-
|
|
1558
|
-
//#region src/governance/run-mode.ts
|
|
1562
|
+
// src/governance/run-mode.ts
|
|
1559
1563
|
/**
|
|
1560
1564
|
* Which mode a run is in, and how the other one is reached.
|
|
1561
1565
|
*
|
|
@@ -1606,8 +1610,7 @@ function resolveRunMode(input) {
|
|
|
1606
1610
|
if (declared === "local-only" || declared === "hosted") return declared;
|
|
1607
1611
|
throw new Error(`${HOSTED_MODE_FLAG}=${String(declared)} is not a run mode. Expected "hosted" or "local-only". Falling back to a default here would let a mistyped flag look like a deliberate choice.`);
|
|
1608
1612
|
}
|
|
1609
|
-
|
|
1610
|
-
//#region src/engine/run-identity.ts
|
|
1613
|
+
// src/engine/run-identity.ts
|
|
1611
1614
|
/**
|
|
1612
1615
|
* Run Identity (product-gap master plan §7, plan 1789009691197 R4c) —
|
|
1613
1616
|
* the deterministic binding that anchors every trust claim to its
|
|
@@ -1736,8 +1739,7 @@ function buildEvidenceGraph(parts) {
|
|
|
1736
1739
|
} } : {}
|
|
1737
1740
|
};
|
|
1738
1741
|
}
|
|
1739
|
-
|
|
1740
|
-
//#region src/engine/file-executor.ts
|
|
1742
|
+
// src/engine/file-executor.ts
|
|
1741
1743
|
function cacheHitOutcome(job) {
|
|
1742
1744
|
return {
|
|
1743
1745
|
path: job.path,
|
|
@@ -1828,8 +1830,7 @@ function createExecutor(name, concurrency, runOne) {
|
|
|
1828
1830
|
execute: runOne
|
|
1829
1831
|
};
|
|
1830
1832
|
}
|
|
1831
|
-
|
|
1832
|
-
//#region src/scope/git-resolve.ts
|
|
1833
|
+
// src/scope/git-resolve.ts
|
|
1833
1834
|
/**
|
|
1834
1835
|
* Absolute-path git resolution (audit S1).
|
|
1835
1836
|
*
|
|
@@ -1904,8 +1905,7 @@ function runGit(root, args) {
|
|
|
1904
1905
|
return null;
|
|
1905
1906
|
}
|
|
1906
1907
|
}
|
|
1907
|
-
|
|
1908
|
-
//#region src/lib/safe-json.ts
|
|
1908
|
+
// src/lib/safe-json.ts
|
|
1909
1909
|
/**
|
|
1910
1910
|
* Reusable safe-JSON parse-and-validate helper.
|
|
1911
1911
|
*
|
|
@@ -1942,8 +1942,7 @@ function isRecord(v) {
|
|
|
1942
1942
|
return false;
|
|
1943
1943
|
}
|
|
1944
1944
|
}
|
|
1945
|
-
|
|
1946
|
-
//#region src/engine/candidate-binding.ts
|
|
1945
|
+
// src/engine/candidate-binding.ts
|
|
1947
1946
|
/**
|
|
1948
1947
|
* Candidate binding (plan V5-010).
|
|
1949
1948
|
*
|
|
@@ -2076,18 +2075,17 @@ function readCandidateBinding(root, manifestPath = CANDIDATE_MANIFEST_PATH) {
|
|
|
2076
2075
|
return null;
|
|
2077
2076
|
}
|
|
2078
2077
|
}
|
|
2079
|
-
|
|
2080
|
-
//#region src/engine/version.ts
|
|
2078
|
+
// src/engine/version.ts
|
|
2081
2079
|
/**
|
|
2082
2080
|
* Engine version, as a leaf module: `runIdentity` (R4c) needs the engine
|
|
2083
2081
|
* version inside scan-pipeline WITHOUT importing cli.ts (a cycle — cli
|
|
2084
2082
|
* imports the pipeline). The literal follows the same discipline as
|
|
2085
2083
|
* CLI_VERSION and SARIF's driver.version: kept in sync on release by
|
|
2086
|
-
*
|
|
2087
|
-
*
|
|
2084
|
+
* `npm run version:surface:sync` (this file is an identity surface) and
|
|
2085
|
+
* guarded by the version-consistency
|
|
2088
2086
|
* spec. cli.ts re-exports this as CLI_VERSION.
|
|
2089
2087
|
*/
|
|
2090
|
-
const ENGINE_VERSION = "6.
|
|
2088
|
+
const ENGINE_VERSION = "6.1.0";
|
|
2091
2089
|
/**
|
|
2092
2090
|
* Build identity — WHICH BUILD IS RUNNING, as distinct from WHICH RELEASE IS
|
|
2093
2091
|
* PUBLISHED.
|
|
@@ -2148,16 +2146,7 @@ function resolveBuildId() {
|
|
|
2148
2146
|
};
|
|
2149
2147
|
return buildIdFrom(runGit);
|
|
2150
2148
|
}
|
|
2151
|
-
|
|
2152
|
-
//#region src/engine/contract-versions.ts
|
|
2153
|
-
/**
|
|
2154
|
-
* Versioned contract registry (VERSION-001).
|
|
2155
|
-
*
|
|
2156
|
-
* Single source of truth for every version constant the engine exposes
|
|
2157
|
-
* to consumers (run identity, evidence graph, machine contract).
|
|
2158
|
-
* Constants are `as const` — literal types flow into the identity
|
|
2159
|
-
* surface without widening.
|
|
2160
|
-
*/
|
|
2149
|
+
// src/engine/contract-versions.ts
|
|
2161
2150
|
const TRUST_MODEL_VERSION = "1.0.0";
|
|
2162
2151
|
/**
|
|
2163
2152
|
* 2.0.0 (ADR 0014): the core tier carries an evidence FLOOR. A core rule's
|
|
@@ -2169,8 +2158,7 @@ const TRUST_MODEL_VERSION = "1.0.0";
|
|
|
2169
2158
|
*/
|
|
2170
2159
|
const SCORING_MODEL_VERSION = "2.0.0";
|
|
2171
2160
|
const FRAMEWORK_SUPPORT_MATRIX_VERSION = "1.0.0";
|
|
2172
|
-
|
|
2173
|
-
//#region src/discovery/evidence-discovery.ts
|
|
2161
|
+
// src/discovery/evidence-discovery.ts
|
|
2174
2162
|
/**
|
|
2175
2163
|
* Zero-config evidence discovery (Mega MVP Master Plan v3.1 §26 WI-11).
|
|
2176
2164
|
*
|
|
@@ -2262,8 +2250,7 @@ function discoverEvidenceCandidates(scanRoot) {
|
|
|
2262
2250
|
}
|
|
2263
2251
|
return out.sort((a, b) => a.depth - b.depth || (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
|
|
2264
2252
|
}
|
|
2265
|
-
|
|
2266
|
-
//#region src/discovery/workspace.ts
|
|
2253
|
+
// src/discovery/workspace.ts
|
|
2267
2254
|
/**
|
|
2268
2255
|
* Repository discovery (Sprint-Plan W1-03).
|
|
2269
2256
|
* Finds project root, parses package.json, detects npm/yarn/pnpm workspaces.
|
|
@@ -2310,8 +2297,7 @@ function basename$1(p) {
|
|
|
2310
2297
|
const parts = p.split(/[\\/]/);
|
|
2311
2298
|
return parts[parts.length - 1];
|
|
2312
2299
|
}
|
|
2313
|
-
|
|
2314
|
-
//#region src/lib/fs-bounded.ts
|
|
2300
|
+
// src/lib/fs-bounded.ts
|
|
2315
2301
|
function closeQuiet(fd) {
|
|
2316
2302
|
try {
|
|
2317
2303
|
closeSync(fd);
|
|
@@ -2363,8 +2349,7 @@ function readFileBounded(path, maxBytes) {
|
|
|
2363
2349
|
if (fd !== void 0) closeQuiet(fd);
|
|
2364
2350
|
}
|
|
2365
2351
|
}
|
|
2366
|
-
|
|
2367
|
-
//#region src/config/config-schema.ts
|
|
2352
|
+
// src/config/config-schema.ts
|
|
2368
2353
|
const VALID_GATES = /* @__PURE__ */ new Set([
|
|
2369
2354
|
"advisory",
|
|
2370
2355
|
"error",
|
|
@@ -2427,15 +2412,18 @@ function validateConfigSchema(config) {
|
|
|
2427
2412
|
}
|
|
2428
2413
|
}
|
|
2429
2414
|
if (cfg["plugins"] !== void 0) {
|
|
2430
|
-
if (cfg["plugins"]
|
|
2415
|
+
if (!Array.isArray(cfg["plugins"])) errors.push(`plugins must be an array of package names or { "package": "<name>" } objects, got ${typeof cfg["plugins"]}`);
|
|
2416
|
+
else for (let i = 0; i < cfg["plugins"].length; i++) {
|
|
2417
|
+
const entry = cfg["plugins"][i];
|
|
2418
|
+
if (!(typeof entry === "string" || entry !== null && typeof entry === "object" && !Array.isArray(entry) && typeof entry["package"] === "string")) errors.push(`plugins[${i}] must be a package name or { "package": "<name>" }, got ${JSON.stringify(entry)}`);
|
|
2419
|
+
}
|
|
2431
2420
|
}
|
|
2432
2421
|
return {
|
|
2433
2422
|
valid: errors.length === 0,
|
|
2434
2423
|
errors
|
|
2435
2424
|
};
|
|
2436
2425
|
}
|
|
2437
|
-
|
|
2438
|
-
//#region src/config/config.ts
|
|
2426
|
+
// src/config/config.ts
|
|
2439
2427
|
/**
|
|
2440
2428
|
* Config file support (Sprint-Plan W7, Product-MVP §27 + GAP-F).
|
|
2441
2429
|
* Zero-config preserved: config presence never changes detection
|
|
@@ -2493,7 +2481,7 @@ function loadConfig(root, options = {}) {
|
|
|
2493
2481
|
return {
|
|
2494
2482
|
config: parsed,
|
|
2495
2483
|
path: p,
|
|
2496
|
-
warnings: validate(parsed, options.knownRuleIds)
|
|
2484
|
+
warnings: validate(parsed, options.knownRuleIds, options.now, name)
|
|
2497
2485
|
};
|
|
2498
2486
|
} catch (err) {
|
|
2499
2487
|
const msg = err.message;
|
|
@@ -2506,7 +2494,7 @@ function loadConfig(root, options = {}) {
|
|
|
2506
2494
|
warnings: []
|
|
2507
2495
|
};
|
|
2508
2496
|
}
|
|
2509
|
-
function validate(cfg, knownRuleIds) {
|
|
2497
|
+
function validate(cfg, knownRuleIds, now = /* @__PURE__ */ new Date(), fileName = "mjolnir.config.json") {
|
|
2510
2498
|
const warnings = [];
|
|
2511
2499
|
const schemaResult = validateConfigSchema(cfg);
|
|
2512
2500
|
if (!schemaResult.valid) throw new Error(schemaResult.errors.join("; "));
|
|
@@ -2526,10 +2514,10 @@ function validate(cfg, knownRuleIds) {
|
|
|
2526
2514
|
if (!Array.isArray(ign.files)) throw new Error(`ignore for ${ign.ruleId}: "files" must be an array of glob strings, got ${typeof ign.files}`);
|
|
2527
2515
|
for (const g of ign.files) if (typeof g !== "string") throw new Error(`ignore for ${ign.ruleId}: "files" entries must be strings, got ${typeof g} (${JSON.stringify(g)})`);
|
|
2528
2516
|
}
|
|
2529
|
-
if (ign.expires !== void 0 && Number.isNaN(
|
|
2530
|
-
if (ign.expires !== void 0 && !
|
|
2517
|
+
if (ign.expires !== void 0 && Number.isNaN(utcMidnight(ign.expires))) throw new Error(`ignore for ${ign.ruleId}: "expires" must be an ISO date (YYYY-MM-DD), got "${ign.expires}"`);
|
|
2518
|
+
if (ign.expires !== void 0 && !isSuppressionActive(ign, now)) warnings.push(`warning: ignore for ${ign.ruleId} has expired (expires "${ign.expires}") — the entry is kept but currently inactive.`);
|
|
2531
2519
|
}
|
|
2532
|
-
for (const key of Object.keys(cfg)) if (!KNOWN_CONFIG_KEYS.has(key)) warnings.push(`warning:
|
|
2520
|
+
for (const key of Object.keys(cfg)) if (!KNOWN_CONFIG_KEYS.has(key)) warnings.push(`warning: ${fileName} has an unknown top-level key "${key}" — ignored (typo? see README §Configuration).`);
|
|
2533
2521
|
if (cfg.exclude !== void 0) {
|
|
2534
2522
|
if (!Array.isArray(cfg.exclude)) throw new Error(`exclude must be an array of strings, got ${typeof cfg.exclude}`);
|
|
2535
2523
|
for (const p of cfg.exclude) if (typeof p !== "string") throw new Error(`exclude entries must be strings, got ${typeof p} (${JSON.stringify(p)})`);
|
|
@@ -2551,12 +2539,22 @@ function isSuppressionActive(ign, now = /* @__PURE__ */ new Date()) {
|
|
|
2551
2539
|
if (!ign.expires) return true;
|
|
2552
2540
|
return utcMidnight(ign.expires) > utcMidnightOf(now);
|
|
2553
2541
|
}
|
|
2554
|
-
/**
|
|
2542
|
+
/**
|
|
2543
|
+
* UTC-midnight timestamp of an ISO calendar date (YYYY-MM-DD), or NaN.
|
|
2544
|
+
*
|
|
2545
|
+
* NaN for anything that is not a real calendar day, so a load-time check and
|
|
2546
|
+
* the active/expired verdict share one parser and cannot disagree.
|
|
2547
|
+
*/
|
|
2555
2548
|
function utcMidnight(isoDate) {
|
|
2556
|
-
if (!/^\d{4}-\d{2}-\d{2}$/.test(isoDate)) return NaN;
|
|
2557
2549
|
const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(isoDate);
|
|
2558
2550
|
if (!m) return NaN;
|
|
2559
|
-
|
|
2551
|
+
const year = Number(m[1]);
|
|
2552
|
+
const month = Number(m[2]) - 1;
|
|
2553
|
+
const day = Number(m[3]);
|
|
2554
|
+
const t = Date.UTC(year, month, day);
|
|
2555
|
+
const back = new Date(t);
|
|
2556
|
+
if (back.getUTCFullYear() !== year || back.getUTCMonth() !== month || back.getUTCDate() !== day) return NaN;
|
|
2557
|
+
return t;
|
|
2560
2558
|
}
|
|
2561
2559
|
/** UTC-midnight timestamp of a Date (calendar day, timezone-independent). */
|
|
2562
2560
|
function utcMidnightOf(d) {
|
|
@@ -2569,8 +2567,7 @@ function applySeverityOverrides(findings, cfg) {
|
|
|
2569
2567
|
if (override && SEVERITY_ORDER$1.includes(override)) f.severity = override;
|
|
2570
2568
|
}
|
|
2571
2569
|
}
|
|
2572
|
-
|
|
2573
|
-
//#region src/lib/compare.ts
|
|
2570
|
+
// src/lib/compare.ts
|
|
2574
2571
|
/**
|
|
2575
2572
|
* Order that does not depend on who is asking.
|
|
2576
2573
|
*
|
|
@@ -2616,8 +2613,7 @@ function compareCodePoints(a, b) {
|
|
|
2616
2613
|
function compareLocalized(locale) {
|
|
2617
2614
|
return (a, b) => a.localeCompare(b, locale);
|
|
2618
2615
|
}
|
|
2619
|
-
|
|
2620
|
-
//#region src/lib/glob.ts
|
|
2616
|
+
// src/lib/glob.ts
|
|
2621
2617
|
/**
|
|
2622
2618
|
* The shared segment compiler: a glob to a regex BODY, with no anchors.
|
|
2623
2619
|
*
|
|
@@ -2671,8 +2667,7 @@ function anyDepthGlobRegExp(glob) {
|
|
|
2671
2667
|
function pathMatchesGlob$1(path, glob) {
|
|
2672
2668
|
return anchoredGlobRegExp(glob).test(path.replaceAll("\\", "/"));
|
|
2673
2669
|
}
|
|
2674
|
-
|
|
2675
|
-
//#region src/discovery/ignores.ts
|
|
2670
|
+
// src/discovery/ignores.ts
|
|
2676
2671
|
/**
|
|
2677
2672
|
* Ignore resolution (Product-MVP.txt R1 — Tier-1 launch blocker).
|
|
2678
2673
|
*
|
|
@@ -2829,8 +2824,7 @@ function isLintFixtureDir(absoluteDir) {
|
|
|
2829
2824
|
return false;
|
|
2830
2825
|
}
|
|
2831
2826
|
}
|
|
2832
|
-
|
|
2833
|
-
//#region src/discovery/shared-walk.ts
|
|
2827
|
+
// src/discovery/shared-walk.ts
|
|
2834
2828
|
/**
|
|
2835
2829
|
* Shared directory walker (audit P-2).
|
|
2836
2830
|
*
|
|
@@ -2893,8 +2887,7 @@ function sharedWalk(options) {
|
|
|
2893
2887
|
};
|
|
2894
2888
|
walk(options.root);
|
|
2895
2889
|
}
|
|
2896
|
-
|
|
2897
|
-
//#region src/frameworks/framework-inventory.ts
|
|
2890
|
+
// src/frameworks/framework-inventory.ts
|
|
2898
2891
|
const FRAMEWORK_INVENTORY = [
|
|
2899
2892
|
{
|
|
2900
2893
|
frameworkId: "playwright",
|
|
@@ -3095,8 +3088,7 @@ const FRAMEWORK_INVENTORY = [
|
|
|
3095
3088
|
function getFrameworkById(frameworkId) {
|
|
3096
3089
|
return FRAMEWORK_INVENTORY.find((f) => f.frameworkId === frameworkId);
|
|
3097
3090
|
}
|
|
3098
|
-
|
|
3099
|
-
//#region src/discovery/frameworks.ts
|
|
3091
|
+
// src/discovery/frameworks.ts
|
|
3100
3092
|
/**
|
|
3101
3093
|
* Framework detection via config resolution (Sprint-Plan W3-01, R8).
|
|
3102
3094
|
*
|
|
@@ -3179,8 +3171,7 @@ function detectFrameworks(ws) {
|
|
|
3179
3171
|
unknown: false
|
|
3180
3172
|
};
|
|
3181
3173
|
}
|
|
3182
|
-
|
|
3183
|
-
//#region src/engine/ts-ast.ts
|
|
3174
|
+
// src/engine/ts-ast.ts
|
|
3184
3175
|
/**
|
|
3185
3176
|
* AST precision layer (Upgrade-Plan-v3 Phase 3).
|
|
3186
3177
|
*
|
|
@@ -3379,8 +3370,7 @@ function getCodeOnlyText(file) {
|
|
|
3379
3370
|
function resetTsMorphProject() {
|
|
3380
3371
|
looseProject = null;
|
|
3381
3372
|
}
|
|
3382
|
-
|
|
3383
|
-
//#region src/engine/code-text.ts
|
|
3373
|
+
// src/engine/code-text.ts
|
|
3384
3374
|
/**
|
|
3385
3375
|
* Code-only text masking (Phase 1 — Tempering Plan).
|
|
3386
3376
|
*
|
|
@@ -3740,8 +3730,7 @@ function getCodeTextForCounting(text, language) {
|
|
|
3740
3730
|
function blankRange(chars, start, end) {
|
|
3741
3731
|
for (let i = start; i < end && i < chars.length; i++) if (chars[i] !== "\n" && chars[i] !== "\r") chars[i] = " ";
|
|
3742
3732
|
}
|
|
3743
|
-
|
|
3744
|
-
//#region src/engine/adapter.ts
|
|
3733
|
+
// src/engine/adapter.ts
|
|
3745
3734
|
/**
|
|
3746
3735
|
* The §15.1 framework filter, shared by every language adapter: a rule
|
|
3747
3736
|
* that declares `frameworks` runs on a file only when the file carries
|
|
@@ -3755,8 +3744,7 @@ function frameworkFilterApplies(rule, file) {
|
|
|
3755
3744
|
if (tags === void 0 || tags.length === 0) return true;
|
|
3756
3745
|
return rule.frameworks.some((f) => tags.includes(f));
|
|
3757
3746
|
}
|
|
3758
|
-
|
|
3759
|
-
//#region src/engine/shared-run-rules.ts
|
|
3747
|
+
// src/engine/shared-run-rules.ts
|
|
3760
3748
|
/**
|
|
3761
3749
|
* Run ONE rule with crash isolation, and return what it produced.
|
|
3762
3750
|
*
|
|
@@ -3793,8 +3781,7 @@ function runRuleIsolated(rule, file, onCrash) {
|
|
|
3793
3781
|
return null;
|
|
3794
3782
|
}
|
|
3795
3783
|
}
|
|
3796
|
-
|
|
3797
|
-
//#region src/adapters/typescript.ts
|
|
3784
|
+
// src/adapters/typescript.ts
|
|
3798
3785
|
/**
|
|
3799
3786
|
* TypeScript/JavaScript adapter (R1).
|
|
3800
3787
|
* Wraps the proven regex-based engine behind the LanguageAdapter
|
|
@@ -4047,8 +4034,7 @@ function loadWorkspaceShim(root) {
|
|
|
4047
4034
|
return null;
|
|
4048
4035
|
}
|
|
4049
4036
|
}
|
|
4050
|
-
|
|
4051
|
-
//#region src/engine/tree-sitter-ast.ts
|
|
4037
|
+
// src/engine/tree-sitter-ast.ts
|
|
4052
4038
|
/**
|
|
4053
4039
|
* Tree-sitter WASM AST layer for Java and C# — Sprint 8 Task 36
|
|
4054
4040
|
* (Master-Stabilization-Plan.md).
|
|
@@ -4267,8 +4253,7 @@ async function releaseTreeSitterResources() {
|
|
|
4267
4253
|
} catch {}
|
|
4268
4254
|
}
|
|
4269
4255
|
}
|
|
4270
|
-
|
|
4271
|
-
//#region src/adapters/python.ts
|
|
4256
|
+
// src/adapters/python.ts
|
|
4272
4257
|
/**
|
|
4273
4258
|
* Python/pytest adapter (Upgrade-Plan-v2 R2).
|
|
4274
4259
|
*
|
|
@@ -4426,8 +4411,7 @@ const pythonAdapter = {
|
|
|
4426
4411
|
function readText$2(p) {
|
|
4427
4412
|
return readFileSync(p, "utf8");
|
|
4428
4413
|
}
|
|
4429
|
-
|
|
4430
|
-
//#region src/adapters/java.ts
|
|
4414
|
+
// src/adapters/java.ts
|
|
4431
4415
|
/**
|
|
4432
4416
|
* Java/JUnit adapter (Upgrade-Plan-v3 Phase 4).
|
|
4433
4417
|
*
|
|
@@ -4495,7 +4479,9 @@ const javaAdapter = {
|
|
|
4495
4479
|
const artifact = /<artifactId>([^<]+)<\/artifactId>/.exec(block[0])?.[1] ?? "";
|
|
4496
4480
|
for (const { re, tag } of JAVA_ARTIFACT_TAGS) if (re.test(artifact)) frameworks.add(tag);
|
|
4497
4481
|
}
|
|
4498
|
-
} catch {
|
|
4482
|
+
} catch {
|
|
4483
|
+
recordDegradation("java-manifest-unreadable");
|
|
4484
|
+
}
|
|
4499
4485
|
for (const gradle of ["build.gradle", "build.gradle.kts"]) {
|
|
4500
4486
|
const path = join(root, gradle);
|
|
4501
4487
|
if (!existsSync(path)) continue;
|
|
@@ -4505,7 +4491,9 @@ const javaAdapter = {
|
|
|
4505
4491
|
const artifact = m[1].split(":")[1];
|
|
4506
4492
|
for (const { re, tag } of JAVA_ARTIFACT_TAGS) if (artifact !== void 0 && re.test(artifact)) frameworks.add(tag);
|
|
4507
4493
|
}
|
|
4508
|
-
} catch {
|
|
4494
|
+
} catch {
|
|
4495
|
+
recordDegradation("java-manifest-unreadable");
|
|
4496
|
+
}
|
|
4509
4497
|
}
|
|
4510
4498
|
if (frameworks.size === 0) return {
|
|
4511
4499
|
frameworks: [],
|
|
@@ -4593,8 +4581,7 @@ function javaFileTags(file) {
|
|
|
4593
4581
|
function readText$1(path) {
|
|
4594
4582
|
return readFileSync(path, "utf8");
|
|
4595
4583
|
}
|
|
4596
|
-
|
|
4597
|
-
//#region src/adapters/csharp.ts
|
|
4584
|
+
// src/adapters/csharp.ts
|
|
4598
4585
|
/**
|
|
4599
4586
|
* C#/.NET adapter (Upgrade-Plan-v3 Phase 5).
|
|
4600
4587
|
*
|
|
@@ -4664,9 +4651,13 @@ const csharpAdapter = {
|
|
|
4664
4651
|
const pkg = m[1];
|
|
4665
4652
|
for (const { re, tag } of CS_PACKAGE_TAGS) if (re.test(pkg)) frameworks.add(tag);
|
|
4666
4653
|
}
|
|
4667
|
-
} catch {
|
|
4654
|
+
} catch {
|
|
4655
|
+
recordDegradation("csharp-project-unreadable");
|
|
4656
|
+
}
|
|
4668
4657
|
}
|
|
4669
|
-
} catch {
|
|
4658
|
+
} catch {
|
|
4659
|
+
recordDegradation("csharp-project-unreadable");
|
|
4660
|
+
}
|
|
4670
4661
|
if (frameworks.size === 0) return {
|
|
4671
4662
|
frameworks: [],
|
|
4672
4663
|
unknown: true
|
|
@@ -4750,8 +4741,7 @@ function csharpFileTags(file) {
|
|
|
4750
4741
|
function readText(path) {
|
|
4751
4742
|
return readFileSync(path, "utf8");
|
|
4752
4743
|
}
|
|
4753
|
-
|
|
4754
|
-
//#region src/discovery/yaml-guards.ts
|
|
4744
|
+
// src/discovery/yaml-guards.ts
|
|
4755
4745
|
/**
|
|
4756
4746
|
* Shared hostile-YAML guards (product-gap master plan P3 common law —
|
|
4757
4747
|
* `azure-pipelines.yml` detection rides the same YAML machinery as the
|
|
@@ -4820,8 +4810,7 @@ function enforceDepthCap(value, max, depthMessage = GENERIC_LABELS.depthMessage)
|
|
|
4820
4810
|
});
|
|
4821
4811
|
}
|
|
4822
4812
|
}
|
|
4823
|
-
|
|
4824
|
-
//#region src/discovery/workflow-parser.ts
|
|
4813
|
+
// src/discovery/workflow-parser.ts
|
|
4825
4814
|
/**
|
|
4826
4815
|
* Safe GitHub Actions workflow parser (Phase 0.3, W4-03 debt).
|
|
4827
4816
|
*
|
|
@@ -4867,8 +4856,7 @@ function parseWorkflow(text) {
|
|
|
4867
4856
|
}
|
|
4868
4857
|
return { jobs };
|
|
4869
4858
|
}
|
|
4870
|
-
|
|
4871
|
-
//#region src/adapters/github-actions.ts
|
|
4859
|
+
// src/adapters/github-actions.ts
|
|
4872
4860
|
/**
|
|
4873
4861
|
* GitHub Actions adapter (R1).
|
|
4874
4862
|
* Owns workflow discovery and safe-YAML parsing; CI rules consume the
|
|
@@ -4953,8 +4941,7 @@ const githubActionsAdapter = {
|
|
|
4953
4941
|
};
|
|
4954
4942
|
var WorkflowParseSkipped = class extends Error {};
|
|
4955
4943
|
const LIMITS_MAX_BYTES$2 = 1048576;
|
|
4956
|
-
|
|
4957
|
-
//#region src/discovery/azure-pipeline-parser.ts
|
|
4944
|
+
// src/discovery/azure-pipeline-parser.ts
|
|
4958
4945
|
/**
|
|
4959
4946
|
* Safe Azure DevOps pipeline parser (product-gap master plan P3b).
|
|
4960
4947
|
*
|
|
@@ -5188,8 +5175,7 @@ function isAzurePipelineFixture(normalizedPath) {
|
|
|
5188
5175
|
const base = normalizedPath.replaceAll("\\", "/").split("/").pop() ?? "";
|
|
5189
5176
|
return AZURE_PIPELINE_FILENAMES_SET.has(base);
|
|
5190
5177
|
}
|
|
5191
|
-
|
|
5192
|
-
//#region src/adapters/azure-pipelines.ts
|
|
5178
|
+
// src/adapters/azure-pipelines.ts
|
|
5193
5179
|
/**
|
|
5194
5180
|
* Azure DevOps pipeline adapter (product-gap master plan P3b).
|
|
5195
5181
|
* Owns `azure-pipelines.yml` discovery (repo root, the platform's
|
|
@@ -5269,8 +5255,7 @@ const azurePipelinesAdapter = {
|
|
|
5269
5255
|
};
|
|
5270
5256
|
var AzureParseSkipped = class extends Error {};
|
|
5271
5257
|
const LIMITS_MAX_BYTES$1 = 1048576;
|
|
5272
|
-
|
|
5273
|
-
//#region src/adapters/jenkins.ts
|
|
5258
|
+
// src/adapters/jenkins.ts
|
|
5274
5259
|
/**
|
|
5275
5260
|
* Jenkins adapter (product-gap master plan P3c).
|
|
5276
5261
|
* The root `Jenkinsfile` is a TEXT-target file kind: declarative and
|
|
@@ -5344,8 +5329,7 @@ const jenkinsAdapter = {
|
|
|
5344
5329
|
}
|
|
5345
5330
|
};
|
|
5346
5331
|
const LIMITS_MAX_BYTES = 1048576;
|
|
5347
|
-
|
|
5348
|
-
//#region src/discovery/scan-adapters.ts
|
|
5332
|
+
// src/discovery/scan-adapters.ts
|
|
5349
5333
|
/**
|
|
5350
5334
|
* The canonical adapter registry (audits H-6/H-9).
|
|
5351
5335
|
*
|
|
@@ -5519,8 +5503,7 @@ const SEARCHED_FOR = [
|
|
|
5519
5503
|
globs: jenkinsAdapter.testFileGlobs
|
|
5520
5504
|
}
|
|
5521
5505
|
];
|
|
5522
|
-
|
|
5523
|
-
//#region src/scope/changed.ts
|
|
5506
|
+
// src/scope/changed.ts
|
|
5524
5507
|
/**
|
|
5525
5508
|
* Changed-scope engine (Sprint-Plan W6, Product-MVP §9 `--scope changed`).
|
|
5526
5509
|
*
|
|
@@ -5625,6 +5608,7 @@ function computeChangedScope(root, baseBranch) {
|
|
|
5625
5608
|
...untrackedSet
|
|
5626
5609
|
])].sort();
|
|
5627
5610
|
const changed = {};
|
|
5611
|
+
const fullyChanged = /* @__PURE__ */ new Set();
|
|
5628
5612
|
const batchDiff = (args, files) => {
|
|
5629
5613
|
const perFile = /* @__PURE__ */ new Map();
|
|
5630
5614
|
if (files.length === 0) return perFile;
|
|
@@ -5676,7 +5660,8 @@ function computeChangedScope(root, baseBranch) {
|
|
|
5676
5660
|
if (untrackedSet.has(file)) {
|
|
5677
5661
|
const all = allLinesOf(root, file);
|
|
5678
5662
|
if (all === null) {
|
|
5679
|
-
|
|
5663
|
+
fullyChanged.add(file);
|
|
5664
|
+
changed[file] = lines;
|
|
5680
5665
|
continue;
|
|
5681
5666
|
}
|
|
5682
5667
|
for (const l of all) lines.add(l);
|
|
@@ -5685,11 +5670,10 @@ function computeChangedScope(root, baseBranch) {
|
|
|
5685
5670
|
}
|
|
5686
5671
|
return {
|
|
5687
5672
|
changed,
|
|
5673
|
+
...fullyChanged.size > 0 ? { fullyChanged } : {},
|
|
5688
5674
|
degraded: false
|
|
5689
5675
|
};
|
|
5690
5676
|
}
|
|
5691
|
-
/** Sentinel size for "fully changed" — bounded, mirrors LIMITS.maxFileBytes scale. */
|
|
5692
|
-
const LIMITS_MAX_LINES = 1e6;
|
|
5693
5677
|
/** Every 1-based line number of an untracked file (it is all new). */
|
|
5694
5678
|
function allLinesOf(root, file) {
|
|
5695
5679
|
const full = join(root, file);
|
|
@@ -5739,6 +5723,7 @@ function parseChangedLines(diff) {
|
|
|
5739
5723
|
function filterToChanged(findings, diff) {
|
|
5740
5724
|
if (diff.degraded) return findings;
|
|
5741
5725
|
return findings.filter((f) => {
|
|
5726
|
+
if (diff.fullyChanged?.has(f.file)) return true;
|
|
5742
5727
|
const lines = diff.changed[f.file];
|
|
5743
5728
|
if (!lines) return false;
|
|
5744
5729
|
for (let l = Math.max(1, f.line - 3); l <= f.line; l++) if (lines.has(l)) return true;
|
|
@@ -5763,8 +5748,7 @@ function computeStagedFiles(root) {
|
|
|
5763
5748
|
if (raw === null) return null;
|
|
5764
5749
|
return raw.split("\0").filter((s) => s.length > 0);
|
|
5765
5750
|
}
|
|
5766
|
-
|
|
5767
|
-
//#region src/rules/rule.ts
|
|
5751
|
+
// src/rules/rule.ts
|
|
5768
5752
|
/**
|
|
5769
5753
|
* Invoke an `astQuery` hook with the mandatory-fallback contract: no
|
|
5770
5754
|
* hook or no AST on the context resolves to `undefined` (→ regex
|
|
@@ -5777,8 +5761,7 @@ function tryAstQuery(hook, ctx) {
|
|
|
5777
5761
|
function defineRule(rule) {
|
|
5778
5762
|
return rule;
|
|
5779
5763
|
}
|
|
5780
|
-
|
|
5781
|
-
//#region src/rules/shared/positions.ts
|
|
5764
|
+
// src/rules/shared/positions.ts
|
|
5782
5765
|
/**
|
|
5783
5766
|
* Shared position helpers (Phase 6 — Tempering Plan).
|
|
5784
5767
|
* One lineAt, one colAt, one matchBrace. The other 79 copies are dead.
|
|
@@ -5823,8 +5806,7 @@ function lineAt(text, index) {
|
|
|
5823
5806
|
function colAt(text, index) {
|
|
5824
5807
|
return index - (index <= 0 ? -1 : text.lastIndexOf("\n", index - 1));
|
|
5825
5808
|
}
|
|
5826
|
-
|
|
5827
|
-
//#region src/rules/test/qa-test-001-focused-test.ts
|
|
5809
|
+
// src/rules/test/qa-test-001-focused-test.ts
|
|
5828
5810
|
/**
|
|
5829
5811
|
* QA-TEST-001 — Focused test committed (`fit`/`fdescribe`/`.only`).
|
|
5830
5812
|
* Severity: error · Confidence: high · deterministic-defect
|
|
@@ -5890,8 +5872,7 @@ const focusedTestCommitted = defineRule({
|
|
|
5890
5872
|
return findings;
|
|
5891
5873
|
}
|
|
5892
5874
|
});
|
|
5893
|
-
|
|
5894
|
-
//#region src/rules/test/qa-test-002-skipped-test.ts
|
|
5875
|
+
// src/rules/test/qa-test-002-skipped-test.ts
|
|
5895
5876
|
/**
|
|
5896
5877
|
* QA-TEST-002 — Skipped test (`it.skip`, `xit`, `test.skip`, `describe.skip`).
|
|
5897
5878
|
* Severity: warning · Confidence: high · deterministic-defect
|
|
@@ -5947,8 +5928,7 @@ const skippedTest = defineRule({
|
|
|
5947
5928
|
return findings;
|
|
5948
5929
|
}
|
|
5949
5930
|
});
|
|
5950
|
-
|
|
5951
|
-
//#region src/rules/shared/masking.ts
|
|
5931
|
+
// src/rules/shared/masking.ts
|
|
5952
5932
|
/** True when the offset was blanked in codeText (inside a string or comment). */
|
|
5953
5933
|
function isMasked(ctx, index) {
|
|
5954
5934
|
const mask = ctx.codeText;
|
|
@@ -6009,8 +5989,7 @@ function isInsideEmbeddedCode(ctx, index) {
|
|
|
6009
5989
|
const hasCallSyntax = /\w\s*\(/.test(inner);
|
|
6010
5990
|
return hasNestedQuote && hasCallSyntax;
|
|
6011
5991
|
}
|
|
6012
|
-
|
|
6013
|
-
//#region src/rules/test/qa-test-003-no-assertions.ts
|
|
5992
|
+
// src/rules/test/qa-test-003-no-assertions.ts
|
|
6014
5993
|
/**
|
|
6015
5994
|
* QA-TEST-003 — Test with no assertions.
|
|
6016
5995
|
* Severity: error · Confidence: high · deterministic-defect
|
|
@@ -6088,8 +6067,7 @@ function matchBrace$2(text, open) {
|
|
|
6088
6067
|
}
|
|
6089
6068
|
return -1;
|
|
6090
6069
|
}
|
|
6091
|
-
|
|
6092
|
-
//#region src/rules/test/qa-test-004-hard-sleep.ts
|
|
6070
|
+
// src/rules/test/qa-test-004-hard-sleep.ts
|
|
6093
6071
|
/**
|
|
6094
6072
|
* QA-TEST-004 — Hard sleep (`sleep()`, `waitForTimeout()`, `setTimeout` as wait).
|
|
6095
6073
|
* Severity: warning · Confidence: high · deterministic-defect
|
|
@@ -6154,8 +6132,7 @@ const hardSleep = defineRule({
|
|
|
6154
6132
|
return findings;
|
|
6155
6133
|
}
|
|
6156
6134
|
});
|
|
6157
|
-
|
|
6158
|
-
//#region src/rules/test/qa-test-006-retry-abuse.ts
|
|
6135
|
+
// src/rules/test/qa-test-006-retry-abuse.ts
|
|
6159
6136
|
/**
|
|
6160
6137
|
* QA-TEST-006 — Retry abuse in test config.
|
|
6161
6138
|
* Severity: warning · Confidence: high · deterministic-defect
|
|
@@ -6221,8 +6198,7 @@ const retryAbuse = defineRule({
|
|
|
6221
6198
|
return findings;
|
|
6222
6199
|
}
|
|
6223
6200
|
});
|
|
6224
|
-
|
|
6225
|
-
//#region src/rules/test/qa-test-010-empty-body.ts
|
|
6201
|
+
// src/rules/test/qa-test-010-empty-body.ts
|
|
6226
6202
|
/**
|
|
6227
6203
|
* QA-TEST-010 — Empty test body.
|
|
6228
6204
|
* Severity: error · Confidence: high · deterministic-defect
|
|
@@ -6277,8 +6253,7 @@ const emptyTestBody = defineRule({
|
|
|
6277
6253
|
return findings;
|
|
6278
6254
|
}
|
|
6279
6255
|
});
|
|
6280
|
-
|
|
6281
|
-
//#region src/rules/quality/qa-tqual-002-tautological.ts
|
|
6256
|
+
// src/rules/quality/qa-tqual-002-tautological.ts
|
|
6282
6257
|
/**
|
|
6283
6258
|
* QA-TQUAL-002 — Tautological assertion.
|
|
6284
6259
|
* Severity: error · Confidence: high · deterministic-defect
|
|
@@ -6355,8 +6330,7 @@ const tautologicalAssertion = defineRule({
|
|
|
6355
6330
|
return findings;
|
|
6356
6331
|
}
|
|
6357
6332
|
});
|
|
6358
|
-
|
|
6359
|
-
//#region src/rules/quality/qa-tqual-009-promise-assertion.ts
|
|
6333
|
+
// src/rules/quality/qa-tqual-009-promise-assertion.ts
|
|
6360
6334
|
/**
|
|
6361
6335
|
* QA-TQUAL-009 — Unawaited assertion inside a promise chain.
|
|
6362
6336
|
* Severity: error · Confidence: high · deterministic-defect
|
|
@@ -6526,8 +6500,7 @@ function matchParen$2(text, open) {
|
|
|
6526
6500
|
}
|
|
6527
6501
|
return -1;
|
|
6528
6502
|
}
|
|
6529
|
-
|
|
6530
|
-
//#region src/rules/quality/qa-tqual-011-commented-out.ts
|
|
6503
|
+
// src/rules/quality/qa-tqual-011-commented-out.ts
|
|
6531
6504
|
/**
|
|
6532
6505
|
* QA-TQUAL-011 — Commented-out tests.
|
|
6533
6506
|
* Severity: warning · Confidence: high · deterministic-defect
|
|
@@ -6640,8 +6613,7 @@ function commentRanges(ctx) {
|
|
|
6640
6613
|
}
|
|
6641
6614
|
return ranges;
|
|
6642
6615
|
}
|
|
6643
|
-
|
|
6644
|
-
//#region src/rules/playwright/qa-pw-002-unawaited-assertion.ts
|
|
6616
|
+
// src/rules/playwright/qa-pw-002-unawaited-assertion.ts
|
|
6645
6617
|
/**
|
|
6646
6618
|
* QA-PW-002 — Missing `await` on a Playwright locator assertion.
|
|
6647
6619
|
* Severity: error · Confidence: high · deterministic-defect
|
|
@@ -6786,8 +6758,7 @@ function runRegexFallback(text, path) {
|
|
|
6786
6758
|
});
|
|
6787
6759
|
return findings;
|
|
6788
6760
|
}
|
|
6789
|
-
|
|
6790
|
-
//#region src/rules/playwright/qa-pw-003-debug-artifacts.ts
|
|
6761
|
+
// src/rules/playwright/qa-pw-003-debug-artifacts.ts
|
|
6791
6762
|
/**
|
|
6792
6763
|
* QA-PW-003 — Committed `page.pause()` or `test.only()` in e2e specs.
|
|
6793
6764
|
* Severity: error · Confidence: high · deterministic-defect
|
|
@@ -6839,8 +6810,7 @@ const committedDebugArtifacts = defineRule({
|
|
|
6839
6810
|
return findings;
|
|
6840
6811
|
}
|
|
6841
6812
|
});
|
|
6842
|
-
|
|
6843
|
-
//#region src/rules/playwright/qa-pw-004-brittle-selectors.ts
|
|
6813
|
+
// src/rules/playwright/qa-pw-004-brittle-selectors.ts
|
|
6844
6814
|
/**
|
|
6845
6815
|
* QA-PW-004 — Brittle CSS/XPath selectors vs role-based locators.
|
|
6846
6816
|
* Severity: warning · Confidence: medium · heuristic-risk
|
|
@@ -6932,8 +6902,7 @@ function stripComments(ctx) {
|
|
|
6932
6902
|
} catch {}
|
|
6933
6903
|
return ctx.text.replace(/\/\/[^\n]*/g, (cm) => " ".repeat(cm.length));
|
|
6934
6904
|
}
|
|
6935
|
-
|
|
6936
|
-
//#region src/rules/ci/verification-gate.ts
|
|
6905
|
+
// src/rules/ci/verification-gate.ts
|
|
6937
6906
|
/**
|
|
6938
6907
|
* Shared "is this shell command a verification gate?" matcher for the QA-CI
|
|
6939
6908
|
* rules. A gate is a command whose failure MUST fail the job — test runners,
|
|
@@ -6973,8 +6942,7 @@ const VERIFICATION_GATE_RE = new RegExp([
|
|
|
6973
6942
|
function looksLikeVerificationGate(cmd) {
|
|
6974
6943
|
return VERIFICATION_GATE_RE.test(cmd);
|
|
6975
6944
|
}
|
|
6976
|
-
|
|
6977
|
-
//#region src/rules/ci/azure-gates.ts
|
|
6945
|
+
// src/rules/ci/azure-gates.ts
|
|
6978
6946
|
/** Discriminator: the Azure parser is the only one emitting `platform`. */
|
|
6979
6947
|
function isAzurePipelineDoc(ast) {
|
|
6980
6948
|
return typeof ast === "object" && ast !== null && ast.platform === "azure-pipelines";
|
|
@@ -7058,8 +7026,7 @@ function locateAzureJobKey(text, job, keyName) {
|
|
|
7058
7026
|
if (m) return lineAt(text, m.index);
|
|
7059
7027
|
return 1;
|
|
7060
7028
|
}
|
|
7061
|
-
|
|
7062
|
-
//#region src/rules/ci/qa-ci-001-continue-on-error.ts
|
|
7029
|
+
// src/rules/ci/qa-ci-001-continue-on-error.ts
|
|
7063
7030
|
/**
|
|
7064
7031
|
* QA-CI-001 — `continue-on-error: true` masks a failing job.
|
|
7065
7032
|
* Severity: error · Confidence: high · deterministic-defect
|
|
@@ -7287,8 +7254,7 @@ function describeAzureStep(step) {
|
|
|
7287
7254
|
if (step.task) return step.task;
|
|
7288
7255
|
return "step";
|
|
7289
7256
|
}
|
|
7290
|
-
|
|
7291
|
-
//#region src/rules/ci/qa-ci-002-swallowed-exit.ts
|
|
7257
|
+
// src/rules/ci/qa-ci-002-swallowed-exit.ts
|
|
7292
7258
|
/**
|
|
7293
7259
|
* QA-CI-002 — `|| true` swallows a command's exit code.
|
|
7294
7260
|
* Severity: error · Confidence: high · deterministic-defect
|
|
@@ -7343,8 +7309,7 @@ const swallowedExitCode = defineRule({
|
|
|
7343
7309
|
return findings;
|
|
7344
7310
|
}
|
|
7345
7311
|
});
|
|
7346
|
-
|
|
7347
|
-
//#region src/rules/ci/qa-ci-007-retry-masking.ts
|
|
7312
|
+
// src/rules/ci/qa-ci-007-retry-masking.ts
|
|
7348
7313
|
/**
|
|
7349
7314
|
* QA-CI-007 — Retry masking test failures.
|
|
7350
7315
|
* Severity: warning · Confidence: high · deterministic-defect
|
|
@@ -7495,8 +7460,7 @@ function azureArm$1(ctx, doc, findings) {
|
|
|
7495
7460
|
});
|
|
7496
7461
|
}
|
|
7497
7462
|
}
|
|
7498
|
-
|
|
7499
|
-
//#region src/rules/ci/qa-ci-005-report-never-generated.ts
|
|
7463
|
+
// src/rules/ci/qa-ci-005-report-never-generated.ts
|
|
7500
7464
|
/**
|
|
7501
7465
|
* QA-CI-005 — Report consumed but never generated.
|
|
7502
7466
|
* Severity: error · Confidence: high · deterministic-defect
|
|
@@ -7654,8 +7618,7 @@ function findJobConsumerLine(text, jobName, consumerRe) {
|
|
|
7654
7618
|
const anywhere = new RegExp(consumerRe.source, "i").exec(text);
|
|
7655
7619
|
return anywhere ? lineAt(text, anywhere.index) : 1;
|
|
7656
7620
|
}
|
|
7657
|
-
|
|
7658
|
-
//#region src/rules/ci/jenkins-gates.ts
|
|
7621
|
+
// src/rules/ci/jenkins-gates.ts
|
|
7659
7622
|
/**
|
|
7660
7623
|
* Jenkinsfile block scanner for the QA-CI Jenkins arms (product-gap
|
|
7661
7624
|
* master plan P3c). Jenkinsfiles are Groovy text — "no new language
|
|
@@ -7901,8 +7864,7 @@ function textIsVerificationGate(text) {
|
|
|
7901
7864
|
function lineOfOffset(text, offset) {
|
|
7902
7865
|
return lineAt(text, offset);
|
|
7903
7866
|
}
|
|
7904
|
-
|
|
7905
|
-
//#region src/rules/ci/qa-ci-008-always-success.ts
|
|
7867
|
+
// src/rules/ci/qa-ci-008-always-success.ts
|
|
7906
7868
|
/**
|
|
7907
7869
|
* QA-CI-008 — Always-success step at the end of a job.
|
|
7908
7870
|
* Severity: error · Confidence: high · deterministic-defect
|
|
@@ -8076,8 +8038,7 @@ function jenkinsArm$1(ctx, findings) {
|
|
|
8076
8038
|
});
|
|
8077
8039
|
}
|
|
8078
8040
|
}
|
|
8079
|
-
|
|
8080
|
-
//#region src/rules/ci/qa-ci-009-exit-code.ts
|
|
8041
|
+
// src/rules/ci/qa-ci-009-exit-code.ts
|
|
8081
8042
|
/**
|
|
8082
8043
|
* QA-CI-009 — Test command does not propagate exit code.
|
|
8083
8044
|
* Severity: error · Confidence: high · deterministic-defect
|
|
@@ -8212,8 +8173,7 @@ function jenkinsArm(ctx, findings) {
|
|
|
8212
8173
|
});
|
|
8213
8174
|
}
|
|
8214
8175
|
}
|
|
8215
|
-
|
|
8216
|
-
//#region src/rules/ci/qa-ci-010-non-blocking.ts
|
|
8176
|
+
// src/rules/ci/qa-ci-010-non-blocking.ts
|
|
8217
8177
|
/**
|
|
8218
8178
|
* QA-CI-010 — Required test job is non-blocking for branch protection.
|
|
8219
8179
|
* Severity: error · Confidence: medium · deterministic-defect
|
|
@@ -8296,8 +8256,7 @@ function findLine(text, re) {
|
|
|
8296
8256
|
function escapeRe(s) {
|
|
8297
8257
|
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
8298
8258
|
}
|
|
8299
|
-
|
|
8300
|
-
//#region src/rules/ci/qa-ci-013-can-never-fail.ts
|
|
8259
|
+
// src/rules/ci/qa-ci-013-can-never-fail.ts
|
|
8301
8260
|
/**
|
|
8302
8261
|
* QA-CI-013 — Verification stage/job conditioned so it can never fail the
|
|
8303
8262
|
* pipeline (product-gap master plan P3b).
|
|
@@ -8401,8 +8360,7 @@ const canNeverFailGate = defineRule({
|
|
|
8401
8360
|
function jobHasGate(job) {
|
|
8402
8361
|
return (job.steps ?? []).some((s) => s !== void 0 && stepIsVerificationGate$1(s));
|
|
8403
8362
|
}
|
|
8404
|
-
|
|
8405
|
-
//#region src/rules/ci/qa-ci-014-swallowed-verification.ts
|
|
8363
|
+
// src/rules/ci/qa-ci-014-swallowed-verification.ts
|
|
8406
8364
|
/**
|
|
8407
8365
|
* QA-CI-014 — try/catch swallowing a verification-stage failure without
|
|
8408
8366
|
* rethrow or failure-marking (product-gap master plan P3c).
|
|
@@ -8459,8 +8417,7 @@ const swallowedVerificationFailure = defineRule({
|
|
|
8459
8417
|
return findings;
|
|
8460
8418
|
}
|
|
8461
8419
|
});
|
|
8462
|
-
|
|
8463
|
-
//#region src/rules/python/qa-py-003-no-assertions.ts
|
|
8420
|
+
// src/rules/python/qa-py-003-no-assertions.ts
|
|
8464
8421
|
/**
|
|
8465
8422
|
* QA-PY-003 — Test function with no assertions.
|
|
8466
8423
|
* Severity: error · Confidence: high · deterministic-defect
|
|
@@ -8581,8 +8538,7 @@ function extractBlock$1(text, afterColon) {
|
|
|
8581
8538
|
const lineEnd = rest.indexOf("\n", firstIdx);
|
|
8582
8539
|
return lineEnd === -1 ? rest : rest.slice(0, lineEnd);
|
|
8583
8540
|
}
|
|
8584
|
-
|
|
8585
|
-
//#region src/rules/python/qa-py-005-hard-sleep.ts
|
|
8541
|
+
// src/rules/python/qa-py-005-hard-sleep.ts
|
|
8586
8542
|
/**
|
|
8587
8543
|
* QA-PY-005 — time.sleep() in tests.
|
|
8588
8544
|
* Severity: warning · Confidence: high · deterministic-defect
|
|
@@ -8629,8 +8585,7 @@ const pyHardSleep = defineRule({
|
|
|
8629
8585
|
return findings;
|
|
8630
8586
|
}
|
|
8631
8587
|
});
|
|
8632
|
-
|
|
8633
|
-
//#region src/rules/python/qa-py-002-skipped-test.ts
|
|
8588
|
+
// src/rules/python/qa-py-002-skipped-test.ts
|
|
8634
8589
|
/**
|
|
8635
8590
|
* QA-PY-002 — Skipped test (`@pytest.mark.skip`, `xfail` non-strict).
|
|
8636
8591
|
* Severity: warning · Confidence: high · deterministic-defect
|
|
@@ -8690,8 +8645,7 @@ const pySkippedTest = defineRule({
|
|
|
8690
8645
|
return findings;
|
|
8691
8646
|
}
|
|
8692
8647
|
});
|
|
8693
|
-
|
|
8694
|
-
//#region src/rules/python/qa-py-012-tautological.ts
|
|
8648
|
+
// src/rules/python/qa-py-012-tautological.ts
|
|
8695
8649
|
/**
|
|
8696
8650
|
* QA-PY-012 — Tautological assertion (`assert True`, `assert x == x`).
|
|
8697
8651
|
* Severity: error · Confidence: high · deterministic-defect
|
|
@@ -8746,8 +8700,7 @@ const pyTautological = defineRule({
|
|
|
8746
8700
|
return findings;
|
|
8747
8701
|
}
|
|
8748
8702
|
});
|
|
8749
|
-
|
|
8750
|
-
//#region src/rules/python/qa-py-001-focused-test.ts
|
|
8703
|
+
// src/rules/python/qa-py-001-focused-test.ts
|
|
8751
8704
|
/**
|
|
8752
8705
|
* QA-PY-001 — Focused test committed.
|
|
8753
8706
|
* Severity: error · Confidence: high · deterministic-defect
|
|
@@ -8801,8 +8754,7 @@ const pyFocusedTest = defineRule({
|
|
|
8801
8754
|
return findings;
|
|
8802
8755
|
}
|
|
8803
8756
|
});
|
|
8804
|
-
|
|
8805
|
-
//#region src/engine/jv-cs-ast.ts
|
|
8757
|
+
// src/engine/jv-cs-ast.ts
|
|
8806
8758
|
/** Narrow the loose `ast` seam back to a tree-sitter Tree. */
|
|
8807
8759
|
function getTreeSitterTree(ast) {
|
|
8808
8760
|
if (!(ast instanceof Object) || !("rootNode" in ast)) return void 0;
|
|
@@ -9006,8 +8958,7 @@ function isHelperIdiom(name) {
|
|
|
9006
8958
|
}
|
|
9007
8959
|
return false;
|
|
9008
8960
|
}
|
|
9009
|
-
|
|
9010
|
-
//#region src/engine/python-ast.ts
|
|
8961
|
+
// src/engine/python-ast.ts
|
|
9011
8962
|
function getPythonTree(ast) {
|
|
9012
8963
|
return getTreeSitterTree(ast);
|
|
9013
8964
|
}
|
|
@@ -9092,8 +9043,7 @@ function pythonWithRaisesBlocks(tree) {
|
|
|
9092
9043
|
});
|
|
9093
9044
|
return out;
|
|
9094
9045
|
}
|
|
9095
|
-
|
|
9096
|
-
//#region src/rules/python/qa-py-007-raises-without-match.ts
|
|
9046
|
+
// src/rules/python/qa-py-007-raises-without-match.ts
|
|
9097
9047
|
/**
|
|
9098
9048
|
* QA-PY-007 — pytest.raises without match.
|
|
9099
9049
|
* Severity: warning · Confidence: medium · heuristic-risk
|
|
@@ -9220,8 +9170,7 @@ function astArm(ctx, tree) {
|
|
|
9220
9170
|
}
|
|
9221
9171
|
return findings;
|
|
9222
9172
|
}
|
|
9223
|
-
|
|
9224
|
-
//#region src/rules/python/qa-py-009-commented-out.ts
|
|
9173
|
+
// src/rules/python/qa-py-009-commented-out.ts
|
|
9225
9174
|
/**
|
|
9226
9175
|
* QA-PY-009 — Commented-out tests.
|
|
9227
9176
|
* Severity: warning · Confidence: high · deterministic-defect
|
|
@@ -9268,8 +9217,7 @@ const pyCommentedOutTest = defineRule({
|
|
|
9268
9217
|
return findings;
|
|
9269
9218
|
}
|
|
9270
9219
|
});
|
|
9271
|
-
|
|
9272
|
-
//#region src/rules/python/qa-py-004-bare-truthiness.ts
|
|
9220
|
+
// src/rules/python/qa-py-004-bare-truthiness.ts
|
|
9273
9221
|
/**
|
|
9274
9222
|
* QA-PY-004 — Bare truthiness assert on complex object.
|
|
9275
9223
|
* Severity: warning · Confidence: medium · heuristic-risk
|
|
@@ -9348,8 +9296,7 @@ const pyBareTruthinessAssert = defineRule({
|
|
|
9348
9296
|
return findings;
|
|
9349
9297
|
}
|
|
9350
9298
|
});
|
|
9351
|
-
|
|
9352
|
-
//#region src/rules/python/qa-py-011-mutable-fixture.ts
|
|
9299
|
+
// src/rules/python/qa-py-011-mutable-fixture.ts
|
|
9353
9300
|
/**
|
|
9354
9301
|
* QA-PY-011 — Mutable fixture shared across tests.
|
|
9355
9302
|
* Severity: warning · Confidence: medium · heuristic-risk
|
|
@@ -9400,8 +9347,7 @@ const pyMutableFixture = defineRule({
|
|
|
9400
9347
|
return findings;
|
|
9401
9348
|
}
|
|
9402
9349
|
});
|
|
9403
|
-
|
|
9404
|
-
//#region src/rules/playwright/qa-pw-101-wait-for-timeout.ts
|
|
9350
|
+
// src/rules/playwright/qa-pw-101-wait-for-timeout.ts
|
|
9405
9351
|
/**
|
|
9406
9352
|
* QA-PW-101 — waitForTimeout() anywhere.
|
|
9407
9353
|
* Severity: error · Confidence: high · deterministic-defect
|
|
@@ -9448,8 +9394,7 @@ const pwWaitForTimeout = defineRule({
|
|
|
9448
9394
|
return findings;
|
|
9449
9395
|
}
|
|
9450
9396
|
});
|
|
9451
|
-
|
|
9452
|
-
//#region src/rules/playwright/qa-pw-102-wait-load.ts
|
|
9397
|
+
// src/rules/playwright/qa-pw-102-wait-load.ts
|
|
9453
9398
|
/**
|
|
9454
9399
|
* QA-PW-102 — waitForEvent('load') / waitForLoadState('load') instead of
|
|
9455
9400
|
* web-first assertions.
|
|
@@ -9503,8 +9448,7 @@ const pwWaitForLoadEvent = defineRule({
|
|
|
9503
9448
|
return findings;
|
|
9504
9449
|
}
|
|
9505
9450
|
});
|
|
9506
|
-
|
|
9507
|
-
//#region src/rules/playwright/qa-pw-113-deep-frames.ts
|
|
9451
|
+
// src/rules/playwright/qa-pw-113-deep-frames.ts
|
|
9508
9452
|
/**
|
|
9509
9453
|
* QA-PW-113 — frameLocator chains > 2 deep.
|
|
9510
9454
|
* Severity: warning · Confidence: medium · heuristic-risk
|
|
@@ -9554,8 +9498,7 @@ const pwDeepFrameLocator = defineRule({
|
|
|
9554
9498
|
function count(fragment) {
|
|
9555
9499
|
return fragment.match(/frameLocator\s*\(/g).length;
|
|
9556
9500
|
}
|
|
9557
|
-
|
|
9558
|
-
//#region src/rules/playwright/qa-pw-117-serial.ts
|
|
9501
|
+
// src/rules/playwright/qa-pw-117-serial.ts
|
|
9559
9502
|
/**
|
|
9560
9503
|
* QA-PW-117 — test.describe.serial without justification comment.
|
|
9561
9504
|
* Severity: warning · Confidence: high · deterministic-defect
|
|
@@ -9609,8 +9552,7 @@ const pwSerialNoJustification = defineRule({
|
|
|
9609
9552
|
return findings;
|
|
9610
9553
|
}
|
|
9611
9554
|
});
|
|
9612
|
-
|
|
9613
|
-
//#region src/rules/playwright/qa-pw-121-config-retries.ts
|
|
9555
|
+
// src/rules/playwright/qa-pw-121-config-retries.ts
|
|
9614
9556
|
/**
|
|
9615
9557
|
* QA-PW-121 — playwright.config: retries >= 3 or workers = 0.
|
|
9616
9558
|
* Severity: warning · Confidence: high · deterministic-defect
|
|
@@ -9675,8 +9617,7 @@ const pwConfigRetryAbuse = defineRule({
|
|
|
9675
9617
|
return findings;
|
|
9676
9618
|
}
|
|
9677
9619
|
});
|
|
9678
|
-
|
|
9679
|
-
//#region src/rules/playwright/qa-pw-122-no-trace.ts
|
|
9620
|
+
// src/rules/playwright/qa-pw-122-no-trace.ts
|
|
9680
9621
|
/**
|
|
9681
9622
|
* QA-PW-122 — No trace capture on retry in playwright.config.
|
|
9682
9623
|
* Severity: warning · Confidence: high · deterministic-defect
|
|
@@ -9725,8 +9666,7 @@ const pwNoTraceOnRetry = defineRule({
|
|
|
9725
9666
|
return findings;
|
|
9726
9667
|
}
|
|
9727
9668
|
});
|
|
9728
|
-
|
|
9729
|
-
//#region src/rules/playwright/qa-pw-124-project-split.ts
|
|
9669
|
+
// src/rules/playwright/qa-pw-124-project-split.ts
|
|
9730
9670
|
/**
|
|
9731
9671
|
* QA-PW-124 — Projects defined but no smoke/regression split.
|
|
9732
9672
|
* Severity: info · Confidence: high · deterministic-defect
|
|
@@ -9788,8 +9728,7 @@ const pwNoProjectSplit = defineRule({
|
|
|
9788
9728
|
return findings;
|
|
9789
9729
|
}
|
|
9790
9730
|
});
|
|
9791
|
-
|
|
9792
|
-
//#region src/rules/playwright/qa-pw-104-trial-click.ts
|
|
9731
|
+
// src/rules/playwright/qa-pw-104-trial-click.ts
|
|
9793
9732
|
/**
|
|
9794
9733
|
* QA-PW-104 — `{ trial: true }` misuse — check-then-act race.
|
|
9795
9734
|
* Severity: warning · Confidence: medium · heuristic-risk
|
|
@@ -9835,8 +9774,7 @@ const pwTrialMisuse = defineRule({
|
|
|
9835
9774
|
return findings;
|
|
9836
9775
|
}
|
|
9837
9776
|
});
|
|
9838
|
-
|
|
9839
|
-
//#region src/rules/playwright/qa-pw-116-storage-state.ts
|
|
9777
|
+
// src/rules/playwright/qa-pw-116-storage-state.ts
|
|
9840
9778
|
/**
|
|
9841
9779
|
* QA-PW-116 — storageState reuse without expiry strategy.
|
|
9842
9780
|
* Severity: warning · Confidence: medium · heuristic-risk
|
|
@@ -9882,8 +9820,7 @@ const pwStorageStateNoExpiry = defineRule({
|
|
|
9882
9820
|
return findings;
|
|
9883
9821
|
}
|
|
9884
9822
|
});
|
|
9885
|
-
|
|
9886
|
-
//#region src/rules/playwright/qa-pw-125-global-setup.ts
|
|
9823
|
+
// src/rules/playwright/qa-pw-125-global-setup.ts
|
|
9887
9824
|
/**
|
|
9888
9825
|
* QA-PW-125 — globalSetup mutating shared external state.
|
|
9889
9826
|
* Severity: warning · Confidence: medium · heuristic-risk
|
|
@@ -9941,8 +9878,7 @@ const pwGlobalSetupSharedState = defineRule({
|
|
|
9941
9878
|
return findings;
|
|
9942
9879
|
}
|
|
9943
9880
|
});
|
|
9944
|
-
|
|
9945
|
-
//#region src/rules/playwright/qa-pw-115-shared-page.ts
|
|
9881
|
+
// src/rules/playwright/qa-pw-115-shared-page.ts
|
|
9946
9882
|
/**
|
|
9947
9883
|
* QA-PW-115 — Shared `page` across parallel tests without isolation.
|
|
9948
9884
|
* Severity: warning · Confidence: medium · heuristic-risk
|
|
@@ -9990,8 +9926,7 @@ const pwSharedPage = defineRule({
|
|
|
9990
9926
|
return findings;
|
|
9991
9927
|
}
|
|
9992
9928
|
});
|
|
9993
|
-
|
|
9994
|
-
//#region src/rules/playwright/qa-pw-140.ts
|
|
9929
|
+
// src/rules/playwright/qa-pw-140.ts
|
|
9995
9930
|
/**
|
|
9996
9931
|
* QA-PW-140 — Screenshot without maxDiffPixelRatio.
|
|
9997
9932
|
*
|
|
@@ -10065,8 +10000,7 @@ function matchParen(text, open) {
|
|
|
10065
10000
|
}
|
|
10066
10001
|
return -1;
|
|
10067
10002
|
}
|
|
10068
|
-
|
|
10069
|
-
//#region src/rules/playwright/qa-pw-123-hardcoded-url.ts
|
|
10003
|
+
// src/rules/playwright/qa-pw-123-hardcoded-url.ts
|
|
10070
10004
|
/**
|
|
10071
10005
|
* QA-PW-123 — Hardcoded baseURL in specs.
|
|
10072
10006
|
* Severity: warning · Confidence: high · deterministic-defect
|
|
@@ -10114,8 +10048,7 @@ const hardcodedBaseUrl = defineRule({
|
|
|
10114
10048
|
return findings;
|
|
10115
10049
|
}
|
|
10116
10050
|
});
|
|
10117
|
-
|
|
10118
|
-
//#region src/rules/quality/qa-env-001-env-coupling.ts
|
|
10051
|
+
// src/rules/quality/qa-env-001-env-coupling.ts
|
|
10119
10052
|
/**
|
|
10120
10053
|
* QA-ENV-001 — "Works On My Machine" detector core (Tier 5 #24).
|
|
10121
10054
|
* Environment coupling in test files: fixed ports, OS paths,
|
|
@@ -10182,8 +10115,7 @@ const envCoupling = defineRule({
|
|
|
10182
10115
|
return findings;
|
|
10183
10116
|
}
|
|
10184
10117
|
});
|
|
10185
|
-
|
|
10186
|
-
//#region src/rules/playwright/qa-pw-141-retry-no-triage.ts
|
|
10118
|
+
// src/rules/playwright/qa-pw-141-retry-no-triage.ts
|
|
10187
10119
|
/**
|
|
10188
10120
|
* QA-PW-141 — playwright.config retries set high enough to mask flakes.
|
|
10189
10121
|
* Severity: warning · Confidence: high · deterministic-defect
|
|
@@ -10240,8 +10172,7 @@ const pwRetryMaskingNoForensics = defineRule({
|
|
|
10240
10172
|
return findings;
|
|
10241
10173
|
}
|
|
10242
10174
|
});
|
|
10243
|
-
|
|
10244
|
-
//#region src/rules/playwright/qa-pw-142-blanket-route.ts
|
|
10175
|
+
// src/rules/playwright/qa-pw-142-blanket-route.ts
|
|
10245
10176
|
/**
|
|
10246
10177
|
* QA-PW-142 — page.route() blanket-mocking instead of scoped intercepts.
|
|
10247
10178
|
* Severity: warning · Confidence: medium · heuristic-risk
|
|
@@ -10296,8 +10227,7 @@ const pwBlanketRouteMock = defineRule({
|
|
|
10296
10227
|
return findings;
|
|
10297
10228
|
}
|
|
10298
10229
|
});
|
|
10299
|
-
|
|
10300
|
-
//#region src/rules/playwright/qa-pw-143-no-artifacts.ts
|
|
10230
|
+
// src/rules/playwright/qa-pw-143-no-artifacts.ts
|
|
10301
10231
|
/**
|
|
10302
10232
|
* QA-PW-143 — No screenshot/video artifact capture on failure.
|
|
10303
10233
|
* Severity: info · Confidence: high · deterministic-defect
|
|
@@ -10350,8 +10280,7 @@ const pwNoFailureArtifacts = defineRule({
|
|
|
10350
10280
|
return findings;
|
|
10351
10281
|
}
|
|
10352
10282
|
});
|
|
10353
|
-
|
|
10354
|
-
//#region src/rules/playwright/qa-pw-144-single-browser.ts
|
|
10283
|
+
// src/rules/playwright/qa-pw-144-single-browser.ts
|
|
10355
10284
|
/**
|
|
10356
10285
|
* QA-PW-144 — Single-browser project matrix (no cross-browser coverage).
|
|
10357
10286
|
* Severity: info · Confidence: high · deterministic-defect
|
|
@@ -10413,8 +10342,7 @@ const pwSingleBrowserMatrix = defineRule({
|
|
|
10413
10342
|
return findings;
|
|
10414
10343
|
}
|
|
10415
10344
|
});
|
|
10416
|
-
|
|
10417
|
-
//#region src/rules/playwright/qa-pw-146-css-locator.ts
|
|
10345
|
+
// src/rules/playwright/qa-pw-146-css-locator.ts
|
|
10418
10346
|
/**
|
|
10419
10347
|
* QA-PW-146 — CSS/XPath string selector where Playwright's locator
|
|
10420
10348
|
* standard expects a normalized locator (plan §17.3).
|
|
@@ -10493,8 +10421,7 @@ const pwLocatorNormalize = defineRule({
|
|
|
10493
10421
|
return findings;
|
|
10494
10422
|
}
|
|
10495
10423
|
});
|
|
10496
|
-
|
|
10497
|
-
//#region src/rules/playwright/qa-pw-147-codegen-artifact.ts
|
|
10424
|
+
// src/rules/playwright/qa-pw-147-codegen-artifact.ts
|
|
10498
10425
|
/**
|
|
10499
10426
|
* QA-PW-147 — Playwright codegen artifact left unedited (plan §17.1).
|
|
10500
10427
|
* Severity: info · Confidence: medium · observation
|
|
@@ -10553,8 +10480,7 @@ const pwCodegenArtifact = defineRule({
|
|
|
10553
10480
|
return findings;
|
|
10554
10481
|
}
|
|
10555
10482
|
});
|
|
10556
|
-
|
|
10557
|
-
//#region src/rules/python/qa-py-101-sync-async-mix.ts
|
|
10483
|
+
// src/rules/python/qa-py-101-sync-async-mix.ts
|
|
10558
10484
|
/**
|
|
10559
10485
|
* QA-PY-101 — Playwright-Python: sync API in async test (or vice versa).
|
|
10560
10486
|
* Severity: warning · Confidence: high · deterministic-defect
|
|
@@ -10603,8 +10529,7 @@ const pyPwSyncAsyncMix = defineRule({
|
|
|
10603
10529
|
return findings;
|
|
10604
10530
|
}
|
|
10605
10531
|
});
|
|
10606
|
-
|
|
10607
|
-
//#region src/rules/python/qa-py-103-wait-for-timeout.ts
|
|
10532
|
+
// src/rules/python/qa-py-103-wait-for-timeout.ts
|
|
10608
10533
|
/**
|
|
10609
10534
|
* QA-PY-103 — Playwright-Python: wait_for_timeout() as synchronization.
|
|
10610
10535
|
* Severity: warning · Confidence: high · deterministic-defect
|
|
@@ -10651,8 +10576,7 @@ const pyPwWaitForTimeout = defineRule({
|
|
|
10651
10576
|
return findings;
|
|
10652
10577
|
}
|
|
10653
10578
|
});
|
|
10654
|
-
|
|
10655
|
-
//#region src/rules/python/qa-py-105-pw-no-assertions.ts
|
|
10579
|
+
// src/rules/python/qa-py-105-pw-no-assertions.ts
|
|
10656
10580
|
/**
|
|
10657
10581
|
* QA-PY-105 — Playwright-Python: no assertion in Playwright test.
|
|
10658
10582
|
* Severity: error · Confidence: high · deterministic-defect
|
|
@@ -10732,8 +10656,7 @@ function extractBlock(text, afterColon) {
|
|
|
10732
10656
|
const lineEnd = rest.indexOf("\n", firstContent.index);
|
|
10733
10657
|
return lineEnd === -1 ? rest : rest.slice(0, lineEnd);
|
|
10734
10658
|
}
|
|
10735
|
-
|
|
10736
|
-
//#region src/rules/java/qa-jv-101-disabled-test.ts
|
|
10659
|
+
// src/rules/java/qa-jv-101-disabled-test.ts
|
|
10737
10660
|
/**
|
|
10738
10661
|
* QA-JV-101 — @Disabled test without justification.
|
|
10739
10662
|
* Severity: warning · Confidence: high · deterministic-defect
|
|
@@ -10787,8 +10710,7 @@ const jvDisabledTest = defineRule({
|
|
|
10787
10710
|
return findings;
|
|
10788
10711
|
}
|
|
10789
10712
|
});
|
|
10790
|
-
|
|
10791
|
-
//#region src/rules/java/qa-jv-103-no-assertions.ts
|
|
10713
|
+
// src/rules/java/qa-jv-103-no-assertions.ts
|
|
10792
10714
|
/**
|
|
10793
10715
|
* QA-JV-103 — Test method without assertions.
|
|
10794
10716
|
* Severity: error · Confidence: high · deterministic-defect
|
|
@@ -10945,8 +10867,7 @@ function matchBrace$1(text, open) {
|
|
|
10945
10867
|
}
|
|
10946
10868
|
return -1;
|
|
10947
10869
|
}
|
|
10948
|
-
|
|
10949
|
-
//#region src/engine/qa-model.ts
|
|
10870
|
+
// src/engine/qa-model.ts
|
|
10950
10871
|
/**
|
|
10951
10872
|
* Extract the semantic model for one parsed file. Resolves undefined
|
|
10952
10873
|
* when the language has no extractor or the parse stage produced no
|
|
@@ -11489,8 +11410,7 @@ function extractPythonModel(file) {
|
|
|
11489
11410
|
nodes
|
|
11490
11411
|
};
|
|
11491
11412
|
}
|
|
11492
|
-
|
|
11493
|
-
//#region src/rules/java/qa-jv-105-wait-for-timeout.ts
|
|
11413
|
+
// src/rules/java/qa-jv-105-wait-for-timeout.ts
|
|
11494
11414
|
/**
|
|
11495
11415
|
* QA-JV-105 — waitForTimeout / hardcoded navigation waits.
|
|
11496
11416
|
* Severity: warning · Confidence: high · deterministic-defect
|
|
@@ -11575,8 +11495,7 @@ const jvWaitForTimeout = defineRule({
|
|
|
11575
11495
|
return findings;
|
|
11576
11496
|
}
|
|
11577
11497
|
});
|
|
11578
|
-
|
|
11579
|
-
//#region src/rules/csharp/qa-cs-101-skipped-test.ts
|
|
11498
|
+
// src/rules/csharp/qa-cs-101-skipped-test.ts
|
|
11580
11499
|
/**
|
|
11581
11500
|
* QA-CS-101 — [Ignore]/[Skip] test without justification.
|
|
11582
11501
|
* Severity: warning · Confidence: high · deterministic-defect
|
|
@@ -11640,8 +11559,7 @@ const csSkippedTest = defineRule({
|
|
|
11640
11559
|
return findings;
|
|
11641
11560
|
}
|
|
11642
11561
|
});
|
|
11643
|
-
|
|
11644
|
-
//#region src/rules/csharp/qa-cs-103-no-assertions.ts
|
|
11562
|
+
// src/rules/csharp/qa-cs-103-no-assertions.ts
|
|
11645
11563
|
/**
|
|
11646
11564
|
* QA-CS-103 — Test method without assertions.
|
|
11647
11565
|
* Severity: error · Confidence: high · deterministic-defect
|
|
@@ -11675,7 +11593,6 @@ const csSkippedTest = defineRule({
|
|
|
11675
11593
|
* Deep-diffing those requires symbol semantics Mjölnir deliberately does
|
|
11676
11594
|
* not promise for C# (plan §13.4).
|
|
11677
11595
|
*/
|
|
11678
|
-
/** Helper/verification idiom: verify/check/assert-prefixed names. */
|
|
11679
11596
|
const WHY$5 = "Without an assertion the test can only fail by throwing — it cannot detect behavioral regressions.";
|
|
11680
11597
|
const FIX$4 = "Add an assertion on the expected outcome, or remove the test.";
|
|
11681
11598
|
function makeFinding(path, position, name) {
|
|
@@ -11839,8 +11756,7 @@ function matchBrace(text, open) {
|
|
|
11839
11756
|
}
|
|
11840
11757
|
return -1;
|
|
11841
11758
|
}
|
|
11842
|
-
|
|
11843
|
-
//#region src/rules/csharp/qa-cs-105-wait-for-timeout.ts
|
|
11759
|
+
// src/rules/csharp/qa-cs-105-wait-for-timeout.ts
|
|
11844
11760
|
/**
|
|
11845
11761
|
* QA-CS-105 — Playwright-.NET: WaitForTimeoutAsync hard sleep.
|
|
11846
11762
|
* Severity: warning · Confidence: high · deterministic-defect
|
|
@@ -11928,8 +11844,7 @@ const csWaitForTimeout = defineRule({
|
|
|
11928
11844
|
return findings;
|
|
11929
11845
|
}
|
|
11930
11846
|
});
|
|
11931
|
-
|
|
11932
|
-
//#region src/rules/cypress/qa-cyp-001-cy-wait.ts
|
|
11847
|
+
// src/rules/cypress/qa-cyp-001-cy-wait.ts
|
|
11933
11848
|
/**
|
|
11934
11849
|
* QA-CYP-001 — Fixed `cy.wait(n)` (hard-coded wait).
|
|
11935
11850
|
* Severity: warning · Confidence: high · deterministic-defect
|
|
@@ -12008,8 +11923,7 @@ function isCypressFile(ctx) {
|
|
|
12008
11923
|
if (/\.cy\.[jt]sx?$/.test(ctx.path)) return true;
|
|
12009
11924
|
return /\bcy\s*\./.test(ctx.codeText ?? ctx.text);
|
|
12010
11925
|
}
|
|
12011
|
-
|
|
12012
|
-
//#region src/rules/cypress/qa-cyp-002-focused-test.ts
|
|
11926
|
+
// src/rules/cypress/qa-cyp-002-focused-test.ts
|
|
12013
11927
|
/**
|
|
12014
11928
|
* QA-CYP-002 — Focused test (`.only`).
|
|
12015
11929
|
* Severity: error · Confidence: high · deterministic-defect
|
|
@@ -12072,8 +11986,7 @@ const cypFocusedTest = defineRule({
|
|
|
12072
11986
|
return findings;
|
|
12073
11987
|
}
|
|
12074
11988
|
});
|
|
12075
|
-
|
|
12076
|
-
//#region src/rules/cypress/qa-cyp-003-config-security.ts
|
|
11989
|
+
// src/rules/cypress/qa-cyp-003-config-security.ts
|
|
12077
11990
|
/**
|
|
12078
11991
|
* QA-CYP-003 — Cypress config disables `chromeWebSecurity`.
|
|
12079
11992
|
* Severity: error · Confidence: high · deterministic-defect
|
|
@@ -12143,8 +12056,7 @@ const cypConfigSecurity = defineRule({
|
|
|
12143
12056
|
return findings;
|
|
12144
12057
|
}
|
|
12145
12058
|
});
|
|
12146
|
-
|
|
12147
|
-
//#region src/rules/selenium/qa-se-sleep-lookup.ts
|
|
12059
|
+
// src/rules/selenium/qa-se-sleep-lookup.ts
|
|
12148
12060
|
/**
|
|
12149
12061
|
* Selenium sleep-then-interact rules (Verification Trust Evolution Plan
|
|
12150
12062
|
* §15.3 — "Selenium implicit-wait abuse / missing WebDriverWait",
|
|
@@ -12295,8 +12207,7 @@ const sePythonSleepLookup = defineRule({
|
|
|
12295
12207
|
return sleepBeforeLookup(ctx, PY_SLEEP_RE, PY_LOOKUP_RE, "QA-SE-003");
|
|
12296
12208
|
}
|
|
12297
12209
|
});
|
|
12298
|
-
|
|
12299
|
-
//#region src/rules/shared/family.ts
|
|
12210
|
+
// src/rules/shared/family.ts
|
|
12300
12211
|
/**
|
|
12301
12212
|
* Cross-language pattern rule family generator (Phase 6 — Tempering Plan).
|
|
12302
12213
|
*
|
|
@@ -12362,8 +12273,7 @@ function definePatternFamily(opts) {
|
|
|
12362
12273
|
}
|
|
12363
12274
|
}));
|
|
12364
12275
|
}
|
|
12365
|
-
|
|
12366
|
-
//#region src/rules/families/hard-sleep.ts
|
|
12276
|
+
// src/rules/families/hard-sleep.ts
|
|
12367
12277
|
/**
|
|
12368
12278
|
* Hard-sleep family (Phase 6 — Tempering Plan).
|
|
12369
12279
|
* Java: Thread.sleep(), C#: Thread.Sleep() / Task.Delay()
|
|
@@ -12514,8 +12424,7 @@ const hardSleepFamily = definePatternFamily({
|
|
|
12514
12424
|
fix: "Use `await Assertions.Expect(locator).ToBeVisibleAsync()` or locator.WaitForAsync()."
|
|
12515
12425
|
}]
|
|
12516
12426
|
});
|
|
12517
|
-
|
|
12518
|
-
//#region src/rules/families/network-idle.ts
|
|
12427
|
+
// src/rules/families/network-idle.ts
|
|
12519
12428
|
/**
|
|
12520
12429
|
* Network-idle family (Phase 6 — Tempering Plan).
|
|
12521
12430
|
* All three language variants of waitForLoadState(networkidle).
|
|
@@ -12576,8 +12485,7 @@ const networkIdleFamily = definePatternFamily({
|
|
|
12576
12485
|
}
|
|
12577
12486
|
]
|
|
12578
12487
|
});
|
|
12579
|
-
|
|
12580
|
-
//#region src/rules/families/hardcoded-url.ts
|
|
12488
|
+
// src/rules/families/hardcoded-url.ts
|
|
12581
12489
|
/**
|
|
12582
12490
|
* Hardcoded-URL family (Phase 6 — Tempering Plan).
|
|
12583
12491
|
* Detects absolute URLs in navigation/request calls across languages.
|
|
@@ -12627,8 +12535,7 @@ const hardcodedUrlFamily = definePatternFamily({
|
|
|
12627
12535
|
fix: "Use `--base-url` / `base_url` fixture, or `os.environ[...]`."
|
|
12628
12536
|
}]
|
|
12629
12537
|
});
|
|
12630
|
-
|
|
12631
|
-
//#region src/rules/families/shared-page.ts
|
|
12538
|
+
// src/rules/families/shared-page.ts
|
|
12632
12539
|
/**
|
|
12633
12540
|
* Shared-page family (Phase 6 — Tempering Plan).
|
|
12634
12541
|
* Detects static/module-level Page/Browser instances shared across tests.
|
|
@@ -12689,8 +12596,7 @@ const sharedPageFamily = definePatternFamily({
|
|
|
12689
12596
|
}
|
|
12690
12597
|
]
|
|
12691
12598
|
});
|
|
12692
|
-
|
|
12693
|
-
//#region src/rules/families/brittle-selectors.ts
|
|
12599
|
+
// src/rules/families/brittle-selectors.ts
|
|
12694
12600
|
/**
|
|
12695
12601
|
* Brittle-selectors family (Phase 6 — Tempering Plan).
|
|
12696
12602
|
* Detects XPath/structural CSS selectors across Java, C#, Python.
|
|
@@ -12802,8 +12708,7 @@ const brittleSelectorsFamily = [
|
|
|
12802
12708
|
}
|
|
12803
12709
|
], "Prefer role-based locators (`get_by_role(...)`) or data-testid attributes.")
|
|
12804
12710
|
];
|
|
12805
|
-
|
|
12806
|
-
//#region src/rules/families/retry-masking.ts
|
|
12711
|
+
// src/rules/families/retry-masking.ts
|
|
12807
12712
|
/**
|
|
12808
12713
|
* Retry-masking family (Phase 6 — Tempering Plan).
|
|
12809
12714
|
* Detects retry annotations/attributes that mask flaky tests.
|
|
@@ -12923,8 +12828,7 @@ const retryMaskingFamily = [defineRule({
|
|
|
12923
12828
|
return findings;
|
|
12924
12829
|
}
|
|
12925
12830
|
})];
|
|
12926
|
-
|
|
12927
|
-
//#region src/rules/index.ts
|
|
12831
|
+
// src/rules/index.ts
|
|
12928
12832
|
const RULES = [
|
|
12929
12833
|
focusedTestCommitted,
|
|
12930
12834
|
skippedTest,
|
|
@@ -13046,8 +12950,7 @@ const RETIRED_RULE_IDS = [
|
|
|
13046
12950
|
"QA-JV-111",
|
|
13047
12951
|
"QA-CS-111"
|
|
13048
12952
|
];
|
|
13049
|
-
|
|
13050
|
-
//#region src/lib/wilson.ts
|
|
12953
|
+
// src/lib/wilson.ts
|
|
13051
12954
|
/**
|
|
13052
12955
|
* 95% Wilson score interval on a proportion (plan §20.2).
|
|
13053
12956
|
*
|
|
@@ -13170,8 +13073,7 @@ function isProvisional(rule) {
|
|
|
13170
13073
|
function isRetiredRule(ruleId) {
|
|
13171
13074
|
return RETIRED_RULE_IDS.includes(ruleId);
|
|
13172
13075
|
}
|
|
13173
|
-
|
|
13174
|
-
//#region src/scorer/scorer.ts
|
|
13076
|
+
// src/scorer/scorer.ts
|
|
13175
13077
|
/**
|
|
13176
13078
|
* Transparent scorer (Product-MVP §8).
|
|
13177
13079
|
* score = 100 − Σ(severity deductions), floor 0.
|
|
@@ -13323,9 +13225,9 @@ function countTestDeclarations(text, codeText) {
|
|
|
13323
13225
|
const source = codeText ?? text;
|
|
13324
13226
|
const patterns = [
|
|
13325
13227
|
/\b(?:it|test)(?:\.\w+)*\s*(?:\(|`)/g,
|
|
13326
|
-
|
|
13327
|
-
|
|
13328
|
-
|
|
13228
|
+
/^[ \t]*(?:async[ \t]+)?def[ \t]+test_\w*/gm,
|
|
13229
|
+
/^[ \t]*@Test\b/gm,
|
|
13230
|
+
/^[ \t]*\[(?:Test|Fact|Theory|TestMethod)\b/gm
|
|
13329
13231
|
];
|
|
13330
13232
|
let count = 0;
|
|
13331
13233
|
for (const re of patterns) {
|
|
@@ -13334,8 +13236,7 @@ function countTestDeclarations(text, codeText) {
|
|
|
13334
13236
|
}
|
|
13335
13237
|
return count;
|
|
13336
13238
|
}
|
|
13337
|
-
|
|
13338
|
-
//#region src/engine/rule-runner.ts
|
|
13239
|
+
// src/engine/rule-runner.ts
|
|
13339
13240
|
/**
|
|
13340
13241
|
* Legacy appliesTo → adapter id mapping.
|
|
13341
13242
|
*
|
|
@@ -13374,8 +13275,7 @@ function asUniversal(rule) {
|
|
|
13374
13275
|
}
|
|
13375
13276
|
};
|
|
13376
13277
|
}
|
|
13377
|
-
|
|
13378
|
-
//#region src/engine/tier-policy.ts
|
|
13278
|
+
// src/engine/tier-policy.ts
|
|
13379
13279
|
/**
|
|
13380
13280
|
* Tier policy (audit H-1): the declared tier is authoritative over a
|
|
13381
13281
|
* rule's severity and evidence claims.
|
|
@@ -13515,8 +13415,7 @@ function enforceTierPolicy(findings, tierByRuleId) {
|
|
|
13515
13415
|
if (EVIDENCE_RANK$1[effective] < EVIDENCE_RANK$1[floor.evidenceLevel]) f.evidenceLevel = floor.evidenceLevel;
|
|
13516
13416
|
}
|
|
13517
13417
|
}
|
|
13518
|
-
|
|
13519
|
-
//#region src/engine/evidence-core.ts
|
|
13418
|
+
// src/engine/evidence-core.ts
|
|
13520
13419
|
/**
|
|
13521
13420
|
* The core's own version — stamped into every record's provenance so a
|
|
13522
13421
|
* consumer can tell which normalization contract produced a record.
|
|
@@ -13583,8 +13482,7 @@ function countEvidence(records) {
|
|
|
13583
13482
|
}
|
|
13584
13483
|
return counts;
|
|
13585
13484
|
}
|
|
13586
|
-
|
|
13587
|
-
//#region src/engine/provenance.ts
|
|
13485
|
+
// src/engine/provenance.ts
|
|
13588
13486
|
const GENERATED_HEADER_RE = /^\s*(?:\/\/|#|\/\*)\s*(?:auto[- ]?generated|generated by|do not edit)/i;
|
|
13589
13487
|
/**
|
|
13590
13488
|
* Codegen fingerprint: the recorder names its first test exactly
|
|
@@ -13625,8 +13523,7 @@ function computeAgenticProfile(files, findings) {
|
|
|
13625
13523
|
note: "Static provenance markers only (generated-file headers, Playwright codegen default titles). Absence of a marker does not prove human authorship; healed and MCP-edited files without markers are indistinguishable from hand-written ones. Provenance never changes scoring or evidence — authorship is metadata, not trust."
|
|
13626
13524
|
};
|
|
13627
13525
|
}
|
|
13628
|
-
|
|
13629
|
-
//#region src/engine/overlap-dedup.ts
|
|
13526
|
+
// src/engine/overlap-dedup.ts
|
|
13630
13527
|
const TIER_RANK = {
|
|
13631
13528
|
core: 0,
|
|
13632
13529
|
extended: 1,
|
|
@@ -13721,8 +13618,7 @@ function dropOrderBefore(a, b, metaByRuleId) {
|
|
|
13721
13618
|
if (ranksBefore(b, a, metaByRuleId)) return true;
|
|
13722
13619
|
return (a.column ?? Number.MAX_SAFE_INTEGER) < (b.column ?? Number.MAX_SAFE_INTEGER);
|
|
13723
13620
|
}
|
|
13724
|
-
|
|
13725
|
-
//#region src/engine/finding-identity.ts
|
|
13621
|
+
// src/engine/finding-identity.ts
|
|
13726
13622
|
/**
|
|
13727
13623
|
* Finding Identity (ENGINE-002): deterministic, stable fingerprints for
|
|
13728
13624
|
* deduplication, correlation, and lifecycle tracking across scans.
|
|
@@ -13761,8 +13657,7 @@ function codeQualityFingerprint(f) {
|
|
|
13761
13657
|
function findingId(f) {
|
|
13762
13658
|
return `${findingFingerprint(f)}\u0000${f.line}\u0000${f.column}`;
|
|
13763
13659
|
}
|
|
13764
|
-
|
|
13765
|
-
//#region src/engine/correlation-engine.ts
|
|
13660
|
+
// src/engine/correlation-engine.ts
|
|
13766
13661
|
const EVIDENCE_RANK = {
|
|
13767
13662
|
E0: 0,
|
|
13768
13663
|
E1: 1,
|
|
@@ -13892,8 +13787,7 @@ function correlateFindings(findings) {
|
|
|
13892
13787
|
});
|
|
13893
13788
|
return conclusions;
|
|
13894
13789
|
}
|
|
13895
|
-
|
|
13896
|
-
//#region src/engine/dependency-graph.ts
|
|
13790
|
+
// src/engine/dependency-graph.ts
|
|
13897
13791
|
/**
|
|
13898
13792
|
* Dependency Graph (ECO-005).
|
|
13899
13793
|
*
|
|
@@ -14066,8 +13960,7 @@ function getReachableFiles(fromFiles, graph) {
|
|
|
14066
13960
|
resolvedAny: unresolvedStarts.length < fromFiles.length
|
|
14067
13961
|
};
|
|
14068
13962
|
}
|
|
14069
|
-
|
|
14070
|
-
//#region src/engine/incremental-analysis.ts
|
|
13963
|
+
// src/engine/incremental-analysis.ts
|
|
14071
13964
|
/**
|
|
14072
13965
|
* Semantic inputs that affect the full scan beyond individual file content.
|
|
14073
13966
|
* If any of these changed, an incremental run is NOT safe — a full scan
|
|
@@ -14103,8 +13996,7 @@ function isIncrementalSafe(changedFiles) {
|
|
|
14103
13996
|
reasons
|
|
14104
13997
|
};
|
|
14105
13998
|
}
|
|
14106
|
-
|
|
14107
|
-
//#region src/brand/tokens.ts
|
|
13999
|
+
// src/brand/tokens.ts
|
|
14108
14000
|
/**
|
|
14109
14001
|
* The single source of brand truth.
|
|
14110
14002
|
*
|
|
@@ -14339,8 +14231,7 @@ RUNG_MEANINGS.map((meaning, i) => ({
|
|
|
14339
14231
|
runtime: i >= 3,
|
|
14340
14232
|
color: RUNG_COLORS[i]
|
|
14341
14233
|
}));
|
|
14342
|
-
|
|
14343
|
-
//#region src/reporter/presentation.ts
|
|
14234
|
+
// src/reporter/presentation.ts
|
|
14344
14235
|
/**
|
|
14345
14236
|
* presentation.ts — the one place a presentation DECISION is made.
|
|
14346
14237
|
*
|
|
@@ -14562,8 +14453,7 @@ function evidenceTag(f) {
|
|
|
14562
14453
|
if (runtime !== null) tag += ` · runtime: ${runtime}`;
|
|
14563
14454
|
return `[${tag}]`;
|
|
14564
14455
|
}
|
|
14565
|
-
|
|
14566
|
-
//#region src/engine/monorepo-analysis.ts
|
|
14456
|
+
// src/engine/monorepo-analysis.ts
|
|
14567
14457
|
/**
|
|
14568
14458
|
* Package verdict, derived from the one ScoreState band model (BW-104).
|
|
14569
14459
|
* These two constants used to be a private copy of the band boundaries;
|
|
@@ -14670,8 +14560,7 @@ function configurableAggregation(results, config) {
|
|
|
14670
14560
|
strategy: config.weightingStrategy
|
|
14671
14561
|
};
|
|
14672
14562
|
}
|
|
14673
|
-
|
|
14674
|
-
//#region src/lib/fs-atomic.ts
|
|
14563
|
+
// src/lib/fs-atomic.ts
|
|
14675
14564
|
/**
|
|
14676
14565
|
* Atomic file writes (audit S9).
|
|
14677
14566
|
*
|
|
@@ -14791,8 +14680,7 @@ function pidAlive(pid) {
|
|
|
14791
14680
|
return err?.code !== "ESRCH";
|
|
14792
14681
|
}
|
|
14793
14682
|
}
|
|
14794
|
-
|
|
14795
|
-
//#region src/engine/scan-cache.ts
|
|
14683
|
+
// src/engine/scan-cache.ts
|
|
14796
14684
|
/**
|
|
14797
14685
|
* Local incremental scan cache (Beta-to-Stable 1.0 plan, M5.2 / A-2).
|
|
14798
14686
|
*
|
|
@@ -14892,7 +14780,9 @@ function hashDir(dir, hash, depth) {
|
|
|
14892
14780
|
else if (entry.isFile() && /\.tsx?$/.test(entry.name)) try {
|
|
14893
14781
|
hash.update(entry.name);
|
|
14894
14782
|
hash.update(readFileSync(full));
|
|
14895
|
-
} catch {
|
|
14783
|
+
} catch {
|
|
14784
|
+
recordDegradation("rules-tree-file-unreadable");
|
|
14785
|
+
}
|
|
14896
14786
|
}
|
|
14897
14787
|
}
|
|
14898
14788
|
/**
|
|
@@ -15002,8 +14892,7 @@ function createScanCache(root) {
|
|
|
15002
14892
|
}
|
|
15003
14893
|
};
|
|
15004
14894
|
}
|
|
15005
|
-
|
|
15006
|
-
//#region src/reporter/theme.ts
|
|
14895
|
+
// src/reporter/theme.ts
|
|
15007
14896
|
/**
|
|
15008
14897
|
* Norse-forge theme system. Pure string-in → string-out helpers.
|
|
15009
14898
|
* Respects NO_COLOR and non-TTY via `palette(isTTY)` — every renderer
|
|
@@ -15212,8 +15101,7 @@ function gaugeColorForBand(band, p) {
|
|
|
15212
15101
|
if (band === "unmeasured") return p.dim;
|
|
15213
15102
|
return band === "warning" ? p.warning : p.error;
|
|
15214
15103
|
}
|
|
15215
|
-
|
|
15216
|
-
//#region src/reporter/ui.ts
|
|
15104
|
+
// src/reporter/ui.ts
|
|
15217
15105
|
/**
|
|
15218
15106
|
* Neutral context for direct calls (tests, library use) and as the
|
|
15219
15107
|
* safe default when a command runner is invoked without wiring: no
|
|
@@ -15302,8 +15190,7 @@ function formatDuration(ms) {
|
|
|
15302
15190
|
if (ms === void 0) return "?";
|
|
15303
15191
|
return ms >= 1e3 ? `${(ms / 1e3).toFixed(1)}s` : `${ms}ms`;
|
|
15304
15192
|
}
|
|
15305
|
-
|
|
15306
|
-
//#region src/forensics/classify.ts
|
|
15193
|
+
// src/forensics/classify.ts
|
|
15307
15194
|
/**
|
|
15308
15195
|
* Strong-signal marker tables. Kept deliberately narrow: a marker here
|
|
15309
15196
|
* is a named family fact, not a vibe. Unknown text yields no signal —
|
|
@@ -15423,8 +15310,7 @@ function classifyForensicVerdict(signals) {
|
|
|
15423
15310
|
signals: signalCounts
|
|
15424
15311
|
};
|
|
15425
15312
|
}
|
|
15426
|
-
|
|
15427
|
-
//#region src/forensics/analyze.ts
|
|
15313
|
+
// src/forensics/analyze.ts
|
|
15428
15314
|
const ui$11 = plainContext();
|
|
15429
15315
|
const MAX_RECORDS = 1e5;
|
|
15430
15316
|
function analyze(records, source) {
|
|
@@ -15587,8 +15473,7 @@ function renderFlakyMd(report) {
|
|
|
15587
15473
|
lines.push("");
|
|
15588
15474
|
return lines.join("\n");
|
|
15589
15475
|
}
|
|
15590
|
-
|
|
15591
|
-
//#region src/forensics/evidence-hygiene.ts
|
|
15476
|
+
// src/forensics/evidence-hygiene.ts
|
|
15592
15477
|
/**
|
|
15593
15478
|
* Evidence hygiene — secret redaction + control-char sanitization
|
|
15594
15479
|
* (ENGINE-012).
|
|
@@ -15650,8 +15535,7 @@ function sanitizeErrorText(text, options) {
|
|
|
15650
15535
|
if (result.length > maxLength) result = result.slice(0, maxLength) + `[truncated at ${maxLength} chars]`;
|
|
15651
15536
|
return result;
|
|
15652
15537
|
}
|
|
15653
|
-
|
|
15654
|
-
//#region src/forensics/trace.ts
|
|
15538
|
+
// src/forensics/trace.ts
|
|
15655
15539
|
/**
|
|
15656
15540
|
* Playwright trace ingester (product-gap master plan §9 R5 / WI-17).
|
|
15657
15541
|
*
|
|
@@ -15844,8 +15728,7 @@ function parseTraceArtifact(bytes, artifactName) {
|
|
|
15844
15728
|
const { actions } = parseTraceNdjson(text);
|
|
15845
15729
|
return traceActionsToRecord(actions, artifactName);
|
|
15846
15730
|
}
|
|
15847
|
-
|
|
15848
|
-
//#region src/forensics/parse-jest-json.ts
|
|
15731
|
+
// src/forensics/parse-jest-json.ts
|
|
15849
15732
|
function toStatus$2(raw) {
|
|
15850
15733
|
switch (raw) {
|
|
15851
15734
|
case "passed": return "passed";
|
|
@@ -15905,8 +15788,7 @@ function parseJestJson(json) {
|
|
|
15905
15788
|
}
|
|
15906
15789
|
return out;
|
|
15907
15790
|
}
|
|
15908
|
-
|
|
15909
|
-
//#region src/forensics/parse-junit.ts
|
|
15791
|
+
// src/forensics/parse-junit.ts
|
|
15910
15792
|
const MAX_INPUT$1 = 20971520;
|
|
15911
15793
|
function decodeEntities$1(s) {
|
|
15912
15794
|
return s.replaceAll("<", "<").replaceAll(">", ">").replaceAll(""", "\"").replaceAll("'", "'").replaceAll("'", "'").replaceAll("&", "&");
|
|
@@ -15948,14 +15830,25 @@ function parseJunitXml(xml) {
|
|
|
15948
15830
|
const skipped = /<skipped\b/i.test(inner);
|
|
15949
15831
|
const errors = [];
|
|
15950
15832
|
if (failed) {
|
|
15951
|
-
const
|
|
15952
|
-
|
|
15953
|
-
|
|
15954
|
-
|
|
15833
|
+
const openRe = /<(?:failure|error)\b/gi;
|
|
15834
|
+
const endRe = /<\/(?:failure|error)\s*>/gi;
|
|
15835
|
+
let open;
|
|
15836
|
+
while ((open = openRe.exec(inner)) !== null) {
|
|
15837
|
+
const gt = inner.indexOf(">", open.index);
|
|
15838
|
+
if (gt === -1) break;
|
|
15839
|
+
const attrsStr = inner.slice(open.index, gt);
|
|
15840
|
+
if (attrsStr.endsWith("/")) {
|
|
15841
|
+
openRe.lastIndex = gt + 1;
|
|
15842
|
+
continue;
|
|
15843
|
+
}
|
|
15844
|
+
endRe.lastIndex = gt + 1;
|
|
15845
|
+
const end = endRe.exec(inner);
|
|
15846
|
+
if (end === null) break;
|
|
15955
15847
|
const msgAttr = /\smessage\s*=\s*"([^"]*)"/i.exec(attrsStr)?.[1];
|
|
15956
15848
|
if (msgAttr) errors.push(sanitizeErrorText(decodeEntities$1(msgAttr)));
|
|
15957
|
-
const body =
|
|
15849
|
+
const body = inner.slice(gt + 1, end.index).trim();
|
|
15958
15850
|
if (body && body !== msgAttr) errors.push(sanitizeErrorText(decodeEntities$1(body)));
|
|
15851
|
+
openRe.lastIndex = end.index + end[0].length;
|
|
15959
15852
|
}
|
|
15960
15853
|
const selfCloseRe = /<(?:failure|error)\b([^>]*)\/>/gi;
|
|
15961
15854
|
let scMatch;
|
|
@@ -15979,8 +15872,7 @@ function parseJunitXml(xml) {
|
|
|
15979
15872
|
}
|
|
15980
15873
|
return out;
|
|
15981
15874
|
}
|
|
15982
|
-
|
|
15983
|
-
//#region src/forensics/parse-trx.ts
|
|
15875
|
+
// src/forensics/parse-trx.ts
|
|
15984
15876
|
const MAX_INPUT = 20971520;
|
|
15985
15877
|
function decodeEntities(s) {
|
|
15986
15878
|
return s.replaceAll("<", "<").replaceAll(">", ">").replaceAll(""", "\"").replaceAll("'", "'").replaceAll("'", "'").replaceAll("&", "&");
|
|
@@ -16091,11 +15983,24 @@ function parseTrxXml(xml) {
|
|
|
16091
15983
|
if (id !== void 0 && name !== void 0) byTestId.set(id, name);
|
|
16092
15984
|
}
|
|
16093
15985
|
const records = [];
|
|
16094
|
-
const
|
|
16095
|
-
|
|
16096
|
-
|
|
16097
|
-
|
|
16098
|
-
|
|
15986
|
+
const startRe = /<UnitTestResult\b/gi;
|
|
15987
|
+
const starts = [];
|
|
15988
|
+
let s;
|
|
15989
|
+
while ((s = startRe.exec(xml)) !== null) {
|
|
15990
|
+
starts.push(s.index);
|
|
15991
|
+
if (starts.length > 2e5) break;
|
|
15992
|
+
}
|
|
15993
|
+
const resultsEnd = xml.search(/<\/Results>/i);
|
|
15994
|
+
for (let i = 0; i < starts.length; i++) {
|
|
15995
|
+
const start = starts[i];
|
|
15996
|
+
const tagEnd = xml.indexOf(">", start);
|
|
15997
|
+
if (tagEnd === -1) break;
|
|
15998
|
+
const selfClosing = xml[tagEnd - 1] === "/";
|
|
15999
|
+
const attrs = xml.slice(start + 15, selfClosing ? tagEnd - 1 : tagEnd);
|
|
16000
|
+
const next = starts[i + 1] ?? -1;
|
|
16001
|
+
let bodyEnd = next === -1 ? xml.length : next;
|
|
16002
|
+
if (resultsEnd > tagEnd && resultsEnd < bodyEnd) bodyEnd = resultsEnd;
|
|
16003
|
+
const inner = selfClosing ? "" : xml.slice(tagEnd + 1, bodyEnd);
|
|
16099
16004
|
const testName = attr(attrs, "testName") ?? attr(attrs, "testId");
|
|
16100
16005
|
if (testName === void 0) continue;
|
|
16101
16006
|
const attempt = {
|
|
@@ -16103,7 +16008,8 @@ function parseTrxXml(xml) {
|
|
|
16103
16008
|
status: mapTrxOutcome(attr(attrs, "outcome")),
|
|
16104
16009
|
durationMs: parseDuration(attr(attrs, "duration"))
|
|
16105
16010
|
};
|
|
16106
|
-
const
|
|
16011
|
+
const messageAt = inner.search(/<Message\b/i);
|
|
16012
|
+
const error = messageAt === -1 ? void 0 : messageOf(inner, messageAt);
|
|
16107
16013
|
records.push({
|
|
16108
16014
|
file: "unknown",
|
|
16109
16015
|
title: testName,
|
|
@@ -16115,8 +16021,7 @@ function parseTrxXml(xml) {
|
|
|
16115
16021
|
}
|
|
16116
16022
|
return records;
|
|
16117
16023
|
}
|
|
16118
|
-
|
|
16119
|
-
//#region src/forensics/parse-playwright-json.ts
|
|
16024
|
+
// src/forensics/parse-playwright-json.ts
|
|
16120
16025
|
const MAX_DEPTH = 32;
|
|
16121
16026
|
function toStatus$1(raw) {
|
|
16122
16027
|
switch (raw) {
|
|
@@ -16181,8 +16086,7 @@ function parsePlaywrightJson(json) {
|
|
|
16181
16086
|
walk(root, 0);
|
|
16182
16087
|
return out;
|
|
16183
16088
|
}
|
|
16184
|
-
|
|
16185
|
-
//#region src/forensics/parse-vitest-json.ts
|
|
16089
|
+
// src/forensics/parse-vitest-json.ts
|
|
16186
16090
|
function toStatus(raw) {
|
|
16187
16091
|
switch (raw) {
|
|
16188
16092
|
case "passed": return "passed";
|
|
@@ -16236,8 +16140,7 @@ function parseVitestJson(json) {
|
|
|
16236
16140
|
}
|
|
16237
16141
|
return out;
|
|
16238
16142
|
}
|
|
16239
|
-
|
|
16240
|
-
//#region src/forensics/parse-har.ts
|
|
16143
|
+
// src/forensics/parse-har.ts
|
|
16241
16144
|
const MAX_ENTRIES = 2e4;
|
|
16242
16145
|
function sanitizeTitleUrl(url) {
|
|
16243
16146
|
try {
|
|
@@ -16322,8 +16225,7 @@ function parseHarJsonDetailed(json) {
|
|
|
16322
16225
|
truncated
|
|
16323
16226
|
};
|
|
16324
16227
|
}
|
|
16325
|
-
|
|
16326
|
-
//#region src/forensics/run.ts
|
|
16228
|
+
// src/forensics/run.ts
|
|
16327
16229
|
/**
|
|
16328
16230
|
* Runtime evidence entry point (R4) — the library behind `explain
|
|
16329
16231
|
* --evidence`. The `forensics` verb is retired in the v6 carve; this module
|
|
@@ -16488,13 +16390,15 @@ function runForensics(target, options = {}) {
|
|
|
16488
16390
|
report.incompleteReasons = [.../* @__PURE__ */ new Set([...report.incompleteReasons, ...incompleteReasons])];
|
|
16489
16391
|
}
|
|
16490
16392
|
let flakyMdPath;
|
|
16393
|
+
let flakyMdWriteError;
|
|
16491
16394
|
if ((options.writeFlakyMd ?? true) && report.totalTests > 0) {
|
|
16492
16395
|
const base = stat.isFile() ? dirname(target) : target;
|
|
16493
16396
|
flakyMdPath = join(base, "FLAKY.md");
|
|
16494
16397
|
try {
|
|
16495
16398
|
writeFileAtomic(flakyMdPath, renderFlakyMd(report));
|
|
16496
|
-
} catch {
|
|
16399
|
+
} catch (e) {
|
|
16497
16400
|
flakyMdPath = void 0;
|
|
16401
|
+
flakyMdWriteError = e instanceof Error ? e.message : String(e);
|
|
16498
16402
|
}
|
|
16499
16403
|
}
|
|
16500
16404
|
return {
|
|
@@ -16502,7 +16406,7 @@ function runForensics(target, options = {}) {
|
|
|
16502
16406
|
output: [
|
|
16503
16407
|
renderLeaderboard(report),
|
|
16504
16408
|
"",
|
|
16505
|
-
flakyMdPath !== void 0 ? renderFlakyMdHint() : report.totalTests === 0 && (report.totalNetworkObservations ?? 0) > 0 && options.writeFlakyMd !== false ? "FLAKY.md was not written — network observations do not establish test outcomes." : renderFlakyMdNotWritten(options)
|
|
16409
|
+
flakyMdPath !== void 0 ? renderFlakyMdHint() : report.totalTests === 0 && (report.totalNetworkObservations ?? 0) > 0 && options.writeFlakyMd !== false ? "FLAKY.md was not written — network observations do not establish test outcomes." : renderFlakyMdNotWritten(options, flakyMdWriteError)
|
|
16506
16410
|
].join("\n"),
|
|
16507
16411
|
flakyMdPath
|
|
16508
16412
|
};
|
|
@@ -16511,9 +16415,10 @@ function renderFlakyMdHint() {
|
|
|
16511
16415
|
return "Full details in FLAKY.md (committed artifact).";
|
|
16512
16416
|
}
|
|
16513
16417
|
/** Honest fallback for every case where FLAKY.md was NOT written (M2). */
|
|
16514
|
-
function renderFlakyMdNotWritten(options) {
|
|
16418
|
+
function renderFlakyMdNotWritten(options, writeError) {
|
|
16515
16419
|
if (options.writeFlakyMd === false) return "FLAKY.md not written (--no-flaky-md).";
|
|
16516
|
-
return
|
|
16420
|
+
if (writeError !== void 0) return `FLAKY.md could not be written: ${writeError}`;
|
|
16421
|
+
return "FLAKY.md was not written — nothing recognized to report.";
|
|
16517
16422
|
}
|
|
16518
16423
|
/**
|
|
16519
16424
|
* R5: the trace artifact's test identity. The per-test trace convention
|
|
@@ -16600,8 +16505,7 @@ function listFiles(dir) {
|
|
|
16600
16505
|
walk(dir, 0);
|
|
16601
16506
|
return out.map((p) => p);
|
|
16602
16507
|
}
|
|
16603
|
-
|
|
16604
|
-
//#region src/config/suppressions.ts
|
|
16508
|
+
// src/config/suppressions.ts
|
|
16605
16509
|
/**
|
|
16606
16510
|
* Suppression governance (Sprint-Plan W7, Product-MVP §12).
|
|
16607
16511
|
* `mjolnir scan --suppressions` — every suppression stays visible.
|
|
@@ -16638,8 +16542,7 @@ function renderSuppressions(report) {
|
|
|
16638
16542
|
lines.push("");
|
|
16639
16543
|
return lines.join("\n");
|
|
16640
16544
|
}
|
|
16641
|
-
|
|
16642
|
-
//#region src/plugins/reserved-prefixes.ts
|
|
16545
|
+
// src/plugins/reserved-prefixes.ts
|
|
16643
16546
|
/**
|
|
16644
16547
|
* Core-owned rule-ID prefixes that external rule sources (npm plugins,
|
|
16645
16548
|
* JS modules, JSON manifests) may never claim — one shared law.
|
|
@@ -16669,8 +16572,7 @@ function isReservedPrefix(ruleId) {
|
|
|
16669
16572
|
const upper = ruleId.toUpperCase();
|
|
16670
16573
|
return RESERVED_PREFIXES.some((p) => upper.startsWith(p));
|
|
16671
16574
|
}
|
|
16672
|
-
|
|
16673
|
-
//#region src/plugins/load.ts
|
|
16575
|
+
// src/plugins/load.ts
|
|
16674
16576
|
/**
|
|
16675
16577
|
* Plugin API (Upgrade-Plan-v3 Phase 6, tier-2 item #1).
|
|
16676
16578
|
*
|
|
@@ -16776,8 +16678,7 @@ function loadPlugins(root, gateOpen = false) {
|
|
|
16776
16678
|
}
|
|
16777
16679
|
return result;
|
|
16778
16680
|
}
|
|
16779
|
-
|
|
16780
|
-
//#region src/plugins/local-rules.ts
|
|
16681
|
+
// src/plugins/local-rules.ts
|
|
16781
16682
|
/**
|
|
16782
16683
|
* Local plugin/rule contract (Verification Trust Evolution Plan §18 —
|
|
16783
16684
|
* Local Extensibility).
|
|
@@ -17089,8 +16990,7 @@ async function loadModuleRules(path, result) {
|
|
|
17089
16990
|
result.rules.push(rule);
|
|
17090
16991
|
}
|
|
17091
16992
|
}
|
|
17092
|
-
|
|
17093
|
-
//#region src/plugins/trust-gate.ts
|
|
16993
|
+
// src/plugins/trust-gate.ts
|
|
17094
16994
|
/**
|
|
17095
16995
|
* Plugin trust gate (audit-remediation-master-plan.md, locked decision C2).
|
|
17096
16996
|
*
|
|
@@ -17126,8 +17026,7 @@ function renderGateNotice(skipped) {
|
|
|
17126
17026
|
lines.push("To run them, re-run with --enable-plugins (or set MJOLNIR_ENABLE_PLUGINS=1).", "Declarative JSON rule manifests (mjolnir-rules/*.json) are unaffected — they execute no code.");
|
|
17127
17027
|
return lines.join("\n");
|
|
17128
17028
|
}
|
|
17129
|
-
|
|
17130
|
-
//#region src/engine/scan-pipeline.ts
|
|
17029
|
+
// src/engine/scan-pipeline.ts
|
|
17131
17030
|
/**
|
|
17132
17031
|
* Canonical scan pipeline (M6 — blueprint §9.2).
|
|
17133
17032
|
*
|
|
@@ -17143,8 +17042,10 @@ function renderGateNotice(skipped) {
|
|
|
17143
17042
|
* module moved. Re-exports in cli.ts keep the historical import surface.
|
|
17144
17043
|
*/
|
|
17145
17044
|
var scan_pipeline_exports = /* @__PURE__ */ __exportAll({
|
|
17045
|
+
DEFAULT_MAX_DURATION_MS: () => DEFAULT_MAX_DURATION_MS$1,
|
|
17146
17046
|
EVIDENCE_OVERRIDES: () => EVIDENCE_OVERRIDES$1,
|
|
17147
17047
|
KNOWN_RULE_IDS: () => KNOWN_RULE_IDS$1,
|
|
17048
|
+
MAX_DURATION_MS: () => MAX_DURATION_MS$1,
|
|
17148
17049
|
OVERLAP_META_BY_RULE_ID: () => OVERLAP_META_BY_RULE_ID$1,
|
|
17149
17050
|
SUITE_INVALIDATING_RULE_IDS: () => SUITE_INVALIDATING_RULE_IDS$1,
|
|
17150
17051
|
applyPostScanProcessing: () => applyPostScanProcessing,
|
|
@@ -17161,8 +17062,13 @@ var scan_pipeline_exports = /* @__PURE__ */ __exportAll({
|
|
|
17161
17062
|
summarizeForensicVerdicts: () => summarizeForensicVerdicts
|
|
17162
17063
|
});
|
|
17163
17064
|
const UNIVERSAL_RULES = RULES.map(asUniversal);
|
|
17164
|
-
|
|
17165
|
-
|
|
17065
|
+
/**
|
|
17066
|
+
* The scan time budget: the default, and the ceiling every surface enforces.
|
|
17067
|
+
* Owned here, where the clamp is applied; `cli.ts` and the MCP server import
|
|
17068
|
+
* these rather than keeping copies that only agree by coincidence.
|
|
17069
|
+
*/
|
|
17070
|
+
const DEFAULT_MAX_DURATION_MS$1 = 6e5;
|
|
17071
|
+
const MAX_DURATION_MS$1 = 36e5;
|
|
17166
17072
|
/** Registered rule IDs — used to warn on unknown severityOverrides keys (M4). */
|
|
17167
17073
|
const KNOWN_RULE_IDS$1 = new Set(RULES.map((r) => r.id));
|
|
17168
17074
|
/**
|
|
@@ -17205,9 +17111,10 @@ async function buildUniversalRules$1(root, strict, opts = {}) {
|
|
|
17205
17111
|
for (const p of plugins) for (const r of p.rules) if (r.tier) tierByRuleId.set(r.id, r.tier);
|
|
17206
17112
|
for (const r of local.rules) if (r.tier) tierByRuleId.set(r.id, r.tier);
|
|
17207
17113
|
let rules = [...UNIVERSAL_RULES, ...externalRules];
|
|
17208
|
-
const
|
|
17114
|
+
const quarantined = rules.filter((r) => tierByRuleId.get(r.id) === "quarantine").map((r) => r.id).sort();
|
|
17115
|
+
const withheldRuleIds = strict ? [] : quarantined;
|
|
17209
17116
|
if (!strict) rules = rules.filter((r) => tierByRuleId.get(r.id) !== "quarantine");
|
|
17210
|
-
const rulesWithheld =
|
|
17117
|
+
const rulesWithheld = withheldRuleIds.length;
|
|
17211
17118
|
const rulesApplied = rules.length;
|
|
17212
17119
|
const pluginMeta = [...plugins.map((p) => ({
|
|
17213
17120
|
name: p.name,
|
|
@@ -17604,12 +17511,28 @@ function applyPostScanProcessing(findings, workspace, args, hooks, scanRoot, dec
|
|
|
17604
17511
|
const runtimeIncomplete = discoveredReport !== void 0 && discoveredReport.report.analysisComplete !== true;
|
|
17605
17512
|
let forensicVerdicts;
|
|
17606
17513
|
let evidenceRecords = [];
|
|
17607
|
-
if (discoveredReport && discoveredReport.report.analysisComplete === true)
|
|
17608
|
-
|
|
17609
|
-
|
|
17610
|
-
|
|
17611
|
-
|
|
17612
|
-
|
|
17514
|
+
if (discoveredReport && discoveredReport.report.analysisComplete === true) {
|
|
17515
|
+
const before = findings.map((f) => ({
|
|
17516
|
+
runtimeCorroboration: f.runtimeCorroboration,
|
|
17517
|
+
trustLevel: f.trustLevel
|
|
17518
|
+
}));
|
|
17519
|
+
try {
|
|
17520
|
+
const records = buildEvidenceRecords(discoveredReport.report, discoveredReport.path);
|
|
17521
|
+
stampRuntimeCorroboration(findings, discoveredReport.report, workspace.root);
|
|
17522
|
+
forensicVerdicts = summarizeForensicVerdicts(discoveredReport.report);
|
|
17523
|
+
evidenceRecords = records;
|
|
17524
|
+
} catch {
|
|
17525
|
+
findings.forEach((f, i) => {
|
|
17526
|
+
const prev = before[i];
|
|
17527
|
+
if (prev?.runtimeCorroboration === void 0) delete f.runtimeCorroboration;
|
|
17528
|
+
else f.runtimeCorroboration = prev.runtimeCorroboration;
|
|
17529
|
+
if (prev?.trustLevel === void 0) delete f.trustLevel;
|
|
17530
|
+
else f.trustLevel = prev.trustLevel;
|
|
17531
|
+
});
|
|
17532
|
+
forensicVerdicts = void 0;
|
|
17533
|
+
evidenceRecords = [];
|
|
17534
|
+
recordDegradation("runtime-corroboration-failed");
|
|
17535
|
+
}
|
|
17613
17536
|
}
|
|
17614
17537
|
return {
|
|
17615
17538
|
evidenceRecords,
|
|
@@ -17900,8 +17823,8 @@ async function runScan$1(args, hooks = {}) {
|
|
|
17900
17823
|
async function runScanInProject(args, hooks = {}) {
|
|
17901
17824
|
const started = Date.now();
|
|
17902
17825
|
const degradationWindow = beginDegradationWindow();
|
|
17903
|
-
const requestedDuration = Number.isFinite(args.maxDurationMs) ? args.maxDurationMs : DEFAULT_MAX_DURATION_MS$
|
|
17904
|
-
const deadline = started + Math.min(Math.max(1, requestedDuration), MAX_DURATION_MS$
|
|
17826
|
+
const requestedDuration = Number.isFinite(args.maxDurationMs) ? args.maxDurationMs : DEFAULT_MAX_DURATION_MS$1;
|
|
17827
|
+
const deadline = started + Math.min(Math.max(1, requestedDuration), MAX_DURATION_MS$1);
|
|
17905
17828
|
const discovered = discoverWorkspace(args.target);
|
|
17906
17829
|
const targetAbs = resolve(args.target);
|
|
17907
17830
|
const scanRoot = discovered && discovered.root !== targetAbs && targetAbs.startsWith(discovered.root + sep) ? {
|
|
@@ -18042,8 +17965,7 @@ async function runScanInProject(args, hooks = {}) {
|
|
|
18042
17965
|
resetTsMorphProject();
|
|
18043
17966
|
return result;
|
|
18044
17967
|
}
|
|
18045
|
-
|
|
18046
|
-
//#region src/commands/help.ts
|
|
17968
|
+
// src/commands/help.ts
|
|
18047
17969
|
/** Ordered registry — grouped by the same categories the overview uses. */
|
|
18048
17970
|
const HELP_ENTRIES = [
|
|
18049
17971
|
{
|
|
@@ -18482,8 +18404,7 @@ function renderRootHelp(schemaVersion = 1, options = {}) {
|
|
|
18482
18404
|
lines.push(`Docs: ${DOCS_URL} (JSON schemaVersion ${schemaVersion}, additive-only)`);
|
|
18483
18405
|
return lines.join("\n");
|
|
18484
18406
|
}
|
|
18485
|
-
|
|
18486
|
-
//#region src/integrations/sentry.ts
|
|
18407
|
+
// src/integrations/sentry.ts
|
|
18487
18408
|
/**
|
|
18488
18409
|
* Sentry crash reporting — opt-in, and inert unless a DSN is configured.
|
|
18489
18410
|
*
|
|
@@ -18609,8 +18530,7 @@ async function flushSentry(timeoutMs = FLUSH_TIMEOUT_MS) {
|
|
|
18609
18530
|
return false;
|
|
18610
18531
|
}
|
|
18611
18532
|
}
|
|
18612
|
-
|
|
18613
|
-
//#region src/integrations/ci-install.ts
|
|
18533
|
+
// src/integrations/ci-install.ts
|
|
18614
18534
|
/**
|
|
18615
18535
|
* CI integration (Sprint-Plan W7): generates .github/workflows/mjolnir.yml
|
|
18616
18536
|
* from internal templates ONLY — no user-input interpolation (R3 supply-chain).
|
|
@@ -18697,10 +18617,12 @@ function publishedVersionForInstall(startDir = import.meta.dirname) {
|
|
|
18697
18617
|
return ENGINE_VERSION;
|
|
18698
18618
|
}
|
|
18699
18619
|
/**
|
|
18700
|
-
* Template
|
|
18701
|
-
*
|
|
18702
|
-
*
|
|
18703
|
-
*
|
|
18620
|
+
* Template v3: the reporting steps render the saved report with
|
|
18621
|
+
* `mjolnir mjolnir.json --format github-summary` (annotations + step summary
|
|
18622
|
+
* via ONE emitter) and `--format pr-comment`. Template v2, generated through
|
|
18623
|
+
* 6.0.0, called `summary` and `pr-comment --from`, the verbs those formats
|
|
18624
|
+
* replaced, so its reporting steps exited 10 and, being `continue-on-error`,
|
|
18625
|
+
* did so silently: no annotations and no comment, on every run. The gate
|
|
18704
18626
|
* script is unchanged (reads `partial`, `findings[].severity`).
|
|
18705
18627
|
*/
|
|
18706
18628
|
/** Renders findings into `$GITHUB_STEP_SUMMARY`; tolerates a missing scan result.
|
|
@@ -18725,12 +18647,52 @@ const SUMMARY_SCRIPT_V1 = [
|
|
|
18725
18647
|
"}",
|
|
18726
18648
|
"process.stdout.write(lines.length + \" finding(s)\\n\");"
|
|
18727
18649
|
].join("\n");
|
|
18728
|
-
/** True when a workflow was generated by any Mjölnir template (v1 or
|
|
18650
|
+
/** True when a workflow was generated by any Mjölnir template (v1, v2 or v3).
|
|
18651
|
+
*
|
|
18652
|
+
* The comparison runs on the canonical form, so a workflow generated by an
|
|
18653
|
+
* earlier release still matches. It used to be byte equality with the current
|
|
18654
|
+
* template, and the pinned package version alone made every file from the
|
|
18655
|
+
* previous release read as hand-customised: an upgrade needed `--force`, the
|
|
18656
|
+
* one flag that also overwrites real customisations.
|
|
18657
|
+
*
|
|
18729
18658
|
* The v1 needle is matched in its INDENTED form: the v1 template embedded
|
|
18730
18659
|
* the script via indentBlock(…, 10) inside the `run: |` scalar, so the
|
|
18731
18660
|
* raw unindented substring never appears in a real v1 file. */
|
|
18732
18661
|
function isKnownTemplate(content) {
|
|
18733
|
-
|
|
18662
|
+
const file = canonicalWorkflow(content);
|
|
18663
|
+
return GATES.some((g) => {
|
|
18664
|
+
const npx = canonicalWorkflow(TEMPLATE(g));
|
|
18665
|
+
const action = canonicalWorkflow(ACTION_TEMPLATE(g));
|
|
18666
|
+
return file === npx || file === templateV2(npx) || file === action || file === actionTemplateWithoutVersion(action);
|
|
18667
|
+
}) || file.includes(indentBlock(SUMMARY_SCRIPT_V1, 10));
|
|
18668
|
+
}
|
|
18669
|
+
/**
|
|
18670
|
+
* A generated workflow with the parts that legitimately change between
|
|
18671
|
+
* releases blanked: line endings, the pinned package version (in the npx
|
|
18672
|
+
* tarball and in the Action's `version:` input) and every pinned action SHA
|
|
18673
|
+
* with its version comment. Dependabot moves the `actions/*` pins between
|
|
18674
|
+
* releases, so blanking only the Mjölnir Action's SHA made each of those bumps
|
|
18675
|
+
* turn every workflow the previous release generated into one that reads as
|
|
18676
|
+
* hand-customised.
|
|
18677
|
+
*/
|
|
18678
|
+
function canonicalWorkflow(text) {
|
|
18679
|
+
return text.replace(/\r\n/g, "\n").replace(/mjolnir-qa-[0-9A-Za-z.+-]+?\.tgz/g, "mjolnir-qa-<version>.tgz").replace(/(\n {10}version: )"[^"\n]*"/g, "$1\"<version>\"").replace(/(uses: [^\s@]+)@[0-9a-f]{40}[^\n]*/g, "$1@<sha>");
|
|
18680
|
+
}
|
|
18681
|
+
/**
|
|
18682
|
+
* The action template through 6.0.0, which passed no `version:` and so ran
|
|
18683
|
+
* whatever its pinned Action defaulted to.
|
|
18684
|
+
*/
|
|
18685
|
+
function actionTemplateWithoutVersion(canonical) {
|
|
18686
|
+
return canonical.replace("\n version: \"<version>\"", "");
|
|
18687
|
+
}
|
|
18688
|
+
/**
|
|
18689
|
+
* Template v2, the npx template through 6.0.0, rebuilt from v3: the same file
|
|
18690
|
+
* with its two reporting steps calling the verbs the formats replaced. A v2
|
|
18691
|
+
* workflow is recognised because it is broken, so `ci install` has to be able
|
|
18692
|
+
* to replace it without `--force`.
|
|
18693
|
+
*/
|
|
18694
|
+
function templateV2(v3) {
|
|
18695
|
+
return v3.replace(".tgz mjolnir.json --format github-summary\n", ".tgz summary mjolnir.json\n").replace(".tgz mjolnir.json --format pr-comment > mjolnir-comment.md\n", ".tgz pr-comment --from mjolnir.json > mjolnir-comment.md\n");
|
|
18734
18696
|
}
|
|
18735
18697
|
/** Indents an embedded script so it sits inside a YAML `run: |` block scalar. */
|
|
18736
18698
|
function indentBlock(text, spaces) {
|
|
@@ -18773,12 +18735,12 @@ jobs:
|
|
|
18773
18735
|
- name: Annotations + Job Summary
|
|
18774
18736
|
if: always()
|
|
18775
18737
|
continue-on-error: true
|
|
18776
|
-
run: npx --yes https://registry.npmjs.org/mjolnir-qa/-/mjolnir-qa-${INSTALL_VERSION}.tgz
|
|
18738
|
+
run: npx --yes https://registry.npmjs.org/mjolnir-qa/-/mjolnir-qa-${INSTALL_VERSION}.tgz mjolnir.json --format github-summary
|
|
18777
18739
|
- name: Render PR comment
|
|
18778
18740
|
if: always()
|
|
18779
18741
|
continue-on-error: true
|
|
18780
|
-
run: npx --yes https://registry.npmjs.org/mjolnir-qa/-/mjolnir-qa-${INSTALL_VERSION}.tgz pr-comment
|
|
18781
|
-
- uses: actions/upload-artifact@
|
|
18742
|
+
run: npx --yes https://registry.npmjs.org/mjolnir-qa/-/mjolnir-qa-${INSTALL_VERSION}.tgz mjolnir.json --format pr-comment > mjolnir-comment.md
|
|
18743
|
+
- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
|
18782
18744
|
if: always()
|
|
18783
18745
|
with:
|
|
18784
18746
|
name: mjolnir-report
|
|
@@ -18803,7 +18765,7 @@ ${indentBlock(gateScript(gate), 10)}
|
|
|
18803
18765
|
contents: read
|
|
18804
18766
|
pull-requests: write
|
|
18805
18767
|
steps:
|
|
18806
|
-
- uses: actions/download-artifact@
|
|
18768
|
+
- uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
|
|
18807
18769
|
with:
|
|
18808
18770
|
name: mjolnir-report
|
|
18809
18771
|
- uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
|
@@ -18846,12 +18808,22 @@ const GATES = [
|
|
|
18846
18808
|
"warning"
|
|
18847
18809
|
];
|
|
18848
18810
|
/**
|
|
18849
|
-
* The
|
|
18850
|
-
*
|
|
18851
|
-
*
|
|
18852
|
-
*
|
|
18811
|
+
* The Action the action-based template pins: a commit on `main`, by SHA,
|
|
18812
|
+
* because a generated workflow must never resolve a mutable ref.
|
|
18813
|
+
*
|
|
18814
|
+
* It is the merge commit of #740, the first commit on `main` whose
|
|
18815
|
+
* `action.yml` renders the saved report with `--format pr-comment` and
|
|
18816
|
+
* `--format github-summary`. The pin it replaced, 4a588bc, was a pull-request
|
|
18817
|
+
* branch commit that `main` never contained: its `action.yml` installed
|
|
18818
|
+
* `mjolnir-qa@latest` and called the retired `trust-report` verb, and every
|
|
18819
|
+
* workflow `ci install` generated ran it. A pin can only name a commit that
|
|
18820
|
+
* already exists, so it moves in the change AFTER the Action it points at.
|
|
18821
|
+
*
|
|
18822
|
+
* The template passes `version:` because the Action's own default is the
|
|
18823
|
+
* published stable AT THIS COMMIT, which may be an older CLI than the one that
|
|
18824
|
+
* generated the workflow.
|
|
18853
18825
|
*/
|
|
18854
|
-
const ACTION_REF = "Sergey-Bar/Mjolnir@
|
|
18826
|
+
const ACTION_REF = "Sergey-Bar/Mjolnir@6bf46a0e64e2f3ee16266d8456d1b16e5ae16214";
|
|
18855
18827
|
/**
|
|
18856
18828
|
* The action-based workflow for one gate level (P1.3): the root
|
|
18857
18829
|
* action.yml does checkout-independent scanning — setup-node, the scan
|
|
@@ -18874,8 +18846,11 @@ const ACTION_REF = "Sergey-Bar/Mjolnir@4a588bc62d517bc85fc44c0eae64c6587d3bf70b"
|
|
|
18874
18846
|
* finish is not a pass — see the exit-code contract in docs/VERSIONING.md.
|
|
18875
18847
|
* 3. `version:` was pinned to the engine's working version, which while the
|
|
18876
18848
|
* candidate is a release candidate is not on npm, so the generated
|
|
18877
|
-
* workflow could not install what it asked for.
|
|
18878
|
-
*
|
|
18849
|
+
* workflow could not install what it asked for. It was then omitted,
|
|
18850
|
+
* which handed the choice to the pinned Action's default, the stable that
|
|
18851
|
+
* was published when that commit was made. It now names the published
|
|
18852
|
+
* version the installing package resolves, the same pin the npx template
|
|
18853
|
+
* uses.
|
|
18879
18854
|
*/
|
|
18880
18855
|
const ACTION_TEMPLATE = (gate) => `name: Mjölnir
|
|
18881
18856
|
|
|
@@ -18905,13 +18880,14 @@ jobs:
|
|
|
18905
18880
|
continue-on-error: true
|
|
18906
18881
|
uses: ${ACTION_REF}
|
|
18907
18882
|
with:
|
|
18883
|
+
version: "${INSTALL_VERSION}"
|
|
18908
18884
|
scope: changed
|
|
18909
18885
|
format: json
|
|
18910
18886
|
fail-on: ${gate === "advisory" ? "none" : gate}
|
|
18911
18887
|
pr-comment: "false"
|
|
18912
18888
|
trust-artifact: "false"
|
|
18913
18889
|
annotations: "true"
|
|
18914
|
-
- uses: actions/upload-artifact@
|
|
18890
|
+
- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
|
18915
18891
|
if: always()
|
|
18916
18892
|
with:
|
|
18917
18893
|
name: mjolnir-report
|
|
@@ -18942,7 +18918,7 @@ jobs:
|
|
|
18942
18918
|
contents: read
|
|
18943
18919
|
pull-requests: write
|
|
18944
18920
|
steps:
|
|
18945
|
-
- uses: actions/download-artifact@
|
|
18921
|
+
- uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
|
|
18946
18922
|
with:
|
|
18947
18923
|
name: mjolnir-report
|
|
18948
18924
|
- uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
|
@@ -19015,8 +18991,7 @@ function ciInstall(root, gate, options = {}) {
|
|
|
19015
18991
|
diffSummary: []
|
|
19016
18992
|
};
|
|
19017
18993
|
}
|
|
19018
|
-
|
|
19019
|
-
//#region src/cli-io.ts
|
|
18994
|
+
// src/cli-io.ts
|
|
19020
18995
|
const out = (...parts) => console.log(parts.map(String).join(" "));
|
|
19021
18996
|
const err = (...parts) => console.error(parts.map(String).join(" "));
|
|
19022
18997
|
function internalErrorMessage(err, emit, debug) {
|
|
@@ -19185,8 +19160,7 @@ function buildMachineContract(result) {
|
|
|
19185
19160
|
...result.forensicVerdicts !== void 0 ? { forensicVerdicts: result.forensicVerdicts } : {}
|
|
19186
19161
|
};
|
|
19187
19162
|
}
|
|
19188
|
-
|
|
19189
|
-
//#region src/commands/fixture-example.ts
|
|
19163
|
+
// src/commands/fixture-example.ts
|
|
19190
19164
|
/**
|
|
19191
19165
|
* Shared example-fixture selection for `mjolnir explain` (explain.ts)
|
|
19192
19166
|
* and the generated rule docs (rule-docs.ts) — one chooser so both
|
|
@@ -19204,8 +19178,7 @@ function firstFixtureFile(dir) {
|
|
|
19204
19178
|
const entries = readdirSync(dir).filter((f) => !f.startsWith(".")).sort();
|
|
19205
19179
|
return entries.length > 0 ? join(dir, entries[0]) : null;
|
|
19206
19180
|
}
|
|
19207
|
-
|
|
19208
|
-
//#region src/commands/explain.ts
|
|
19181
|
+
// src/commands/explain.ts
|
|
19209
19182
|
/**
|
|
19210
19183
|
* `mjolnir explain <RULE-ID>` — implements Plan.md Sprint 1.3
|
|
19211
19184
|
* (Master-Stabilization-Plan Sprint 5, Task 19).
|
|
@@ -19546,8 +19519,7 @@ function renderVerdictExplain(result, width = DEFAULT_EXPLAIN_WIDTH) {
|
|
|
19546
19519
|
function pctOf(v) {
|
|
19547
19520
|
return `${Math.round(v * 100)}%`;
|
|
19548
19521
|
}
|
|
19549
|
-
|
|
19550
|
-
//#region src/engine/resolution.ts
|
|
19522
|
+
// src/engine/resolution.ts
|
|
19551
19523
|
/** Correlation identity (v1-compatible): ruleId\0file\0message. */
|
|
19552
19524
|
const fingerprint$1 = findingFingerprint;
|
|
19553
19525
|
/**
|
|
@@ -19612,8 +19584,7 @@ function resolve$1(input) {
|
|
|
19612
19584
|
comparedAgainst
|
|
19613
19585
|
};
|
|
19614
19586
|
}
|
|
19615
|
-
|
|
19616
|
-
//#region src/commands/baseline.ts
|
|
19587
|
+
// src/commands/baseline.ts
|
|
19617
19588
|
/**
|
|
19618
19589
|
* A shared finding-set READER. Neither verb it used to serve survives the v6 CLI collapse (plan §3): `baseline` and `diff` are both retired, and the capability they shared is now `mjolnir scan --scope changed`.
|
|
19619
19590
|
* (Master-Stabilization-Plan.md).
|
|
@@ -19767,8 +19738,7 @@ function renderBaselineSaved(path, count, replaced) {
|
|
|
19767
19738
|
lines.push(nextStep("mjolnir scan --scope changed", ui$8) + " — see only what's new.");
|
|
19768
19739
|
return lines.join("\n");
|
|
19769
19740
|
}
|
|
19770
|
-
|
|
19771
|
-
//#region src/commands/verify.ts
|
|
19741
|
+
// src/commands/verify.ts
|
|
19772
19742
|
/**
|
|
19773
19743
|
* `mjolnir ci verify` — the agent-loop digest (product-gap-remediation
|
|
19774
19744
|
* master plan P7, plan 1788853205786 — flag agent, decision 6).
|
|
@@ -19875,8 +19845,7 @@ function renderVerifyDigest(digest) {
|
|
|
19875
19845
|
}
|
|
19876
19846
|
return lines.join("\n").trimEnd();
|
|
19877
19847
|
}
|
|
19878
|
-
|
|
19879
|
-
//#region src/forensics/triage.ts
|
|
19848
|
+
// src/forensics/triage.ts
|
|
19880
19849
|
const ui$7 = plainContext();
|
|
19881
19850
|
const QUARANTINE_MIN_ATTEMPTS = 2;
|
|
19882
19851
|
/** Deterministic triage table rows, worst first. */
|
|
@@ -20023,8 +19992,7 @@ function renderTriageWorkflowJson(report) {
|
|
|
20023
19992
|
rows: workflowRows(report)
|
|
20024
19993
|
}, null, 2) + "\n";
|
|
20025
19994
|
}
|
|
20026
|
-
|
|
20027
|
-
//#region src/commands/pw-report.ts
|
|
19995
|
+
// src/commands/pw-report.ts
|
|
20028
19996
|
const ui$6 = plainContext();
|
|
20029
19997
|
function summarizePwRun(report) {
|
|
20030
19998
|
const slowest = [...report.verdicts].sort((a, b) => b.totalDurationMs - a.totalDurationMs).slice(0, 5).map((v) => ({
|
|
@@ -20058,8 +20026,7 @@ function renderPwRunSummary(s) {
|
|
|
20058
20026
|
}
|
|
20059
20027
|
return lines.join("\n");
|
|
20060
20028
|
}
|
|
20061
|
-
|
|
20062
|
-
//#region src/mcp/server.ts
|
|
20029
|
+
// src/mcp/server.ts
|
|
20063
20030
|
/**
|
|
20064
20031
|
* MCP stdio transport (blueprint §21 — Phase 4, PURE TRANSPORT).
|
|
20065
20032
|
*
|
|
@@ -20077,22 +20044,38 @@ function renderPwRunSummary(s) {
|
|
|
20077
20044
|
* - parameter size caps; ONE scan in flight (serialized queue) reusing
|
|
20078
20045
|
* the pipeline's own budgets; zero network (no fetches anywhere); no
|
|
20079
20046
|
* shell execution beyond Mjölnir's own verbs.
|
|
20080
|
-
* - filesystem boundary
|
|
20047
|
+
* - filesystem boundary: the tools are read-only, and with no further
|
|
20048
|
+
* configuration they accept any path the parent process passes (the
|
|
20049
|
+
* parent is the trust boundary). `MJOLNIR_WORKSPACE_ROOT` confines every
|
|
20050
|
+
* path argument to that directory, symlinks resolved.
|
|
20081
20051
|
* - the plugin trust gate applies unchanged (--enable-plugins is NOT
|
|
20082
20052
|
* exposed over MCP: tools run with the gate CLOSED unless the server
|
|
20083
20053
|
* was started with MJOLNIR_ENABLE_PLUGINS=1 in its own environment).
|
|
20084
20054
|
*/
|
|
20085
20055
|
/** Hard caps (§21 threat model: parameter size + resource bounds). */
|
|
20086
20056
|
const MAX_PARAM_BYTES = 65536;
|
|
20087
|
-
const MAX_DURATION_MS$1 = 36e5;
|
|
20088
|
-
const DEFAULT_MAX_DURATION_MS$1 = 6e5;
|
|
20089
20057
|
const PROTOCOL_VERSION = "2025-06-18";
|
|
20090
|
-
|
|
20058
|
+
/** The real path of `p`, or its lexical resolution when it does not exist. */
|
|
20059
|
+
function realOrResolved(p) {
|
|
20060
|
+
try {
|
|
20061
|
+
return realpathSync.native(resolve(p));
|
|
20062
|
+
} catch {
|
|
20063
|
+
return resolve(p);
|
|
20064
|
+
}
|
|
20065
|
+
}
|
|
20066
|
+
/**
|
|
20067
|
+
* The optional workspace root. When set, every path argument must resolve
|
|
20068
|
+
* INSIDE it after symlinks are followed.
|
|
20069
|
+
*
|
|
20070
|
+
* Unset is open by design and is documented that way in the header: the MCP
|
|
20071
|
+
* tools are read-only and the stdio parent is the trust boundary. Setting
|
|
20072
|
+
* `MJOLNIR_WORKSPACE_ROOT` is how a host confines them.
|
|
20073
|
+
*/
|
|
20074
|
+
const CONFIGURED_ROOT = process.env.MJOLNIR_WORKSPACE_ROOT ? realOrResolved(process.env.MJOLNIR_WORKSPACE_ROOT) : null;
|
|
20091
20075
|
function isPathAllowed(path) {
|
|
20092
20076
|
if (!CONFIGURED_ROOT) return true;
|
|
20093
|
-
const
|
|
20094
|
-
|
|
20095
|
-
return rel === "" || !rel.startsWith("..") && !isAbsolute(rel);
|
|
20077
|
+
const rel = relative(CONFIGURED_ROOT, realOrResolved(path));
|
|
20078
|
+
return rel === "" || rel !== ".." && !rel.startsWith(`..${sep}`) && !isAbsolute(rel);
|
|
20096
20079
|
}
|
|
20097
20080
|
/** JSON-RPC error codes (subset used here). */
|
|
20098
20081
|
const MCP_ERRORS = {
|
|
@@ -20134,7 +20117,7 @@ const MCP_TOOLS = [
|
|
|
20134
20117
|
},
|
|
20135
20118
|
fixturesRoot: {
|
|
20136
20119
|
type: "string",
|
|
20137
|
-
description: "Root directory for fixture lookups (defaults to cwd)."
|
|
20120
|
+
description: "Root directory for fixture lookups (defaults to <cwd>/tests/fixtures, as for `mjolnir explain`)."
|
|
20138
20121
|
}
|
|
20139
20122
|
},
|
|
20140
20123
|
required: ["ruleId"]
|
|
@@ -20214,7 +20197,7 @@ function validateParams(name, args) {
|
|
|
20214
20197
|
if (typeof args["path"] !== "string" || args["path"].length === 0) return "scan requires a non-empty string `path`";
|
|
20215
20198
|
if (args["maxDurationMs"] !== void 0) {
|
|
20216
20199
|
const duration = args["maxDurationMs"];
|
|
20217
|
-
if (typeof duration !== "number" || !Number.isFinite(duration) || duration <= 0 || duration >
|
|
20200
|
+
if (typeof duration !== "number" || !Number.isFinite(duration) || duration <= 0 || duration > 36e5) return "`maxDurationMs` must be a finite positive number within the server budget";
|
|
20218
20201
|
}
|
|
20219
20202
|
}
|
|
20220
20203
|
if (name === "explain") {
|
|
@@ -20318,7 +20301,7 @@ async function handleToolCall(call) {
|
|
|
20318
20301
|
message: "fixturesRoot must be a string"
|
|
20319
20302
|
}
|
|
20320
20303
|
};
|
|
20321
|
-
const explanation = explainRule(ruleId, (typeof rawFixturesRoot === "string" ? rawFixturesRoot : void 0) ?? process.cwd());
|
|
20304
|
+
const explanation = explainRule(ruleId, (typeof rawFixturesRoot === "string" ? rawFixturesRoot : void 0) ?? join(process.cwd(), "tests", "fixtures"));
|
|
20322
20305
|
if (!explanation.ok) return {
|
|
20323
20306
|
jsonrpc: "2.0",
|
|
20324
20307
|
id: call.id,
|
|
@@ -20569,8 +20552,7 @@ async function handleMcpMessage(msg) {
|
|
|
20569
20552
|
}
|
|
20570
20553
|
};
|
|
20571
20554
|
}
|
|
20572
|
-
|
|
20573
|
-
//#region src/mcp/transport.ts
|
|
20555
|
+
// src/mcp/transport.ts
|
|
20574
20556
|
/**
|
|
20575
20557
|
* MCP stdio transport loop (blueprint §21 — the framing around the
|
|
20576
20558
|
* pure-transport server).
|
|
@@ -20667,8 +20649,7 @@ async function handleStdioLine(line, write) {
|
|
|
20667
20649
|
}
|
|
20668
20650
|
write(`${JSON.stringify(response)}\n`);
|
|
20669
20651
|
}
|
|
20670
|
-
|
|
20671
|
-
//#region src/scorer/prioritize.ts
|
|
20652
|
+
// src/scorer/prioritize.ts
|
|
20672
20653
|
/**
|
|
20673
20654
|
* "Fix this first" prioritization (Master-Stabilization-Plan Sprint 5,
|
|
20674
20655
|
* Task 20).
|
|
@@ -20718,8 +20699,7 @@ function prioritize(findings) {
|
|
|
20718
20699
|
function topFixes(findings, n = 3) {
|
|
20719
20700
|
return prioritize(findings).filter((p) => p.scoreGain > 0).slice(0, n);
|
|
20720
20701
|
}
|
|
20721
|
-
|
|
20722
|
-
//#region src/reporter/art.ts
|
|
20702
|
+
// src/reporter/art.ts
|
|
20723
20703
|
/**
|
|
20724
20704
|
* ASCII art assets for the Mjölnir terminal experience.
|
|
20725
20705
|
* Minimal Nordic / engineering aesthetic — professional, not fantasy.
|
|
@@ -20756,8 +20736,7 @@ const TROPHY = String.raw`
|
|
|
20756
20736
|
"─".repeat(58);
|
|
20757
20737
|
/** Wordmark for the 100-state celebration block. */
|
|
20758
20738
|
const FORGED_WORDMARK = "⚡ F O R G E D ⚡";
|
|
20759
|
-
|
|
20760
|
-
//#region src/reporter/tone-blunt.ts
|
|
20739
|
+
// src/reporter/tone-blunt.ts
|
|
20761
20740
|
/**
|
|
20762
20741
|
* Static blunt messages keyed by rule ID. A simple string lookup
|
|
20763
20742
|
* (no per-entry closures) so coverage is not artificially fragmented
|
|
@@ -20829,8 +20808,7 @@ function bluntMessage(f) {
|
|
|
20829
20808
|
if (msg !== void 0) return msg;
|
|
20830
20809
|
return `${f.message} — fix it or suppress it; ignoring it is worse than either.`;
|
|
20831
20810
|
}
|
|
20832
|
-
|
|
20833
|
-
//#region src/reporter/terminal.ts
|
|
20811
|
+
// src/reporter/terminal.ts
|
|
20834
20812
|
/** Non-verbose finding cards shown before the overflow line. The
|
|
20835
20813
|
* JSON/SARIF contract always carries ALL findings — this cap is a
|
|
20836
20814
|
* terminal display concern only. */
|
|
@@ -21312,8 +21290,7 @@ function countBySeverity(result) {
|
|
|
21312
21290
|
}
|
|
21313
21291
|
return counts;
|
|
21314
21292
|
}
|
|
21315
|
-
|
|
21316
|
-
//#region src/engine/trust-classification.ts
|
|
21293
|
+
// src/engine/trust-classification.ts
|
|
21317
21294
|
/**
|
|
21318
21295
|
* Completeness, decided once.
|
|
21319
21296
|
*
|
|
@@ -21419,8 +21396,7 @@ function classifyTrust(result, summary) {
|
|
|
21419
21396
|
gaps
|
|
21420
21397
|
};
|
|
21421
21398
|
}
|
|
21422
|
-
|
|
21423
|
-
//#region src/lib/format.ts
|
|
21399
|
+
// src/lib/format.ts
|
|
21424
21400
|
/**
|
|
21425
21401
|
* Shared formatting utilities. Single definition site for `pct()` (Task 20)
|
|
21426
21402
|
* — the two callers (commands/trust-report.ts, reporter/trust-report.ts)
|
|
@@ -21429,8 +21405,7 @@ function classifyTrust(result, summary) {
|
|
|
21429
21405
|
function pct$1(v) {
|
|
21430
21406
|
return `${Math.round(v * 100)}%`;
|
|
21431
21407
|
}
|
|
21432
|
-
|
|
21433
|
-
//#region src/reporter/trust-report.ts
|
|
21408
|
+
// src/reporter/trust-report.ts
|
|
21434
21409
|
/** The one label rule, so no renderer grows a second opinion. */
|
|
21435
21410
|
function gateLabel(f, gate) {
|
|
21436
21411
|
return isAtGate(f, gate, isAdvisoryFinding) ? "GATE" : "WARN";
|
|
@@ -21571,8 +21546,7 @@ function renderTrustReport(result, opts) {
|
|
|
21571
21546
|
lines.push("");
|
|
21572
21547
|
return lines.join("\n");
|
|
21573
21548
|
}
|
|
21574
|
-
|
|
21575
|
-
//#region src/reporter/sarif.ts
|
|
21549
|
+
// src/reporter/sarif.ts
|
|
21576
21550
|
/**
|
|
21577
21551
|
* SARIF fingerprint values must be printable and stable. The engine's identity
|
|
21578
21552
|
* strings join their parts with NUL (a separator no repository path or message
|
|
@@ -21684,6 +21658,10 @@ function renderSarif(result, repoRootUri) {
|
|
|
21684
21658
|
}
|
|
21685
21659
|
}))
|
|
21686
21660
|
};
|
|
21661
|
+
const srcRoot = (uri) => {
|
|
21662
|
+
const encoded = uri.replaceAll("\\", "/").split("/").map((seg) => seg === "" ? "" : encodeURI(seg).replaceAll("%25", "%").replaceAll("#", "%23").replaceAll("?", "%3F")).join("/");
|
|
21663
|
+
return encoded.endsWith("/") ? encoded : `${encoded}/`;
|
|
21664
|
+
};
|
|
21687
21665
|
const sarif = {
|
|
21688
21666
|
$schema: "https://json.schemastore.org/sarif-2.1.0.json",
|
|
21689
21667
|
version: [
|
|
@@ -21693,13 +21671,12 @@ function renderSarif(result, repoRootUri) {
|
|
|
21693
21671
|
].join("."),
|
|
21694
21672
|
runs: [repoRootUri ? {
|
|
21695
21673
|
...run,
|
|
21696
|
-
originalUriBaseIds: { SRCROOT: { uri: repoRootUri
|
|
21674
|
+
originalUriBaseIds: { SRCROOT: { uri: srcRoot(repoRootUri) } }
|
|
21697
21675
|
} : run]
|
|
21698
21676
|
};
|
|
21699
21677
|
return JSON.stringify(sarif, null, 2);
|
|
21700
21678
|
}
|
|
21701
|
-
|
|
21702
|
-
//#region src/reporter/codequality.ts
|
|
21679
|
+
// src/reporter/codequality.ts
|
|
21703
21680
|
function codeQualitySeverity(severity) {
|
|
21704
21681
|
if (severity === "error") return "major";
|
|
21705
21682
|
if (severity === "warning") return "minor";
|
|
@@ -21718,8 +21695,7 @@ function renderCodeQuality(result) {
|
|
|
21718
21695
|
}));
|
|
21719
21696
|
return JSON.stringify(issues, null, 2) + "\n";
|
|
21720
21697
|
}
|
|
21721
|
-
|
|
21722
|
-
//#region src/reporter/mermaid.ts
|
|
21698
|
+
// src/reporter/mermaid.ts
|
|
21723
21699
|
/**
|
|
21724
21700
|
* `--format mermaid` — test-architecture diagram (Sprint 9 Task 38,
|
|
21725
21701
|
* Master-Stabilization-Plan.md). Ranked first among the delight
|
|
@@ -21818,8 +21794,7 @@ function renderMermaid(result) {
|
|
|
21818
21794
|
for (const { id, cls } of styleAssignments) lines.push(` class ${id} ${cls};`);
|
|
21819
21795
|
return lines.join("\n");
|
|
21820
21796
|
}
|
|
21821
|
-
|
|
21822
|
-
//#region src/reporter/progress.ts
|
|
21797
|
+
// src/reporter/progress.ts
|
|
21823
21798
|
/**
|
|
21824
21799
|
* Live scan progress (Terminal + CI UX Overhaul plan, M3).
|
|
21825
21800
|
*
|
|
@@ -21918,8 +21893,7 @@ var ProgressRenderer = class {
|
|
|
21918
21893
|
this.erase();
|
|
21919
21894
|
}
|
|
21920
21895
|
};
|
|
21921
|
-
|
|
21922
|
-
//#region src/lib/git-utils.ts
|
|
21896
|
+
// src/lib/git-utils.ts
|
|
21923
21897
|
/**
|
|
21924
21898
|
* Shared git utilities (Task 18). The `currentCommit` / `targetCommit`
|
|
21925
21899
|
* pattern was duplicated between cli.ts and commands/trust-report.ts.
|
|
@@ -21952,8 +21926,7 @@ function currentCommit(root, opts = {}) {
|
|
|
21952
21926
|
return opts.nullable ? null : "unknown";
|
|
21953
21927
|
}
|
|
21954
21928
|
}
|
|
21955
|
-
|
|
21956
|
-
//#region src/commands/report-io.ts
|
|
21929
|
+
// src/commands/report-io.ts
|
|
21957
21930
|
/**
|
|
21958
21931
|
* Shared saved-report loading (agent-handoff plan §9.0).
|
|
21959
21932
|
*
|
|
@@ -22146,8 +22119,7 @@ function loadSavedReport(reportPath) {
|
|
|
22146
22119
|
function reportExists(reportPath) {
|
|
22147
22120
|
return existsSync(reportPath);
|
|
22148
22121
|
}
|
|
22149
|
-
|
|
22150
|
-
//#region src/commands/trust-report.ts
|
|
22122
|
+
// src/commands/trust-report.ts
|
|
22151
22123
|
function buildArtifactIdentity(result, commit) {
|
|
22152
22124
|
const revisions = /* @__PURE__ */ new Map();
|
|
22153
22125
|
const levels = /* @__PURE__ */ new Map();
|
|
@@ -22173,8 +22145,7 @@ function buildArtifactIdentity(result, commit) {
|
|
|
22173
22145
|
}
|
|
22174
22146
|
};
|
|
22175
22147
|
}
|
|
22176
|
-
|
|
22177
|
-
//#region src/reporter/pr-report-actions.ts
|
|
22148
|
+
// src/reporter/pr-report-actions.ts
|
|
22178
22149
|
/** GitHub-native actions. Social drafts contain only public project information. */
|
|
22179
22150
|
const PROJECT_URL = "https://github.com/Sergey-Bar/Mjolnir";
|
|
22180
22151
|
const SHARE_URL = "https://sergey-bar.github.io/Mjolnir/";
|
|
@@ -22293,8 +22264,7 @@ const PR_BRAND_CONTRACT = {
|
|
|
22293
22264
|
terminology: "trust-engine",
|
|
22294
22265
|
footerFormat: "scanId + generatedBy"
|
|
22295
22266
|
};
|
|
22296
|
-
|
|
22297
|
-
//#region src/reporter/pr-report-shared.ts
|
|
22267
|
+
// src/reporter/pr-report-shared.ts
|
|
22298
22268
|
const UNIFIED_MARKER = "<!-- mjolnir-report:v2 -->";
|
|
22299
22269
|
/**
|
|
22300
22270
|
* The one place a comment asset URL is written down.
|
|
@@ -22321,6 +22291,24 @@ const MAX_FINDINGS_PER_GROUP = 25;
|
|
|
22321
22291
|
function escMd(s) {
|
|
22322
22292
|
return sanitizeData(s).replace(/([\\`*_{}[\]()#+!|<>])/g, "\\$1");
|
|
22323
22293
|
}
|
|
22294
|
+
/**
|
|
22295
|
+
* A Markdown code span for untrusted text.
|
|
22296
|
+
*
|
|
22297
|
+
* Backslash escapes are literal inside a code span, so `escMd` there rendered
|
|
22298
|
+
* `__snapshots__` as `\_\_snapshots\_\_` and `app/(auth)` as `app/\(auth\)`.
|
|
22299
|
+
* A span cannot be broken out of with formatting characters, only with a
|
|
22300
|
+
* backtick run, so it is fenced with one backtick more than the longest run
|
|
22301
|
+
* inside (CommonMark 6.1) and padded where the text would otherwise touch the
|
|
22302
|
+
* fence. Kept to one line, because a newline would end the list item.
|
|
22303
|
+
*/
|
|
22304
|
+
function mdCodeSpan(raw) {
|
|
22305
|
+
const text = sanitizeData(raw).replace(/[\r\n]+/g, " ");
|
|
22306
|
+
if (text.trim() === "") return "` `";
|
|
22307
|
+
const longest = Math.max(0, ...[...text.matchAll(/`+/g)].map((m) => m[0].length));
|
|
22308
|
+
const fence = "`".repeat(longest + 1);
|
|
22309
|
+
const pad = text.startsWith("`") || text.endsWith("`") || text.startsWith(" ") && text.endsWith(" ") ? " " : "";
|
|
22310
|
+
return `${fence}${pad}${text}${pad}${fence}`;
|
|
22311
|
+
}
|
|
22324
22312
|
function looksLikeCode(s) {
|
|
22325
22313
|
return /[(;={]|await |expect\(/.test(s);
|
|
22326
22314
|
}
|
|
@@ -22335,10 +22323,8 @@ function evidenceTagLine(f) {
|
|
|
22335
22323
|
}
|
|
22336
22324
|
function findingLineMd(f) {
|
|
22337
22325
|
const icon = f.severity === "error" ? "🔴" : f.severity === "warning" ? "🟡" : "🔵";
|
|
22338
|
-
const
|
|
22339
|
-
|
|
22340
|
-
const safePath = escMd(f.file).replace(/[\r\n]+/g, " ");
|
|
22341
|
-
return `${icon} **${escMd(f.ruleId)}** \`${safePath}:${f.line}\` — ${escMd(f.message)} ${escMd(`[${evidenceTagLine(f)}]`)}\n Fix: ${fixBody}`;
|
|
22326
|
+
const fixBody = looksLikeCode(f.fix) ? mdCodeSpan(f.fix) : `_${escMd(f.fix)}_`;
|
|
22327
|
+
return `${icon} **${escMd(f.ruleId)}** ${mdCodeSpan(`${f.file}:${f.line}`)} — ${escMd(f.message)} ${escMd(`[${evidenceTagLine(f)}]`)}\n Fix: ${fixBody}`;
|
|
22342
22328
|
}
|
|
22343
22329
|
/**
|
|
22344
22330
|
* Reader-facing sentences for the `analysisStatus.reasons` tokens.
|
|
@@ -22369,7 +22355,8 @@ const REASON_PHRASES = [
|
|
|
22369
22355
|
["runtime-incomplete", "the runtime evidence set is incomplete"],
|
|
22370
22356
|
["identity-incomplete", "the run could not be bound to the tree it analysed"],
|
|
22371
22357
|
["degraded:", "capability was lost to a swallowed error"],
|
|
22372
|
-
["truncated:", "the scan was cut short"]
|
|
22358
|
+
["truncated:", "the scan was cut short"],
|
|
22359
|
+
["coverage:quarantine:", "quarantine-tier rule(s) withheld, as in every default scan (`--include-warn` runs them)"]
|
|
22373
22360
|
];
|
|
22374
22361
|
/**
|
|
22375
22362
|
* `scope-ignored:1` -> "1 path was ignored by the scope filter".
|
|
@@ -22516,7 +22503,9 @@ function renderUnifiedReport(result, options = {}) {
|
|
|
22516
22503
|
provisionalRuleIds: [],
|
|
22517
22504
|
ceilingReasons: []
|
|
22518
22505
|
};
|
|
22519
|
-
const
|
|
22506
|
+
const analysisIncomplete = isIncompleteResult(result) || !result.trustSummary;
|
|
22507
|
+
const noEvidence = summary.level === "L0" && summary.evidenceCoverage === 0;
|
|
22508
|
+
const incomplete = analysisIncomplete || noEvidence;
|
|
22520
22509
|
lines.push(`## <img width="16" height="16" alt="" src="${MARK_URL}"/> Mjölnir Verification Report`);
|
|
22521
22510
|
lines.push("");
|
|
22522
22511
|
lines.push("> Tests tell you what passed. Mjölnir tells you what you can trust.");
|
|
@@ -22527,8 +22516,9 @@ function renderUnifiedReport(result, options = {}) {
|
|
|
22527
22516
|
}
|
|
22528
22517
|
if (incomplete) {
|
|
22529
22518
|
const reasons = result.analysisStatus?.reasons ?? [];
|
|
22519
|
+
const explained = [...reasons.length > 0 ? [humanizeReasons(reasons)] : [], ...!analysisIncomplete && noEvidence ? ["no runtime evidence corroborates this run, so an empty result is an observation, not a verdict"] : []];
|
|
22530
22520
|
lines.push(`> [!WARNING]
|
|
22531
|
-
> **Analysis status: INCOMPLETE.** ${
|
|
22521
|
+
> **Analysis status: INCOMPLETE.** ${explained.length > 0 ? explained.join("; ") : "The scan did not complete a trustworthy analysis."}`);
|
|
22532
22522
|
lines.push("");
|
|
22533
22523
|
}
|
|
22534
22524
|
const diff = options.diff;
|
|
@@ -22698,8 +22688,7 @@ function enforceCommentLimit(lines) {
|
|
|
22698
22688
|
}
|
|
22699
22689
|
return render() + notice;
|
|
22700
22690
|
}
|
|
22701
|
-
|
|
22702
|
-
//#region src/commands/pr-comment.ts
|
|
22691
|
+
// src/commands/pr-comment.ts
|
|
22703
22692
|
/**
|
|
22704
22693
|
* Escape markdown-significant characters (markdown spec).
|
|
22705
22694
|
* Kept exported for downstream consumer compatibility.
|
|
@@ -22721,8 +22710,7 @@ function renderPrComment(result, options = {}) {
|
|
|
22721
22710
|
...options.commit !== void 0 ? { commit: options.commit } : {}
|
|
22722
22711
|
});
|
|
22723
22712
|
}
|
|
22724
|
-
|
|
22725
|
-
//#region src/commands/why.ts
|
|
22713
|
+
// src/commands/why.ts
|
|
22726
22714
|
/**
|
|
22727
22715
|
* `mjolnir explain <file:line> <file>:<line>` — occurrence-level evidence/explanation
|
|
22728
22716
|
* query (agent-handoff plan M2).
|
|
@@ -22894,8 +22882,7 @@ async function runWhyCommand(argv, io = {
|
|
|
22894
22882
|
}));
|
|
22895
22883
|
return filtered.length > 0 ? 0 : 1;
|
|
22896
22884
|
}
|
|
22897
|
-
|
|
22898
|
-
//#region src/commands/handover.ts
|
|
22885
|
+
// src/commands/handover.ts
|
|
22899
22886
|
const ui$5 = plainContext();
|
|
22900
22887
|
const FAKE_GREEN_RULES = /* @__PURE__ */ new Set([
|
|
22901
22888
|
"QA-TEST-003",
|
|
@@ -22983,8 +22970,7 @@ function renderHandover(map) {
|
|
|
22983
22970
|
lines.push("Generated by mjolnir explain --plan — re-run after big refactors.");
|
|
22984
22971
|
return lines.join("\n");
|
|
22985
22972
|
}
|
|
22986
|
-
|
|
22987
|
-
//#region src/commands/stats.ts
|
|
22973
|
+
// src/commands/stats.ts
|
|
22988
22974
|
/**
|
|
22989
22975
|
* `mjolnir stats` — Sprint 6 Task 26 (Master-Stabilization-Plan.md).
|
|
22990
22976
|
*
|
|
@@ -23104,14 +23090,72 @@ function renderStats(stats) {
|
|
|
23104
23090
|
lines.push("different machine — this file is local-only, by design.");
|
|
23105
23091
|
return lines.join("\n");
|
|
23106
23092
|
}
|
|
23107
|
-
|
|
23108
|
-
|
|
23093
|
+
// src/reporter/github.ts
|
|
23094
|
+
/** Escape a workflow-command PROPERTY value. */
|
|
23095
|
+
function escapeAnnotationProperty(value) {
|
|
23096
|
+
return value.replaceAll("%", "%25").replaceAll("\r", "%0D").replaceAll("\n", "%0A").replaceAll(":", "%3A").replaceAll(",", "%2C");
|
|
23097
|
+
}
|
|
23098
|
+
/** Escape a workflow-command MESSAGE. */
|
|
23099
|
+
function escapeAnnotationMessage(value) {
|
|
23100
|
+
return value.replaceAll("%", "%25").replaceAll("\r", "%0D").replaceAll("\n", "%0A");
|
|
23101
|
+
}
|
|
23109
23102
|
/** ANSI-injection guard for the summary path (belt + braces). */
|
|
23110
23103
|
function stripAnsiForSummary(value) {
|
|
23111
23104
|
return value.replace(/\x1b\[[0-9;:?]*[ -/]*[@-~]/g, "");
|
|
23112
23105
|
}
|
|
23113
|
-
|
|
23114
|
-
|
|
23106
|
+
const SEVERITY_TO_COMMAND = {
|
|
23107
|
+
error: "error",
|
|
23108
|
+
warning: "warning",
|
|
23109
|
+
info: "notice"
|
|
23110
|
+
};
|
|
23111
|
+
/** Render ONE workflow-command annotation line (no trailing newline). */
|
|
23112
|
+
function renderAnnotation(a) {
|
|
23113
|
+
const props = [`file=${escapeAnnotationProperty(a.file)}`, `line=${Math.max(1, a.line ?? 1)}`];
|
|
23114
|
+
if (a.column !== void 0) props.push(`column=${Math.max(1, a.column)}`);
|
|
23115
|
+
props.push(`title=${escapeAnnotationProperty(a.title)}`);
|
|
23116
|
+
return `::${SEVERITY_TO_COMMAND[a.severity]} ${props.join(",")}::${escapeAnnotationMessage(a.message)}`;
|
|
23117
|
+
}
|
|
23118
|
+
/** Render annotations for every finding in a report's findings array. */
|
|
23119
|
+
function renderAnnotations(findings) {
|
|
23120
|
+
return findings.map((f) => {
|
|
23121
|
+
return renderAnnotation({
|
|
23122
|
+
severity: f.severity === "error" || f.severity === "warning" ? f.severity : "info",
|
|
23123
|
+
file: f.file,
|
|
23124
|
+
line: f.line,
|
|
23125
|
+
column: f.column,
|
|
23126
|
+
title: f.ruleId,
|
|
23127
|
+
message: f.message
|
|
23128
|
+
});
|
|
23129
|
+
});
|
|
23130
|
+
}
|
|
23131
|
+
/** GitHub caps annotation messages — keep the head, point at the summary. */
|
|
23132
|
+
function truncateMessage(message, max = 250) {
|
|
23133
|
+
if (message.length <= max) return message;
|
|
23134
|
+
return `${message.slice(0, max)}… (full text in the step summary)`;
|
|
23135
|
+
}
|
|
23136
|
+
// src/commands/summary.ts
|
|
23137
|
+
/**
|
|
23138
|
+
* the `--format github-summary` render — CI annotations + step summary
|
|
23139
|
+
* (Terminal + CI UX Overhaul plan, M4).
|
|
23140
|
+
*
|
|
23141
|
+
* Additive command; default report path `mjolnir.json`. Reads a saved
|
|
23142
|
+
* ScanResult JSON (the `--json` scan output) and emits:
|
|
23143
|
+
* 1. GitHub annotations to stdout, only when GITHUB_ACTIONS=true —
|
|
23144
|
+
* one per finding, via the single github.ts emitter.
|
|
23145
|
+
* 2. A step-summary markdown document to $GITHUB_STEP_SUMMARY when
|
|
23146
|
+
* set (else stdout; --stdout forces stdout): score + band, text
|
|
23147
|
+
* score bar, dimensions table, top deductions, collapsible
|
|
23148
|
+
* per-severity details with fix lines, and an honesty notice for
|
|
23149
|
+
* partial/no-score reports.
|
|
23150
|
+
*
|
|
23151
|
+
* Exit codes (frozen contract): 0 on success — this command NEVER
|
|
23152
|
+
* blocks; the gate step decides. 10 missing file argument. 2
|
|
23153
|
+
* unreadable/invalid JSON (a data problem; the message explains).
|
|
23154
|
+
*
|
|
23155
|
+
* Paths: findings are scan-target-relative. The generated workflow
|
|
23156
|
+
* scans the repo root, so the default output is directly usable;
|
|
23157
|
+
* `--path-prefix <dir>` re-scopes for subdirectory scans.
|
|
23158
|
+
*/
|
|
23115
23159
|
const DETAILS_PER_SEVERITY_CAP = 25;
|
|
23116
23160
|
/**
|
|
23117
23161
|
* The score bar. BW-105: the canonical gauge with the inert palette — the
|
|
@@ -23188,7 +23232,7 @@ function renderStepSummary(result, options = {}) {
|
|
|
23188
23232
|
lines.push(`<details${open}>`, `<summary>${icons[sev]} ${list.length} ${sev}${list.length === 1 ? "" : "s"}</summary>`, "");
|
|
23189
23233
|
for (const f of list.slice(0, DETAILS_PER_SEVERITY_CAP)) {
|
|
23190
23234
|
const path = options.pathPrefix ? `${options.pathPrefix.replace(/\/$/, "")}/${f.file}` : f.file;
|
|
23191
|
-
lines.push(`- **${escapeMarkdown(f.ruleId)}**
|
|
23235
|
+
lines.push(`- **${escapeMarkdown(f.ruleId)}** ${mdCodeSpan(`${path}:${f.line}`)} — ${escapeMarkdown(stripAnsiForSummary(f.message))}`);
|
|
23192
23236
|
lines.push(` - Fix: ${escapeMarkdown(stripAnsiForSummary(f.fix))}`);
|
|
23193
23237
|
}
|
|
23194
23238
|
if (list.length > DETAILS_PER_SEVERITY_CAP) lines.push(`- … and ${list.length - DETAILS_PER_SEVERITY_CAP} more — see the full JSON artifact.`);
|
|
@@ -23205,8 +23249,92 @@ function renderStepSummary(result, options = {}) {
|
|
|
23205
23249
|
lines.push("<!-- mjolnir-honesty: scores derive from rules with published evidence levels and measured false-positive rates where available. -->");
|
|
23206
23250
|
return lines.join("\n");
|
|
23207
23251
|
}
|
|
23208
|
-
|
|
23209
|
-
|
|
23252
|
+
/** Render one finding as an escaped annotation line. Fields are
|
|
23253
|
+
* sanitized with the same sanitizeData layer the terminal uses
|
|
23254
|
+
* (github.ts's escapers cover %/CR/LF, not OSC/C0), then %/CR/LF-escaped. */
|
|
23255
|
+
function annotationForFinding(f) {
|
|
23256
|
+
return renderAnnotations([{
|
|
23257
|
+
...f,
|
|
23258
|
+
file: sanitizeData(f.file),
|
|
23259
|
+
ruleId: sanitizeData(f.ruleId),
|
|
23260
|
+
message: truncateMessage(stripAnsiForSummary(sanitizeData(f.message)))
|
|
23261
|
+
}])[0];
|
|
23262
|
+
}
|
|
23263
|
+
const KNOWN_SUMMARY_FLAGS = /* @__PURE__ */ new Set([
|
|
23264
|
+
"--stdout",
|
|
23265
|
+
"--path-prefix",
|
|
23266
|
+
"--help",
|
|
23267
|
+
"-h"
|
|
23268
|
+
]);
|
|
23269
|
+
/**
|
|
23270
|
+
* Testable summary command core. Returns the process exit code.
|
|
23271
|
+
* Streams: annotations → stdout (always; GitHub greps them), summary →
|
|
23272
|
+
* step-summary file when set unless --stdout. Unknown flags are a
|
|
23273
|
+
* usage error (exit 10) — a typo'd --stdout must not silently route
|
|
23274
|
+
* the summary to $GITHUB_STEP_SUMMARY.
|
|
23275
|
+
*/
|
|
23276
|
+
function runSummaryCommand(argv, io = {
|
|
23277
|
+
out: (line) => console.log(line),
|
|
23278
|
+
err: (line) => console.error(line)
|
|
23279
|
+
}) {
|
|
23280
|
+
for (let i = 0; i < argv.length; i++) {
|
|
23281
|
+
const a = argv[i] ?? "";
|
|
23282
|
+
if (!a.startsWith("-")) continue;
|
|
23283
|
+
if (a === "--path-prefix") {
|
|
23284
|
+
const val = argv[i + 1];
|
|
23285
|
+
if (val === void 0 || val.startsWith("-")) {
|
|
23286
|
+
io.err(usageErrorMessage({
|
|
23287
|
+
flag: "--path-prefix",
|
|
23288
|
+
token: val
|
|
23289
|
+
}));
|
|
23290
|
+
return 10;
|
|
23291
|
+
}
|
|
23292
|
+
i++;
|
|
23293
|
+
continue;
|
|
23294
|
+
}
|
|
23295
|
+
if (KNOWN_SUMMARY_FLAGS.has(a)) continue;
|
|
23296
|
+
io.err(usageErrorMessage({ token: a }));
|
|
23297
|
+
return 10;
|
|
23298
|
+
}
|
|
23299
|
+
const stdout = argv.includes("--stdout");
|
|
23300
|
+
const prefixIdx = argv.indexOf("--path-prefix");
|
|
23301
|
+
const pathPrefix = prefixIdx !== -1 ? argv[prefixIdx + 1] : void 0;
|
|
23302
|
+
const reportPath = argv.filter((a, i) => a !== void 0 && !a.startsWith("-") && (prefixIdx === -1 || i !== prefixIdx + 1))[0] ?? "mjolnir.json";
|
|
23303
|
+
if (!existsSync(reportPath)) {
|
|
23304
|
+
io.err(`mjolnir: report file not found: ${reportPath}`);
|
|
23305
|
+
io.err(" Run the scan with --json first: mjolnir --json > mjolnir.json");
|
|
23306
|
+
return 10;
|
|
23307
|
+
}
|
|
23308
|
+
let result;
|
|
23309
|
+
try {
|
|
23310
|
+
result = loadSavedReport(reportPath);
|
|
23311
|
+
} catch (err) {
|
|
23312
|
+
io.err(`mjolnir: cannot read ${reportPath}: ${errorMessage(err)}`);
|
|
23313
|
+
return 2;
|
|
23314
|
+
}
|
|
23315
|
+
const env = process.env;
|
|
23316
|
+
if (env["GITHUB_ACTIONS"] === "true") for (const f of result.findings) {
|
|
23317
|
+
const file = pathPrefix ? `${pathPrefix.replace(/\/$/, "")}/${f.file}` : f.file;
|
|
23318
|
+
io.out(annotationForFinding({
|
|
23319
|
+
...f,
|
|
23320
|
+
file
|
|
23321
|
+
}));
|
|
23322
|
+
}
|
|
23323
|
+
const summary = renderStepSummary(result, {
|
|
23324
|
+
stdout,
|
|
23325
|
+
...pathPrefix !== void 0 ? { pathPrefix } : {}
|
|
23326
|
+
});
|
|
23327
|
+
const stepSummaryPath = stdout ? void 0 : env["GITHUB_STEP_SUMMARY"];
|
|
23328
|
+
if (stepSummaryPath) try {
|
|
23329
|
+
appendFileSync(stepSummaryPath, `${summary}\n`);
|
|
23330
|
+
} catch (err) {
|
|
23331
|
+
io.err(`mjolnir: could not write $GITHUB_STEP_SUMMARY (${errorMessage(err)}); printing to stdout instead.`);
|
|
23332
|
+
io.out(summary);
|
|
23333
|
+
}
|
|
23334
|
+
else io.out(summary);
|
|
23335
|
+
return 0;
|
|
23336
|
+
}
|
|
23337
|
+
// src/commands/milestone-args.ts
|
|
23210
23338
|
/**
|
|
23211
23339
|
* The argument layer every milestone command shares.
|
|
23212
23340
|
*
|
|
@@ -23358,8 +23486,7 @@ function isWorkflowFile(path) {
|
|
|
23358
23486
|
const normalized = path.replaceAll("\\", "/");
|
|
23359
23487
|
return normalized.startsWith(".github/workflows/") || normalized === ".gitlab-ci.yml" || normalized === ".gitlab-ci.yaml" || basename(normalized) === "Jenkinsfile";
|
|
23360
23488
|
}
|
|
23361
|
-
|
|
23362
|
-
//#region src/engine/cross-file.ts
|
|
23489
|
+
// src/engine/cross-file.ts
|
|
23363
23490
|
/**
|
|
23364
23491
|
* Cross-file analysis (Upgrade-Plan-v3 Phase 6, tier-3 item #2).
|
|
23365
23492
|
*
|
|
@@ -23405,8 +23532,7 @@ function findDuplicateTestNames(files) {
|
|
|
23405
23532
|
});
|
|
23406
23533
|
return out.sort((a, b) => compareCodePoints(a.name, b.name));
|
|
23407
23534
|
}
|
|
23408
|
-
|
|
23409
|
-
//#region src/engine/cross-file-analysis.ts
|
|
23535
|
+
// src/engine/cross-file-analysis.ts
|
|
23410
23536
|
/**
|
|
23411
23537
|
* Analyze cross-file signals across the scanned test files.
|
|
23412
23538
|
* Pure function — same input always produces same output.
|
|
@@ -23544,8 +23670,7 @@ function renderCrossFileAnalysis(result) {
|
|
|
23544
23670
|
}
|
|
23545
23671
|
return lines.join("\n").trimEnd();
|
|
23546
23672
|
}
|
|
23547
|
-
|
|
23548
|
-
//#region src/engine/evidence-graph.ts
|
|
23673
|
+
// src/engine/evidence-graph.ts
|
|
23549
23674
|
/**
|
|
23550
23675
|
* Build a complete evidence graph from a scan result.
|
|
23551
23676
|
* Pure function — deterministic output for the same input.
|
|
@@ -23691,8 +23816,7 @@ function renderEvidenceGraphResult(result) {
|
|
|
23691
23816
|
lines.push(` ${result.agenticProfile.note}`);
|
|
23692
23817
|
return lines.join("\n").trimEnd();
|
|
23693
23818
|
}
|
|
23694
|
-
|
|
23695
|
-
//#region src/frameworks/scorecard.ts
|
|
23819
|
+
// src/frameworks/scorecard.ts
|
|
23696
23820
|
const SCORECARD_DIMENSIONS = [
|
|
23697
23821
|
"discovery",
|
|
23698
23822
|
"astUsage",
|
|
@@ -24802,8 +24926,7 @@ const FRAMEWORK_SCORECARDS = [
|
|
|
24802
24926
|
makeScorecard("jenkins", FRAMEWORK_OVERRIDES["jenkins"]),
|
|
24803
24927
|
makeScorecard("gitlab-ci", FRAMEWORK_OVERRIDES["gitlab-ci"])
|
|
24804
24928
|
];
|
|
24805
|
-
|
|
24806
|
-
//#region src/engine/framework-maturity.ts
|
|
24929
|
+
// src/engine/framework-maturity.ts
|
|
24807
24930
|
/**
|
|
24808
24931
|
* M5 — Framework Maturity Tracking for Playwright.
|
|
24809
24932
|
*
|
|
@@ -24892,8 +25015,7 @@ function renderPlaywrightMaturityReport(report) {
|
|
|
24892
25015
|
}
|
|
24893
25016
|
return lines.join("\n").trimEnd();
|
|
24894
25017
|
}
|
|
24895
|
-
|
|
24896
|
-
//#region src/engine/suppression-integrity.ts
|
|
25018
|
+
// src/engine/suppression-integrity.ts
|
|
24897
25019
|
/**
|
|
24898
25020
|
* ENGINE-006 — Suppression Integrity: detect suppression abuse,
|
|
24899
25021
|
* unknown rules, expired entries, and compute a full integrity report.
|
|
@@ -24987,8 +25109,7 @@ function computeSuppressionIntegrity(suppressions, findings, knownRuleIds, now =
|
|
|
24987
25109
|
expiredSuppressions: detectExpiredSuppressions(suppressions, now)
|
|
24988
25110
|
};
|
|
24989
25111
|
}
|
|
24990
|
-
|
|
24991
|
-
//#region src/engine/suppression-governance.ts
|
|
25112
|
+
// src/engine/suppression-governance.ts
|
|
24992
25113
|
/**
|
|
24993
25114
|
* M6 — Suppression Policy Governance Gate.
|
|
24994
25115
|
*
|
|
@@ -25051,15 +25172,13 @@ function renderSuppressionGovernanceResult(result) {
|
|
|
25051
25172
|
} else lines.push("All suppression policy checks passed.");
|
|
25052
25173
|
return lines.join("\n").trimEnd();
|
|
25053
25174
|
}
|
|
25054
|
-
|
|
25055
|
-
//#region src/adapters/gitlab-ci.ts
|
|
25175
|
+
// src/adapters/gitlab-ci.ts
|
|
25056
25176
|
function parseGitLabCi(content) {
|
|
25057
25177
|
const parsed = parse(content);
|
|
25058
25178
|
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) throw new Error("GitLab CI YAML is not a valid object.");
|
|
25059
25179
|
return parsed;
|
|
25060
25180
|
}
|
|
25061
|
-
|
|
25062
|
-
//#region src/engine/cli-command-names.ts
|
|
25181
|
+
// src/engine/cli-command-names.ts
|
|
25063
25182
|
const CLI_COMMAND_NAMES = [
|
|
25064
25183
|
"scan",
|
|
25065
25184
|
"policy",
|
|
@@ -25079,8 +25198,7 @@ const CLI_COMMAND_NAMES = [
|
|
|
25079
25198
|
"help"
|
|
25080
25199
|
];
|
|
25081
25200
|
const CLI_NON_SCAN_COMMANDS = new Set(CLI_COMMAND_NAMES.filter((command) => command !== "scan"));
|
|
25082
|
-
|
|
25083
|
-
//#region src/engine/verification-intelligence.ts
|
|
25201
|
+
// src/engine/verification-intelligence.ts
|
|
25084
25202
|
const CI_WORKFLOW_FILES = [
|
|
25085
25203
|
".github/workflows/ci.yml",
|
|
25086
25204
|
".github/workflows/test.yml",
|
|
@@ -25571,8 +25689,7 @@ function renderCIIntegrityReport(report) {
|
|
|
25571
25689
|
}
|
|
25572
25690
|
return lines.join("\n").trimEnd();
|
|
25573
25691
|
}
|
|
25574
|
-
|
|
25575
|
-
//#region src/commands/milestone.ts
|
|
25692
|
+
// src/commands/milestone.ts
|
|
25576
25693
|
async function runMilestoneScan(args, io, readSources, strict = false) {
|
|
25577
25694
|
const target = resolve(args.target);
|
|
25578
25695
|
const valid = validateTarget(target, io);
|
|
@@ -25804,8 +25921,7 @@ async function runCIIntegrityCommand(argv, io = {
|
|
|
25804
25921
|
return milestoneError(error, io, argv.includes("--debug"));
|
|
25805
25922
|
}
|
|
25806
25923
|
}
|
|
25807
|
-
|
|
25808
|
-
//#region src/scorer/scoring-policy.ts
|
|
25924
|
+
// src/scorer/scoring-policy.ts
|
|
25809
25925
|
/**
|
|
25810
25926
|
* `mjolnir scan --policy` — the scoring policy, as the code states it.
|
|
25811
25927
|
*
|
|
@@ -25867,8 +25983,7 @@ function renderScoringPolicy() {
|
|
|
25867
25983
|
lines.push("Every number above is imported from src/scorer/scorer.ts at run time. A", "policy that cannot be read cannot be checked, and an uncheckable policy", "is the thing this product exists to catch.");
|
|
25868
25984
|
return lines.join("\n");
|
|
25869
25985
|
}
|
|
25870
|
-
|
|
25871
|
-
//#region src/commands/fix.ts
|
|
25986
|
+
// src/commands/fix.ts
|
|
25872
25987
|
/**
|
|
25873
25988
|
* `mjolnir fix` — Safe Auto-Fix with Proof (Tier 1 #3).
|
|
25874
25989
|
*
|
|
@@ -26292,8 +26407,7 @@ function renderFixReport(results, dryRun) {
|
|
|
26292
26407
|
lines.push(`${parts.join(" · ")}${dryRun ? " (dry-run)" : ""}. Every applied fix was verified against the rule that flagged it.`);
|
|
26293
26408
|
return lines.join("\n");
|
|
26294
26409
|
}
|
|
26295
|
-
|
|
26296
|
-
//#region src/commands/rules-catalog.ts
|
|
26410
|
+
// src/commands/rules-catalog.ts
|
|
26297
26411
|
/**
|
|
26298
26412
|
* `mjolnir explain --list` — machine-readable rule catalog with Trust Metadata.
|
|
26299
26413
|
*
|
|
@@ -26510,8 +26624,7 @@ function countCompleteQuads(ruleIds, root = process.cwd()) {
|
|
|
26510
26624
|
total: ruleIds.length
|
|
26511
26625
|
};
|
|
26512
26626
|
}
|
|
26513
|
-
|
|
26514
|
-
//#region src/frameworks/provider-capability-contract.ts
|
|
26627
|
+
// src/frameworks/provider-capability-contract.ts
|
|
26515
26628
|
const CI_PROVIDER_CAPABILITY_SCHEMA = "m30.ci-provider-capabilities@1";
|
|
26516
26629
|
const CI_PROVIDER_IDS = [
|
|
26517
26630
|
"github-actions",
|
|
@@ -26757,8 +26870,7 @@ const CI_PROVIDER_CAPABILITY_RECORDS = [
|
|
|
26757
26870
|
}
|
|
26758
26871
|
];
|
|
26759
26872
|
new Map(CI_PROVIDER_CAPABILITY_RECORDS.map((record) => [record.provider, record]));
|
|
26760
|
-
|
|
26761
|
-
//#region src/v6/maturity.ts
|
|
26873
|
+
// src/v6/maturity.ts
|
|
26762
26874
|
/**
|
|
26763
26875
|
* v6 capability maturity ladder — `M0`–`M5` (ADR 0001).
|
|
26764
26876
|
*
|
|
@@ -26957,8 +27069,7 @@ function nextLevelGapFromEvidence(evidence, owner, revisitTrigger) {
|
|
|
26957
27069
|
question: "How much does it matter? (technical severity stays separate)"
|
|
26958
27070
|
}
|
|
26959
27071
|
].map((axis) => axis.id);
|
|
26960
|
-
|
|
26961
|
-
//#region src/v6/ecosystem-census.ts
|
|
27072
|
+
// src/v6/ecosystem-census.ts
|
|
26962
27073
|
/**
|
|
26963
27074
|
* The leading major of a version or range. `FRAMEWORK_INVENTORY`
|
|
26964
27075
|
* records *validated versions* (`1.44`, `29`) while a manifest records a
|
|
@@ -26971,8 +27082,7 @@ function versionMajor(raw) {
|
|
|
26971
27082
|
if (!match) return null;
|
|
26972
27083
|
return (match[0] ?? "").split(".")[0] ?? null;
|
|
26973
27084
|
}
|
|
26974
|
-
|
|
26975
|
-
//#region src/v6/ecosystem-probe.ts
|
|
27085
|
+
// src/v6/ecosystem-probe.ts
|
|
26976
27086
|
/**
|
|
26977
27087
|
* Filesystem-backed census evidence + the discovery probe (ADR 0010).
|
|
26978
27088
|
*
|
|
@@ -27224,8 +27334,7 @@ function corpusReposOf(root) {
|
|
|
27224
27334
|
}
|
|
27225
27335
|
});
|
|
27226
27336
|
}
|
|
27227
|
-
|
|
27228
|
-
//#region src/v6/capability-registry.ts
|
|
27337
|
+
// src/v6/capability-registry.ts
|
|
27229
27338
|
/**
|
|
27230
27339
|
|
|
27231
27340
|
}
|
|
@@ -27708,8 +27817,7 @@ function validateRegistry(registry) {
|
|
|
27708
27817
|
}
|
|
27709
27818
|
return diagnostics;
|
|
27710
27819
|
}
|
|
27711
|
-
|
|
27712
|
-
//#region src/commands/capability.ts
|
|
27820
|
+
// src/commands/capability.ts
|
|
27713
27821
|
/** Parse the flags. Anything flag-shaped that is not documented is a
|
|
27714
27822
|
* usage error rather than a silently ignored argument, because
|
|
27715
27823
|
* `capability --promote-m3` must not read as "show me everything". */
|
|
@@ -27829,8 +27937,7 @@ function runCapabilityCommand(argv, io = {
|
|
|
27829
27937
|
io.out(json ? capabilityJson(entries, diagnostics) : `${renderCapabilityText(entries, diagnostics)}\n`);
|
|
27830
27938
|
return diagnostics.length > 0 ? 1 : 0;
|
|
27831
27939
|
}
|
|
27832
|
-
|
|
27833
|
-
//#region src/rules/measurement-status.ts
|
|
27940
|
+
// src/rules/measurement-status.ts
|
|
27834
27941
|
/**
|
|
27835
27942
|
* Derive the measurement status for a single rule.
|
|
27836
27943
|
*
|
|
@@ -27877,8 +27984,7 @@ function getMeasurementStatus(rules = RULES) {
|
|
|
27877
27984
|
};
|
|
27878
27985
|
});
|
|
27879
27986
|
}
|
|
27880
|
-
|
|
27881
|
-
//#region src/commands/rule-health.ts
|
|
27987
|
+
// src/commands/rule-health.ts
|
|
27882
27988
|
/**
|
|
27883
27989
|
* Build the per-rule health table from the shipped registry. Deterministic
|
|
27884
27990
|
* order: the RULES registry order (stable across runs).
|
|
@@ -28009,8 +28115,7 @@ function renderRuleHealth(rows, limit) {
|
|
|
28009
28115
|
return lines.join("\n");
|
|
28010
28116
|
}
|
|
28011
28117
|
Object.keys(MEASURED_FP).length;
|
|
28012
|
-
|
|
28013
|
-
//#region src/playwright/selector-health-types.ts
|
|
28118
|
+
// src/playwright/selector-health-types.ts
|
|
28014
28119
|
/** Risk weights per locator shape (0 = resilient, higher = brittle). */
|
|
28015
28120
|
const LOCATOR_RISK = {
|
|
28016
28121
|
"role-based": 0,
|
|
@@ -28019,8 +28124,7 @@ const LOCATOR_RISK = {
|
|
|
28019
28124
|
"css-chain": 60,
|
|
28020
28125
|
xpath: 90
|
|
28021
28126
|
};
|
|
28022
|
-
|
|
28023
|
-
//#region src/playwright/selector-health.ts
|
|
28127
|
+
// src/playwright/selector-health.ts
|
|
28024
28128
|
/**
|
|
28025
28129
|
* Selector Health Score engine (Upgrade-Plan-v2 R3, the headline feature).
|
|
28026
28130
|
*
|
|
@@ -28148,8 +28252,7 @@ function computeSelectorHealth(root, ignoreMatcher = DEFAULT_IGNORE_MATCHER) {
|
|
|
28148
28252
|
walk(root);
|
|
28149
28253
|
return specs.sort((a, b) => a.score - b.score || compareCodePoints(a.file, b.file));
|
|
28150
28254
|
}
|
|
28151
|
-
|
|
28152
|
-
//#region src/cli-handlers.ts
|
|
28255
|
+
// src/cli-handlers.ts
|
|
28153
28256
|
/**
|
|
28154
28257
|
* Handler functions for every Mjolnir CLI verb.
|
|
28155
28258
|
*
|
|
@@ -28158,6 +28261,35 @@ function computeSelectorHealth(root, ignoreMatcher = DEFAULT_IGNORE_MATCHER) {
|
|
|
28158
28261
|
* (Task 6: renderScanOutput helper).
|
|
28159
28262
|
*/
|
|
28160
28263
|
/**
|
|
28264
|
+
* The one catch for a scan-backed verb.
|
|
28265
|
+
*
|
|
28266
|
+
* An invalid `mjolnir.config.json` is the USER's error and exits 10 (usage),
|
|
28267
|
+
* as `scan` always did. `fix`, `ci verify` and `explain --plan` each had
|
|
28268
|
+
* their own catch that only knew the internal-error arm, so the same config
|
|
28269
|
+
* typo exited 20 and printed "this is a bug in Mjölnir" on three verbs.
|
|
28270
|
+
*/
|
|
28271
|
+
function verbFailure(error, io, debug) {
|
|
28272
|
+
if (error instanceof ConfigValidationError) {
|
|
28273
|
+
io.err(error.message);
|
|
28274
|
+
return 10;
|
|
28275
|
+
}
|
|
28276
|
+
internalErrorMessage(error, io.err, debug);
|
|
28277
|
+
return 20;
|
|
28278
|
+
}
|
|
28279
|
+
/**
|
|
28280
|
+
* The notices a scan reports about itself, sent to the verb's own stderr.
|
|
28281
|
+
*
|
|
28282
|
+
* `fix`, `ci verify` and `handover` called `runScan` with no hooks, so a
|
|
28283
|
+
* config warning (an unknown key, an expired suppression) never reached the
|
|
28284
|
+
* user on those verbs, and the plugin gate notice bypassed `io.err`.
|
|
28285
|
+
*/
|
|
28286
|
+
function scanNotices(io) {
|
|
28287
|
+
return {
|
|
28288
|
+
onConfigWarning: (message) => io.err(message),
|
|
28289
|
+
onGateNotice: (notice) => io.err(notice)
|
|
28290
|
+
};
|
|
28291
|
+
}
|
|
28292
|
+
/**
|
|
28161
28293
|
* The one place a run's gate level is resolved from `--blocking`, then
|
|
28162
28294
|
* `mjolnir.config.json`, then the shipped default.
|
|
28163
28295
|
*
|
|
@@ -28207,9 +28339,11 @@ function renderScanOutput(result, args, target, io, gate) {
|
|
|
28207
28339
|
else if (args.format === "pr-comment") {
|
|
28208
28340
|
const baseline = loadBaseline(join(target, DEFAULT_BASELINE_PATH));
|
|
28209
28341
|
const diff = baseline ? diffAgainstBaseline(result, baseline) : void 0;
|
|
28342
|
+
const commit = commitBoundTo(result, target);
|
|
28210
28343
|
io.out(renderPrComment(result, {
|
|
28211
28344
|
...diff ? { diff } : {},
|
|
28212
|
-
version: ENGINE_VERSION
|
|
28345
|
+
version: ENGINE_VERSION,
|
|
28346
|
+
...commit !== void 0 ? { commit } : {}
|
|
28213
28347
|
}));
|
|
28214
28348
|
} else if (args.format === "github-summary") io.out(renderStepSummary(result));
|
|
28215
28349
|
else if (args.json) io.out(JSON.stringify({
|
|
@@ -28298,6 +28432,58 @@ async function runSuppressionGate(args, target, io) {
|
|
|
28298
28432
|
if (args.base !== void 0) argv.push("--base", args.base);
|
|
28299
28433
|
return runSuppressionGateCommand(argv, io);
|
|
28300
28434
|
}
|
|
28435
|
+
/**
|
|
28436
|
+
* The commit a rendered report may claim, or undefined when it cannot claim one.
|
|
28437
|
+
*
|
|
28438
|
+
* The scan records the HEAD it analysed (`runIdentity.commit`). A report states
|
|
28439
|
+
* that commit only when the repository at `dir` is still at it, so a report
|
|
28440
|
+
* rendered from another commit's JSON reads `unknown` instead of borrowing an
|
|
28441
|
+
* identity nothing here can confirm. The retired `pr-comment --from` verb took
|
|
28442
|
+
* the commit from a `--commit` argument; the formats that replaced it check the
|
|
28443
|
+
* checkout instead, which needs no flag and cannot be handed the wrong value.
|
|
28444
|
+
*/
|
|
28445
|
+
function commitBoundTo(result, dir) {
|
|
28446
|
+
const recorded = result.runIdentity?.commit;
|
|
28447
|
+
if (recorded === void 0) return void 0;
|
|
28448
|
+
return currentCommit(dir, { nullable: true }) === recorded ? recorded : void 0;
|
|
28449
|
+
}
|
|
28450
|
+
/**
|
|
28451
|
+
* `--format github-summary|pr-comment <report.json>`: render a SAVED report.
|
|
28452
|
+
*
|
|
28453
|
+
* The v6 collapse retired `summary <report>` and `pr-comment --from <report>`
|
|
28454
|
+
* onto these two formats and shipped only the half that scans. Every CI
|
|
28455
|
+
* consumer renders the `--json` report its scan step already wrote: the
|
|
28456
|
+
* Action, both `ci install` templates and this repository's own PR workflow.
|
|
28457
|
+
* All of them kept calling the retired verbs, and all of them exited 10. A
|
|
28458
|
+
* second scan is not a substitute, because the comment has to describe the
|
|
28459
|
+
* result the gate read rather than one that happens to agree with it.
|
|
28460
|
+
*
|
|
28461
|
+
* Delegation, not reimplementation, which is the rule `--suppression-gate`
|
|
28462
|
+
* follows. Each retired verb's command still owns its contract, exit codes
|
|
28463
|
+
* included. The render never gates and exits 0. A report that cannot be read
|
|
28464
|
+
* exits 2 for the summary and 10 for the comment, as those verbs did.
|
|
28465
|
+
*
|
|
28466
|
+
* Returns null when the request is a scan: another format, or a target that
|
|
28467
|
+
* is not a file.
|
|
28468
|
+
*/
|
|
28469
|
+
function renderSavedReport(args, target, io) {
|
|
28470
|
+
if (args.format !== "github-summary" && args.format !== "pr-comment") return null;
|
|
28471
|
+
if (!isRegularFile(target)) return null;
|
|
28472
|
+
if (args.format === "github-summary") return runSummaryCommand([target], io);
|
|
28473
|
+
return runPrCommentCommand([
|
|
28474
|
+
"--from",
|
|
28475
|
+
target,
|
|
28476
|
+
"--version",
|
|
28477
|
+
ENGINE_VERSION
|
|
28478
|
+
], io);
|
|
28479
|
+
}
|
|
28480
|
+
function isRegularFile(path) {
|
|
28481
|
+
try {
|
|
28482
|
+
return statSync(path).isFile();
|
|
28483
|
+
} catch {
|
|
28484
|
+
return false;
|
|
28485
|
+
}
|
|
28486
|
+
}
|
|
28301
28487
|
async function runScanCommand(argv, io = {
|
|
28302
28488
|
out,
|
|
28303
28489
|
err
|
|
@@ -28305,6 +28491,8 @@ async function runScanCommand(argv, io = {
|
|
|
28305
28491
|
const args = parseArgsOrUsage(argv, io);
|
|
28306
28492
|
if (!args) return 10;
|
|
28307
28493
|
const target = resolve(args.target);
|
|
28494
|
+
const savedRender = renderSavedReport(args, target, io);
|
|
28495
|
+
if (savedRender !== null) return savedRender;
|
|
28308
28496
|
const invalid = validateScanTarget(target, io.err);
|
|
28309
28497
|
if (invalid !== null) return invalid;
|
|
28310
28498
|
if (args.suppressions) return printSuppressionLedger(target, io);
|
|
@@ -28325,24 +28513,26 @@ async function runScanCommand(argv, io = {
|
|
|
28325
28513
|
...args,
|
|
28326
28514
|
target
|
|
28327
28515
|
}, {
|
|
28328
|
-
|
|
28516
|
+
...scanNotices(io),
|
|
28329
28517
|
onProgress: (e) => progress.onEvent(e),
|
|
28330
|
-
onGateNotice: (notice) => io.err(notice),
|
|
28331
28518
|
...args.debug ? { onRuleCrash: (ruleId, file, error) => {
|
|
28332
28519
|
const detail = error instanceof Error ? [error.message, error.stack].filter((value) => Boolean(value)).join("\n") : String(error);
|
|
28333
28520
|
crashLog.push(`${ruleId} crashed on ${file}: ${detail}`);
|
|
28334
28521
|
} } : {}
|
|
28335
28522
|
});
|
|
28336
28523
|
progress.done();
|
|
28524
|
+
let baselineFailed = false;
|
|
28337
28525
|
if (args.saveBaseline) {
|
|
28338
28526
|
const outPath = join(target, DEFAULT_BASELINE_PATH);
|
|
28339
|
-
|
|
28527
|
+
const notice = args.format === "terminal" ? io.out : io.err;
|
|
28528
|
+
if (result.partial) io.err("baseline NOT saved — this scan is partial, and a truncated before-state would report every finding it missed as RESOLVED. Re-run without the time budget.");
|
|
28529
|
+
else try {
|
|
28340
28530
|
const saved = saveBaseline(result, currentCommit(target) ?? "unknown", outPath);
|
|
28341
|
-
|
|
28531
|
+
notice(renderBaselineSaved(DEFAULT_BASELINE_PATH, result.findings.length, { ...saved.backupPath !== void 0 ? { backupPath: saved.backupPath } : {} }));
|
|
28342
28532
|
} catch (saveErr) {
|
|
28343
28533
|
io.err(`baseline save FAILED — ${saveErr instanceof Error ? saveErr.message : String(saveErr)}`);
|
|
28344
28534
|
io.err("The scan completed; the snapshot was not written. Fix the path permissions and re-run with `--save-baseline`.");
|
|
28345
|
-
|
|
28535
|
+
baselineFailed = true;
|
|
28346
28536
|
}
|
|
28347
28537
|
}
|
|
28348
28538
|
if (args.debug && crashLog.length > 0) {
|
|
@@ -28350,7 +28540,7 @@ async function runScanCommand(argv, io = {
|
|
|
28350
28540
|
for (const line of crashLog.slice(0, 50)) io.err(` ${line}`);
|
|
28351
28541
|
if (crashLog.length > 50) io.err(` … and ${crashLog.length - 50} more`);
|
|
28352
28542
|
}
|
|
28353
|
-
const { config
|
|
28543
|
+
const { config } = loadConfig(target, { knownRuleIds: KNOWN_RULE_IDS$1 });
|
|
28354
28544
|
for (const d of args.deprecatedFlags ?? []) io.err(`warning: ${d.flag} is deprecated and will be removed in 7.0 — use ${d.replacement}. It still does exactly what it did; nothing about your scan changes this release.`);
|
|
28355
28545
|
const gate = resolveGateLevel(args.blocking, config);
|
|
28356
28546
|
if (args.scoreOnly) {
|
|
@@ -28377,7 +28567,7 @@ async function runScanCommand(argv, io = {
|
|
|
28377
28567
|
}
|
|
28378
28568
|
}
|
|
28379
28569
|
if (result.partial) return 2;
|
|
28380
|
-
|
|
28570
|
+
if (baselineFailed) return 2;
|
|
28381
28571
|
return scanExitCode({
|
|
28382
28572
|
partial: false,
|
|
28383
28573
|
findings: result.findings,
|
|
@@ -28387,12 +28577,7 @@ async function runScanCommand(argv, io = {
|
|
|
28387
28577
|
...args.requireFullCoverage !== void 0 ? { requireFullCoverage: args.requireFullCoverage } : {}
|
|
28388
28578
|
});
|
|
28389
28579
|
} catch (err) {
|
|
28390
|
-
|
|
28391
|
-
io.err(err.message);
|
|
28392
|
-
return 10;
|
|
28393
|
-
}
|
|
28394
|
-
internalErrorMessage(err, io.err, args?.debug === true);
|
|
28395
|
-
return 20;
|
|
28580
|
+
return verbFailure(err, io, args?.debug === true);
|
|
28396
28581
|
}
|
|
28397
28582
|
}
|
|
28398
28583
|
async function runFixCommand(argv, io = {
|
|
@@ -28406,16 +28591,17 @@ async function runFixCommand(argv, io = {
|
|
|
28406
28591
|
const target = resolve(args.target);
|
|
28407
28592
|
const invalid = validateScanTarget(target, io.err);
|
|
28408
28593
|
if (invalid !== null) return invalid;
|
|
28409
|
-
const
|
|
28594
|
+
const result = await runScan$1({
|
|
28410
28595
|
...args,
|
|
28411
28596
|
target,
|
|
28412
28597
|
strict: true
|
|
28413
|
-
}
|
|
28598
|
+
}, scanNotices(io));
|
|
28599
|
+
const fixes = planAndApplyFixes(result, target, { dryRun });
|
|
28414
28600
|
io.out(renderFixReport(fixes, dryRun));
|
|
28601
|
+
if (result.partial) return 2;
|
|
28415
28602
|
return fixes.some((f) => f.status === "failed") ? 1 : 0;
|
|
28416
28603
|
} catch (err) {
|
|
28417
|
-
|
|
28418
|
-
return 20;
|
|
28604
|
+
return verbFailure(err, io, args?.debug === true);
|
|
28419
28605
|
}
|
|
28420
28606
|
}
|
|
28421
28607
|
async function runVerifyCommand(argv, io = {
|
|
@@ -28431,15 +28617,21 @@ async function runVerifyCommand(argv, io = {
|
|
|
28431
28617
|
const result = await runScan$1({
|
|
28432
28618
|
...args,
|
|
28433
28619
|
target
|
|
28434
|
-
});
|
|
28435
|
-
const
|
|
28620
|
+
}, scanNotices(io));
|
|
28621
|
+
const baseline = loadBaseline(join(target, DEFAULT_BASELINE_PATH), (w) => io.err(w));
|
|
28622
|
+
const digest = buildVerifyDigest(result, baseline);
|
|
28436
28623
|
io.out(renderVerifyDigest(digest));
|
|
28437
28624
|
if (result.partial) return 2;
|
|
28438
28625
|
if (!digest.hasBaseline) return 2;
|
|
28439
|
-
|
|
28626
|
+
const { config } = loadConfig(target, { knownRuleIds: KNOWN_RULE_IDS$1 });
|
|
28627
|
+
return scanExitCode({
|
|
28628
|
+
partial: false,
|
|
28629
|
+
findings: diffAgainstBaseline(result, baseline).newFindings,
|
|
28630
|
+
gate: resolveGateLevel(args.blocking, config),
|
|
28631
|
+
isAdvisory: isAdvisoryFinding
|
|
28632
|
+
});
|
|
28440
28633
|
} catch (err) {
|
|
28441
|
-
|
|
28442
|
-
return 20;
|
|
28634
|
+
return verbFailure(err, io, args?.debug === true);
|
|
28443
28635
|
}
|
|
28444
28636
|
}
|
|
28445
28637
|
async function runPrCommentCommand(argv, io = {
|
|
@@ -28454,13 +28646,14 @@ async function runPrCommentCommand(argv, io = {
|
|
|
28454
28646
|
return 10;
|
|
28455
28647
|
}
|
|
28456
28648
|
try {
|
|
28457
|
-
const
|
|
28649
|
+
const reportPath = resolve(fromPath);
|
|
28650
|
+
const saved = loadSavedReport(reportPath);
|
|
28458
28651
|
const option = (flag) => {
|
|
28459
28652
|
const index = argv.indexOf(flag);
|
|
28460
28653
|
return index >= 0 ? argv[index + 1] : void 0;
|
|
28461
28654
|
};
|
|
28462
28655
|
const version = option("--version");
|
|
28463
|
-
const commit = option("--commit");
|
|
28656
|
+
const commit = option("--commit") ?? commitBoundTo(saved, dirname(reportPath));
|
|
28464
28657
|
const repoUrl = option("--repo-url");
|
|
28465
28658
|
io.out(renderPrComment(saved, {
|
|
28466
28659
|
...version ? { version } : {},
|
|
@@ -28523,17 +28716,21 @@ async function runHandoverCommand(argv, io = {
|
|
|
28523
28716
|
const result = await runScan$1({
|
|
28524
28717
|
...args,
|
|
28525
28718
|
target
|
|
28526
|
-
});
|
|
28719
|
+
}, scanNotices(io));
|
|
28527
28720
|
let forensics = null;
|
|
28721
|
+
let forensicsFailed = false;
|
|
28528
28722
|
const resultsDir = join(target, "test-results");
|
|
28529
28723
|
if (existsSync(resultsDir)) try {
|
|
28530
28724
|
forensics = runForensics(resultsDir, { writeFlakyMd: false }).report;
|
|
28531
|
-
} catch {
|
|
28725
|
+
} catch (forensicsErr) {
|
|
28726
|
+
io.err(`warning: test-results/ could not be read (${errorMessage(forensicsErr)}) — the map below is static-only.`);
|
|
28727
|
+
forensicsFailed = true;
|
|
28728
|
+
}
|
|
28532
28729
|
io.out(renderHandover(buildHandover(result, forensics)));
|
|
28730
|
+
if (result.partial || forensicsFailed) return 2;
|
|
28533
28731
|
return 0;
|
|
28534
28732
|
} catch (err) {
|
|
28535
|
-
|
|
28536
|
-
return 20;
|
|
28733
|
+
return verbFailure(err, io, args?.debug === true);
|
|
28537
28734
|
}
|
|
28538
28735
|
}
|
|
28539
28736
|
function runPwReportCommand(argv, io = {
|
|
@@ -28667,7 +28864,7 @@ function runEvidenceArm(argv, io) {
|
|
|
28667
28864
|
const { report } = runForensics(resolve(target), { writeFlakyMd });
|
|
28668
28865
|
if (asJson) io.out(JSON.stringify(report, null, 2));
|
|
28669
28866
|
else io.out(asMd ? renderTriageMd(report) : renderTriage(report));
|
|
28670
|
-
return report.analysisComplete === false ?
|
|
28867
|
+
return report.analysisComplete === false ? 2 : 0;
|
|
28671
28868
|
} catch (err) {
|
|
28672
28869
|
internalErrorMessage(err, io.err, argv.includes("--debug"));
|
|
28673
28870
|
return 20;
|
|
@@ -28820,8 +29017,7 @@ async function runDoctorPlaywright(argv, io = { out }) {
|
|
|
28820
29017
|
return 20;
|
|
28821
29018
|
}
|
|
28822
29019
|
}
|
|
28823
|
-
|
|
28824
|
-
//#region src/rules/rule-metadata-schema.ts
|
|
29020
|
+
// src/rules/rule-metadata-schema.ts
|
|
28825
29021
|
const VALID_SEVERITIES = SEVERITY_ORDER$1;
|
|
28826
29022
|
const VALID_CATEGORIES = RULE_CATEGORIES;
|
|
28827
29023
|
const VALID_QA_IMPACTS = Object.keys(QA_IMPACT_LABELS);
|
|
@@ -28869,8 +29065,7 @@ function validateAllRulesMetadata(rules) {
|
|
|
28869
29065
|
for (const rule of rules) all.push(...validateRuleMetadata(rule));
|
|
28870
29066
|
return all;
|
|
28871
29067
|
}
|
|
28872
|
-
|
|
28873
|
-
//#region src/engine/detector-hash.ts
|
|
29068
|
+
// src/engine/detector-hash.ts
|
|
28874
29069
|
/**
|
|
28875
29070
|
* Detector revision integrity engine (certification-audit Phase 3,
|
|
28876
29071
|
* G4/D8v2/D17).
|
|
@@ -29138,8 +29333,7 @@ const RULE_ID_RE = new RegExp(`^QA-(?:${[
|
|
|
29138
29333
|
appliesTo: "test-files"
|
|
29139
29334
|
}
|
|
29140
29335
|
].map((f) => f.token).join("|")})-\\d{3}$`);
|
|
29141
|
-
|
|
29142
|
-
//#region src/commands/doctor.ts
|
|
29336
|
+
// src/commands/doctor.ts
|
|
29143
29337
|
/**
|
|
29144
29338
|
* `mjolnir doctor` — self-audit of Mjolnir's own rule base.
|
|
29145
29339
|
*
|
|
@@ -30086,8 +30280,7 @@ function stripNonDeterministicFields(json, fields = NON_DETERMINISTIC_FIELDS) {
|
|
|
30086
30280
|
for (const field of fields) delete json[field];
|
|
30087
30281
|
return json;
|
|
30088
30282
|
}
|
|
30089
|
-
|
|
30090
|
-
//#region src/commands/doctor-run.ts
|
|
30283
|
+
// src/commands/doctor-run.ts
|
|
30091
30284
|
/**
|
|
30092
30285
|
* Doctor CLI entry (certification-audit Phase 5, G6): the command handler
|
|
30093
30286
|
* lives beside the check implementations instead of in cli.ts — the CLI
|
|
@@ -30185,8 +30378,7 @@ function installedPackageRoot() {
|
|
|
30185
30378
|
return process.cwd();
|
|
30186
30379
|
}
|
|
30187
30380
|
}
|
|
30188
|
-
|
|
30189
|
-
//#region src/commands/handoff.ts
|
|
30381
|
+
// src/commands/handoff.ts
|
|
30190
30382
|
/**
|
|
30191
30383
|
* `mjolnir handoff [mjolnir.json]` — the deterministic fix-handoff
|
|
30192
30384
|
* artifact (agent-handoff plan M3).
|
|
@@ -30272,7 +30464,7 @@ function verificationBlock(version) {
|
|
|
30272
30464
|
/** The per-rule remediation copy block (fenced, self-contained). */
|
|
30273
30465
|
function ruleCopyBlock(group, version) {
|
|
30274
30466
|
const f = group.findings[0];
|
|
30275
|
-
const occurrences = group.findings.slice(0, OCCURRENCE_CAP).map((x) => `-
|
|
30467
|
+
const occurrences = group.findings.slice(0, OCCURRENCE_CAP).map((x) => `- ${mdCodeSpan(`${x.file}:${x.line}`)}`).join("\n");
|
|
30276
30468
|
const overflow = group.findings.length > OCCURRENCE_CAP ? `\n- … and ${group.findings.length - OCCURRENCE_CAP} more — see the JSON report.` : "";
|
|
30277
30469
|
return [
|
|
30278
30470
|
"```text",
|
|
@@ -30398,7 +30590,7 @@ function renderHandoff(result, options = {}, version = ENGINE_VERSION) {
|
|
|
30398
30590
|
lines.push("");
|
|
30399
30591
|
lines.push(`**Occurrences (${g.findings.length}):**`);
|
|
30400
30592
|
lines.push("");
|
|
30401
|
-
for (const f of g.findings.slice(0, OCCURRENCE_CAP)) lines.push(`-
|
|
30593
|
+
for (const f of g.findings.slice(0, OCCURRENCE_CAP)) lines.push(`- ${mdCodeSpan(`${f.file}:${f.line}`)} — ${escapeMarkdown(f.message)}`);
|
|
30402
30594
|
if (g.findings.length > OCCURRENCE_CAP) lines.push(`- … and ${g.findings.length - OCCURRENCE_CAP} more — see the JSON report.`);
|
|
30403
30595
|
lines.push("");
|
|
30404
30596
|
lines.push(ruleCopyBlock(g, version));
|
|
@@ -30496,8 +30688,7 @@ function runHandoffCommand(argv, io = {
|
|
|
30496
30688
|
}));
|
|
30497
30689
|
return 0;
|
|
30498
30690
|
}
|
|
30499
|
-
|
|
30500
|
-
//#region src/commands/install-agents.ts
|
|
30691
|
+
// src/commands/install-agents.ts
|
|
30501
30692
|
/**
|
|
30502
30693
|
* `mjolnir install` — instruction-surface installer (agent-handoff
|
|
30503
30694
|
* plan M4).
|
|
@@ -30823,8 +31014,7 @@ function executeHookInstall(entry) {
|
|
|
30823
31014
|
default: return false;
|
|
30824
31015
|
}
|
|
30825
31016
|
}
|
|
30826
|
-
|
|
30827
|
-
//#region src/commands/release-trust.ts
|
|
31017
|
+
// src/commands/release-trust.ts
|
|
30828
31018
|
/**
|
|
30829
31019
|
* `mjolnir ci release-trust` — the Release Trust Verdict (product-gap
|
|
30830
31020
|
* master plan §5, plan 1789009691197 R4a).
|
|
@@ -31535,8 +31725,7 @@ function runReleaseTrustCommand(argv, io = {
|
|
|
31535
31725
|
return 20;
|
|
31536
31726
|
}
|
|
31537
31727
|
}
|
|
31538
|
-
|
|
31539
|
-
//#region src/commands/policy.ts
|
|
31728
|
+
// src/commands/policy.ts
|
|
31540
31729
|
/**
|
|
31541
31730
|
* `mjolnir policy` — Policy-as-Code (TL-2).
|
|
31542
31731
|
*
|
|
@@ -31660,8 +31849,7 @@ async function runPolicyCommand(argv, io) {
|
|
|
31660
31849
|
io.err(`Unknown policy subcommand: ${subcommand}`);
|
|
31661
31850
|
return 10;
|
|
31662
31851
|
}
|
|
31663
|
-
|
|
31664
|
-
//#region src/commands/analyze.ts
|
|
31852
|
+
// src/commands/analyze.ts
|
|
31665
31853
|
/**
|
|
31666
31854
|
* `mjolnir analyze --cross-file` — Cross-File Analysis (SDET-7).
|
|
31667
31855
|
*
|
|
@@ -31797,8 +31985,7 @@ function runAnalyzeCommand(argv, io) {
|
|
|
31797
31985
|
return 20;
|
|
31798
31986
|
}
|
|
31799
31987
|
}
|
|
31800
|
-
|
|
31801
|
-
//#region src/commands/ci-adapter.ts
|
|
31988
|
+
// src/commands/ci-adapter.ts
|
|
31802
31989
|
/**
|
|
31803
31990
|
* `mjolnir ci adapters` — CI Adapters (SDET-4).
|
|
31804
31991
|
*
|
|
@@ -31919,8 +32106,7 @@ function runCiAdapterCommand(argv, io) {
|
|
|
31919
32106
|
io.out(output);
|
|
31920
32107
|
return 0;
|
|
31921
32108
|
}
|
|
31922
|
-
|
|
31923
|
-
//#region src/engine/machine-contract-verification.ts
|
|
32109
|
+
// src/engine/machine-contract-verification.ts
|
|
31924
32110
|
/**
|
|
31925
32111
|
* M11 — Machine Contract Verification.
|
|
31926
32112
|
*
|
|
@@ -32082,8 +32268,7 @@ function renderContractVerification(result) {
|
|
|
32082
32268
|
}
|
|
32083
32269
|
return lines.join("\n").trimEnd();
|
|
32084
32270
|
}
|
|
32085
|
-
|
|
32086
|
-
//#region src/commands/contract-verify.ts
|
|
32271
|
+
// src/commands/contract-verify.ts
|
|
32087
32272
|
/**
|
|
32088
32273
|
* `contract-verify` — the machine-contract and exit-code validators, and the
|
|
32089
32274
|
* command that runs them.
|
|
@@ -32240,8 +32425,7 @@ async function runContractVerifyCommand(argv, io = {
|
|
|
32240
32425
|
return milestoneError(error, io, argv.includes("--debug"));
|
|
32241
32426
|
}
|
|
32242
32427
|
}
|
|
32243
|
-
|
|
32244
|
-
//#region src/commands/share.ts
|
|
32428
|
+
// src/commands/share.ts
|
|
32245
32429
|
/**
|
|
32246
32430
|
* `mjolnir share` — React Doctor's proof loop, without its server.
|
|
32247
32431
|
*
|
|
@@ -32471,8 +32655,7 @@ async function runShareCommand(argv, io) {
|
|
|
32471
32655
|
return 20;
|
|
32472
32656
|
}
|
|
32473
32657
|
}
|
|
32474
|
-
|
|
32475
|
-
//#region src/engine/historical-trust.ts
|
|
32658
|
+
// src/engine/historical-trust.ts
|
|
32476
32659
|
/**
|
|
32477
32660
|
* Compute a trust snapshot from a scan result and run identity.
|
|
32478
32661
|
* Pure function — deterministic output for the same input.
|
|
@@ -32658,8 +32841,7 @@ function renderTrustTrend(trend) {
|
|
|
32658
32841
|
}
|
|
32659
32842
|
return lines.join("\n").trimEnd();
|
|
32660
32843
|
}
|
|
32661
|
-
|
|
32662
|
-
//#region src/commands/release-trend.ts
|
|
32844
|
+
// src/commands/release-trend.ts
|
|
32663
32845
|
/**
|
|
32664
32846
|
* `ci release-trend` — how the signed release's trust record has moved across
|
|
32665
32847
|
* releases.
|
|
@@ -32716,8 +32898,7 @@ async function runTrustTrendCommand(argv, io = {
|
|
|
32716
32898
|
return milestoneError(error, io, argv.includes("--debug"));
|
|
32717
32899
|
}
|
|
32718
32900
|
}
|
|
32719
|
-
|
|
32720
|
-
//#region src/cli.ts
|
|
32901
|
+
// src/cli.ts
|
|
32721
32902
|
/**
|
|
32722
32903
|
* Mjölnir CLI entry point (W1-02).
|
|
32723
32904
|
* Exit codes (§24.1, frozen): 0 clean · 1 findings ≥ gate · 2 partial ·
|
|
@@ -32730,16 +32911,14 @@ async function runTrustTrendCommand(argv, io = {
|
|
|
32730
32911
|
* bundled `dist/cli.mjs`, so resolving package.json at runtime depends on
|
|
32731
32912
|
* where the file happens to sit after install. This follows the same
|
|
32732
32913
|
* discipline as SARIF's `driver.version` — kept in sync by
|
|
32733
|
-
* `
|
|
32734
|
-
*
|
|
32914
|
+
* `npm run version:surface:sync` (it rewrites every identity surface) on
|
|
32915
|
+
* release and guarded by
|
|
32735
32916
|
* `tests/contract/version-consistency.spec.ts` locally. R4c moved the literal to
|
|
32736
32917
|
* src/engine/version.ts (a leaf module) so the scan pipeline's run
|
|
32737
32918
|
* identity can carry it without a cli.ts import cycle; this re-export
|
|
32738
32919
|
* keeps every existing consumer stable.
|
|
32739
32920
|
*/
|
|
32740
|
-
const { runScan, buildUniversalRules, fallbackWorkspace, pathMatchesGlob, isValidFindingRecord, discoverAndParseRuntimeReport, KNOWN_RULE_IDS, OVERLAP_META_BY_RULE_ID, EVIDENCE_OVERRIDES, SUITE_INVALIDATING_RULE_IDS } = scan_pipeline_exports;
|
|
32741
|
-
const DEFAULT_MAX_DURATION_MS = 6e5;
|
|
32742
|
-
const MAX_DURATION_MS = 36e5;
|
|
32921
|
+
const { runScan, buildUniversalRules, fallbackWorkspace, pathMatchesGlob, isValidFindingRecord, discoverAndParseRuntimeReport, KNOWN_RULE_IDS, OVERLAP_META_BY_RULE_ID, EVIDENCE_OVERRIDES, SUITE_INVALIDATING_RULE_IDS, DEFAULT_MAX_DURATION_MS, MAX_DURATION_MS } = scan_pipeline_exports;
|
|
32743
32922
|
function parseArgs(argv, onError) {
|
|
32744
32923
|
const args = {
|
|
32745
32924
|
target: ".",
|
|
@@ -32798,7 +32977,7 @@ function parseArgs(argv, onError) {
|
|
|
32798
32977
|
args.base = ref;
|
|
32799
32978
|
} else if (a === "--max-duration") {
|
|
32800
32979
|
const v = Number(argv[++i]);
|
|
32801
|
-
if (!Number.isFinite(v) || v <= 0 || v * 1e3 >
|
|
32980
|
+
if (!Number.isFinite(v) || v <= 0 || v * 1e3 > MAX_DURATION_MS) return reject({
|
|
32802
32981
|
flag: "--max-duration",
|
|
32803
32982
|
token: argv[i]
|
|
32804
32983
|
});
|
|
@@ -32857,11 +33036,22 @@ function parseArgs(argv, onError) {
|
|
|
32857
33036
|
}
|
|
32858
33037
|
return args;
|
|
32859
33038
|
}
|
|
32860
|
-
/**
|
|
33039
|
+
/**
|
|
33040
|
+
* Scan flags that exist — the "did you mean" candidate pool.
|
|
33041
|
+
*
|
|
33042
|
+
* Every flag `parseArgs` accepts, and nothing it rejects. The pool had
|
|
33043
|
+
* drifted both ways: ten accepted flags were missing, and `--dry-run` and
|
|
33044
|
+
* `--version` were in it although a scan rejects them, so `mjolnir scan
|
|
33045
|
+
* --dry-run` answered `unknown flag "--dry-run"` and then suggested it.
|
|
33046
|
+
* tests/cli/help.spec.ts reads `parseArgs` to hold the two lists together.
|
|
33047
|
+
*/
|
|
32861
33048
|
const KNOWN_SCAN_FLAGS = [
|
|
32862
33049
|
"--json",
|
|
32863
33050
|
"--format",
|
|
32864
33051
|
"--verbose",
|
|
33052
|
+
"--suppressions",
|
|
33053
|
+
"--suppression-gate",
|
|
33054
|
+
"--policy",
|
|
32865
33055
|
"--scope",
|
|
32866
33056
|
"--base",
|
|
32867
33057
|
"--max-duration",
|
|
@@ -32876,12 +33066,16 @@ const KNOWN_SCAN_FLAGS = [
|
|
|
32876
33066
|
"--record-milestones",
|
|
32877
33067
|
"--save-baseline",
|
|
32878
33068
|
"--cache",
|
|
33069
|
+
"--no-progress",
|
|
33070
|
+
"--category",
|
|
33071
|
+
"--score",
|
|
33072
|
+
"--require-full-coverage",
|
|
33073
|
+
"--staged",
|
|
33074
|
+
"--blocking",
|
|
33075
|
+
"--enable-plugins",
|
|
32879
33076
|
"--monorepo",
|
|
32880
33077
|
"--help",
|
|
32881
|
-
"-h"
|
|
32882
|
-
"--version",
|
|
32883
|
-
"-v",
|
|
32884
|
-
"--dry-run"
|
|
33078
|
+
"-h"
|
|
32885
33079
|
];
|
|
32886
33080
|
/** Hand-rolled Levenshtein distance (plan M2: no new dependencies). */
|
|
32887
33081
|
function levenshtein(a, b) {
|
|
@@ -33085,7 +33279,7 @@ function runCiInstall(argv, io = {
|
|
|
33085
33279
|
return 10;
|
|
33086
33280
|
}
|
|
33087
33281
|
io.out(`${result.existed ? "Updated" : "Created"} ${result.written}`);
|
|
33088
|
-
io.out(noAction ? `Plain-npx template (--no-action). Gate: ${gate}.` : `Action-based template: uses
|
|
33282
|
+
io.out(noAction ? `Plain-npx template (--no-action). Gate: ${gate}.` : `Action-based template: uses ${ACTION_REF} (immutable pin). Gate: ${gate}.`);
|
|
33089
33283
|
io.out(gate === "advisory" ? "Advisory mode reports findings without blocking. Opt in with --gate error or --gate warning." : `Blocking mode fails on ${gate} findings.`);
|
|
33090
33284
|
io.out("Change with: mjolnir ci install --gate error|warning|advisory");
|
|
33091
33285
|
if (!noAction) io.out("Prefer the plain-npx workflow? Re-run with --no-action.");
|
|
@@ -33138,13 +33332,8 @@ async function main(argv = process$1.argv.slice(2), io = {
|
|
|
33138
33332
|
io.out(renderCiSubcommandHelp());
|
|
33139
33333
|
return 10;
|
|
33140
33334
|
}
|
|
33141
|
-
if (argv[0] === "ci" && argv[1] === "install") return runCiInstall(argv.slice(2), io);
|
|
33142
33335
|
if (argv[0] === "ci") {
|
|
33143
33336
|
const sub = argv[1];
|
|
33144
|
-
if (sub === void 0 || sub === "--help" || sub === "-h") {
|
|
33145
|
-
io.out(renderCiSubcommandHelp());
|
|
33146
|
-
return sub === void 0 ? 10 : 0;
|
|
33147
|
-
}
|
|
33148
33337
|
const entry = CI_SUBCOMMANDS[sub];
|
|
33149
33338
|
if (entry === void 0) {
|
|
33150
33339
|
io.err(`mjolnir ci: unknown subcommand "${sub}".`);
|
|
@@ -33236,7 +33425,6 @@ if (isEntryPoint()) {
|
|
|
33236
33425
|
await flushSentry();
|
|
33237
33426
|
}
|
|
33238
33427
|
}
|
|
33239
|
-
|
|
33240
|
-
export { ENGINE_VERSION as CLI_VERSION, DEFAULT_MAX_DURATION_MS, EVIDENCE_OVERRIDES, EXPLAIN_ARMS, KNOWN_RULE_IDS, MAX_DURATION_MS, OVERLAP_META_BY_RULE_ID, SUITE_INVALIDATING_RULE_IDS, buildUniversalRules, discoverAndParseRuntimeReport, err, exitForFindings, fallbackWorkspace, internalErrorMessage, isEntryPoint, isValidFindingRecord, levenshtein, main, nearestFlags, out, parseArgs, parseArgsOrUsage, pathMatchesGlob, printUsage, renderCiSubcommandHelp, renderScanOutput, runAnalyzeCommand, runCIIntegrityCommand, runCiAdapterCommand, runCiInstall, runContractVerifyCommand, runCrossFileCommand, runDoctorCommand, runDoctorPlaywright, runEvidenceArm, runEvidenceGraphCommand, runExplainCommand, runFixCommand, runFrameworkMaturityCommand, runHandoffCommand, runHandoverCommand, runHelpCommand, runInstallCommand, runPolicyCommand, runPrCommentCommand, runPwReportCommand, runReleaseTrustCommand, runRulesCommand, runScan, runScanCommand, runStatsCommand, runSuppressionGateCommand, runSuppressions, runTrustTrendCommand, runVerifyCommand, runWhyCommand, usageErrorMessage, validateScanTarget };
|
|
33428
|
+
export { ENGINE_VERSION as CLI_VERSION, DEFAULT_MAX_DURATION_MS, EVIDENCE_OVERRIDES, EXPLAIN_ARMS, KNOWN_RULE_IDS, KNOWN_SCAN_FLAGS, MAX_DURATION_MS, OVERLAP_META_BY_RULE_ID, SUITE_INVALIDATING_RULE_IDS, buildUniversalRules, discoverAndParseRuntimeReport, err, exitForFindings, fallbackWorkspace, internalErrorMessage, isEntryPoint, isValidFindingRecord, levenshtein, main, nearestFlags, out, parseArgs, parseArgsOrUsage, pathMatchesGlob, printUsage, renderCiSubcommandHelp, renderScanOutput, runAnalyzeCommand, runCIIntegrityCommand, runCiAdapterCommand, runCiInstall, runContractVerifyCommand, runCrossFileCommand, runDoctorCommand, runDoctorPlaywright, runEvidenceArm, runEvidenceGraphCommand, runExplainCommand, runFixCommand, runFrameworkMaturityCommand, runHandoffCommand, runHandoverCommand, runHelpCommand, runInstallCommand, runPolicyCommand, runPrCommentCommand, runPwReportCommand, runReleaseTrustCommand, runRulesCommand, runScan, runScanCommand, runStatsCommand, runSuppressionGateCommand, runSuppressions, runTrustTrendCommand, runVerifyCommand, runWhyCommand, usageErrorMessage, validateScanTarget };
|
|
33241
33429
|
|
|
33242
33430
|
//# sourceMappingURL=cli.mjs.map
|