mjolnir-qa 0.5.15 → 0.5.17
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 +13 -1
- package/dist/cli.d.mts +1 -1
- package/dist/cli.mjs +72 -4
- package/dist/mcp/stdio.mjs +72 -4
- package/package.json +2 -3
package/CHANGELOG.md
CHANGED
|
@@ -9,7 +9,19 @@ Rule behavior changes (new rules, FP-rate changes against the corpus,
|
|
|
9
9
|
severity changes) are first-class entries here — rule IDs are immutable
|
|
10
10
|
once shipped, so this file is the record of what changed between versions.
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
## [0.5.17] — 2026-09-07
|
|
13
|
+
|
|
14
|
+
### Changes since 0.5.16
|
|
15
|
+
|
|
16
|
+
- Fixture integrity gate: Layer A structural typecheck, doctor fixture-integrity, Python tier (F1, D9, G3) (#53)
|
|
17
|
+
|
|
18
|
+
## [0.5.16] — 2026-09-07
|
|
19
|
+
|
|
20
|
+
### Changes since 0.5.15
|
|
21
|
+
|
|
22
|
+
- Curation + mechanical hygiene: QA-PW-124 D3 alignment, F7-F10 closures (#51)
|
|
23
|
+
|
|
24
|
+
## [0.5.4] — 2026-09-06
|
|
13
25
|
|
|
14
26
|
### Agent Handoff + Minimized Reporting (plan 1788599400000)
|
|
15
27
|
|
package/dist/cli.d.mts
CHANGED
|
@@ -699,7 +699,7 @@ declare const runScan: typeof runScan$1, buildUniversalRules: typeof buildUniver
|
|
|
699
699
|
* `scripts/sync-sarif-version.cjs` on release and guarded by
|
|
700
700
|
* `tests/version-consistency.spec.ts` locally.
|
|
701
701
|
*/
|
|
702
|
-
declare const CLI_VERSION = "0.5.
|
|
702
|
+
declare const CLI_VERSION = "0.5.17";
|
|
703
703
|
/** A usage-error detail: the offending token, when one exists. */
|
|
704
704
|
interface UsageErrorDetail {
|
|
705
705
|
/** The unknown flag or rejected value (e.g. `--nope`, `loud`). */
|
package/dist/cli.mjs
CHANGED
|
@@ -5403,6 +5403,13 @@ const pwNoTraceOnRetry = defineRule({
|
|
|
5403
5403
|
* QA-PW-124 — Projects defined but no smoke/regression split.
|
|
5404
5404
|
* Severity: info · Confidence: high · deterministic-defect
|
|
5405
5405
|
* One monolithic project forces full-suite-or-nothing; PR feedback dies.
|
|
5406
|
+
*
|
|
5407
|
+
* detectorRevision 2 (certification-audit D3, 2026-09-07): the rule is now
|
|
5408
|
+
* DECLARED as a config-gated rule (`configRule` + `configFiles`) instead of
|
|
5409
|
+
* relying only on its internal basename gate — same gating surface as
|
|
5410
|
+
* QA-PW-121/122/141/143/144. Detection logic is unchanged (the internal
|
|
5411
|
+
* basename gate remains as defense in depth), so scan output is
|
|
5412
|
+
* byte-identical; the revision bump declares the metadata change per §07.
|
|
5406
5413
|
*/
|
|
5407
5414
|
const pwNoProjectSplit = defineRule({
|
|
5408
5415
|
id: "QA-PW-124",
|
|
@@ -5413,13 +5420,16 @@ const pwNoProjectSplit = defineRule({
|
|
|
5413
5420
|
findingType: "heuristic-risk",
|
|
5414
5421
|
qaImpact: "HYGIENE",
|
|
5415
5422
|
appliesTo: "test-files",
|
|
5423
|
+
configRule: true,
|
|
5424
|
+
configFiles: ["^playwright\\.config\\.(?:ts|js|mjs|cts)$"],
|
|
5416
5425
|
languages: ["typescript", "javascript"],
|
|
5417
5426
|
frameworks: ["playwright"],
|
|
5418
5427
|
falsePositiveRisk: "low",
|
|
5419
5428
|
autofix: false,
|
|
5420
5429
|
detectionStrategy: "LEXICAL",
|
|
5421
|
-
detectionNotes: "regex heuristic",
|
|
5430
|
+
detectionNotes: "regex heuristic over playwright.config.* (adapter-gated)",
|
|
5422
5431
|
introduced: "0.3.0",
|
|
5432
|
+
detectorRevision: 2,
|
|
5423
5433
|
run(ctx) {
|
|
5424
5434
|
const text = ctx.text;
|
|
5425
5435
|
const findings = [];
|
|
@@ -13254,7 +13264,7 @@ function renderSarif(result, repoRootUri) {
|
|
|
13254
13264
|
tool: { driver: {
|
|
13255
13265
|
name: "Mjölnir",
|
|
13256
13266
|
informationUri: "https://github.com/Sergey-Bar/Mjolnir",
|
|
13257
|
-
version: "0.5.
|
|
13267
|
+
version: "0.5.17",
|
|
13258
13268
|
rules: [...rules.values()].map((r) => {
|
|
13259
13269
|
const meta = RULES.find((x) => x.id === r.id);
|
|
13260
13270
|
return {
|
|
@@ -17816,6 +17826,63 @@ function checkQuarantineEnforcement(rules = RULES) {
|
|
|
17816
17826
|
details
|
|
17817
17827
|
};
|
|
17818
17828
|
}
|
|
17829
|
+
/**
|
|
17830
|
+
* Check 8 (certification-audit Phase 2.5, plan G3 Layer A support):
|
|
17831
|
+
* fixture-integrity — deterministic structural census of the fixture
|
|
17832
|
+
* trees. Complements the fixture firewall (which proves must-fire /
|
|
17833
|
+
* must-not-fire PRESENCE per rule) with: orphaned fixture dirs (no
|
|
17834
|
+
* registered rule), empty dirs, and the Layer A typecheck allowlist
|
|
17835
|
+
* census. Blocking: an orphaned dir or an empty fixture dir means the
|
|
17836
|
+
* structural layer has silently drifted.
|
|
17837
|
+
*/
|
|
17838
|
+
function checkFixtureIntegrity(fixturesRoot, rules = RULES) {
|
|
17839
|
+
const details = [];
|
|
17840
|
+
let ok = true;
|
|
17841
|
+
const registeredIds = new Set(rules.map((r) => r.id));
|
|
17842
|
+
const fixtureRoots = [fixturesRoot, join(fixturesRoot, "..", "corpus", "positive-fixtures")];
|
|
17843
|
+
let fixtureFiles = 0;
|
|
17844
|
+
let fixtureDirs = 0;
|
|
17845
|
+
for (const root of fixtureRoots) {
|
|
17846
|
+
if (!existsSync(root)) continue;
|
|
17847
|
+
for (const entry of readdirSync(root, { withFileTypes: true })) {
|
|
17848
|
+
if (!entry.isDirectory() || entry.name.startsWith(".")) continue;
|
|
17849
|
+
fixtureDirs++;
|
|
17850
|
+
const ruleId = entry.name;
|
|
17851
|
+
if (!registeredIds.has(ruleId)) {
|
|
17852
|
+
ok = false;
|
|
17853
|
+
details.push(`orphaned fixture dir (no registered rule): ${ruleId}`);
|
|
17854
|
+
continue;
|
|
17855
|
+
}
|
|
17856
|
+
const sub = join(root, ruleId);
|
|
17857
|
+
for (const child of readdirSync(sub, { withFileTypes: true })) if (child.isDirectory()) {
|
|
17858
|
+
const files = nonHiddenFiles(join(sub, child.name));
|
|
17859
|
+
fixtureFiles += files.length;
|
|
17860
|
+
if (files.length === 0) {
|
|
17861
|
+
ok = false;
|
|
17862
|
+
details.push(`empty fixture dir: ${ruleId}/${child.name}`);
|
|
17863
|
+
}
|
|
17864
|
+
} else if (!child.name.startsWith(".")) fixtureFiles++;
|
|
17865
|
+
}
|
|
17866
|
+
}
|
|
17867
|
+
const allowlistPath = join(fixturesRoot, "typecheck-allowlist.json");
|
|
17868
|
+
if (existsSync(allowlistPath)) try {
|
|
17869
|
+
const parsed = JSON.parse(readFileSync(allowlistPath, "utf8"));
|
|
17870
|
+
if (Array.isArray(parsed.entries)) details.push(`Layer A typecheck allowlist: ${parsed.entries.length} justified entr(ies)`);
|
|
17871
|
+
else {
|
|
17872
|
+
ok = false;
|
|
17873
|
+
details.push("typecheck-allowlist.json entries is not an array");
|
|
17874
|
+
}
|
|
17875
|
+
} catch {
|
|
17876
|
+
ok = false;
|
|
17877
|
+
details.push("typecheck-allowlist.json is unreadable/malformed");
|
|
17878
|
+
}
|
|
17879
|
+
details.unshift(`fixture trees: ${fixtureDirs} rule dirs, ${fixtureFiles} fixture files — orphaned dirs and empty dirs are blocking`);
|
|
17880
|
+
return {
|
|
17881
|
+
name: "fixture-integrity",
|
|
17882
|
+
ok,
|
|
17883
|
+
details
|
|
17884
|
+
};
|
|
17885
|
+
}
|
|
17819
17886
|
function runDoctorSelfAudit(fixturesRoot) {
|
|
17820
17887
|
const verdictsDir = join(fixturesRoot, "..", "corpus", "verdicts");
|
|
17821
17888
|
const checks = [
|
|
@@ -17825,7 +17892,8 @@ function runDoctorSelfAudit(fixturesRoot) {
|
|
|
17825
17892
|
checkEvidenceHonesty(),
|
|
17826
17893
|
checkTierEnforcement(verdictsDir),
|
|
17827
17894
|
checkAntiCreep(),
|
|
17828
|
-
checkQuarantineEnforcement()
|
|
17895
|
+
checkQuarantineEnforcement(),
|
|
17896
|
+
checkFixtureIntegrity(fixturesRoot)
|
|
17829
17897
|
];
|
|
17830
17898
|
return {
|
|
17831
17899
|
checks,
|
|
@@ -18064,7 +18132,7 @@ const { runScan, buildUniversalRules, fallbackWorkspace, pathMatchesGlob, isVali
|
|
|
18064
18132
|
* `scripts/sync-sarif-version.cjs` on release and guarded by
|
|
18065
18133
|
* `tests/version-consistency.spec.ts` locally.
|
|
18066
18134
|
*/
|
|
18067
|
-
const CLI_VERSION = "0.5.
|
|
18135
|
+
const CLI_VERSION = "0.5.17";
|
|
18068
18136
|
function parseArgs(argv, onError) {
|
|
18069
18137
|
const args = {
|
|
18070
18138
|
target: ".",
|
package/dist/mcp/stdio.mjs
CHANGED
|
@@ -5402,6 +5402,13 @@ const pwNoTraceOnRetry = defineRule({
|
|
|
5402
5402
|
* QA-PW-124 — Projects defined but no smoke/regression split.
|
|
5403
5403
|
* Severity: info · Confidence: high · deterministic-defect
|
|
5404
5404
|
* One monolithic project forces full-suite-or-nothing; PR feedback dies.
|
|
5405
|
+
*
|
|
5406
|
+
* detectorRevision 2 (certification-audit D3, 2026-09-07): the rule is now
|
|
5407
|
+
* DECLARED as a config-gated rule (`configRule` + `configFiles`) instead of
|
|
5408
|
+
* relying only on its internal basename gate — same gating surface as
|
|
5409
|
+
* QA-PW-121/122/141/143/144. Detection logic is unchanged (the internal
|
|
5410
|
+
* basename gate remains as defense in depth), so scan output is
|
|
5411
|
+
* byte-identical; the revision bump declares the metadata change per §07.
|
|
5405
5412
|
*/
|
|
5406
5413
|
const pwNoProjectSplit = defineRule({
|
|
5407
5414
|
id: "QA-PW-124",
|
|
@@ -5412,13 +5419,16 @@ const pwNoProjectSplit = defineRule({
|
|
|
5412
5419
|
findingType: "heuristic-risk",
|
|
5413
5420
|
qaImpact: "HYGIENE",
|
|
5414
5421
|
appliesTo: "test-files",
|
|
5422
|
+
configRule: true,
|
|
5423
|
+
configFiles: ["^playwright\\.config\\.(?:ts|js|mjs|cts)$"],
|
|
5415
5424
|
languages: ["typescript", "javascript"],
|
|
5416
5425
|
frameworks: ["playwright"],
|
|
5417
5426
|
falsePositiveRisk: "low",
|
|
5418
5427
|
autofix: false,
|
|
5419
5428
|
detectionStrategy: "LEXICAL",
|
|
5420
|
-
detectionNotes: "regex heuristic",
|
|
5429
|
+
detectionNotes: "regex heuristic over playwright.config.* (adapter-gated)",
|
|
5421
5430
|
introduced: "0.3.0",
|
|
5431
|
+
detectorRevision: 2,
|
|
5422
5432
|
run(ctx) {
|
|
5423
5433
|
const text = ctx.text;
|
|
5424
5434
|
const findings = [];
|
|
@@ -13253,7 +13263,7 @@ function renderSarif(result, repoRootUri) {
|
|
|
13253
13263
|
tool: { driver: {
|
|
13254
13264
|
name: "Mjölnir",
|
|
13255
13265
|
informationUri: "https://github.com/Sergey-Bar/Mjolnir",
|
|
13256
|
-
version: "0.5.
|
|
13266
|
+
version: "0.5.17",
|
|
13257
13267
|
rules: [...rules.values()].map((r) => {
|
|
13258
13268
|
const meta = RULES.find((x) => x.id === r.id);
|
|
13259
13269
|
return {
|
|
@@ -17298,6 +17308,63 @@ function checkQuarantineEnforcement(rules = RULES) {
|
|
|
17298
17308
|
details
|
|
17299
17309
|
};
|
|
17300
17310
|
}
|
|
17311
|
+
/**
|
|
17312
|
+
* Check 8 (certification-audit Phase 2.5, plan G3 Layer A support):
|
|
17313
|
+
* fixture-integrity — deterministic structural census of the fixture
|
|
17314
|
+
* trees. Complements the fixture firewall (which proves must-fire /
|
|
17315
|
+
* must-not-fire PRESENCE per rule) with: orphaned fixture dirs (no
|
|
17316
|
+
* registered rule), empty dirs, and the Layer A typecheck allowlist
|
|
17317
|
+
* census. Blocking: an orphaned dir or an empty fixture dir means the
|
|
17318
|
+
* structural layer has silently drifted.
|
|
17319
|
+
*/
|
|
17320
|
+
function checkFixtureIntegrity(fixturesRoot, rules = RULES) {
|
|
17321
|
+
const details = [];
|
|
17322
|
+
let ok = true;
|
|
17323
|
+
const registeredIds = new Set(rules.map((r) => r.id));
|
|
17324
|
+
const fixtureRoots = [fixturesRoot, join(fixturesRoot, "..", "corpus", "positive-fixtures")];
|
|
17325
|
+
let fixtureFiles = 0;
|
|
17326
|
+
let fixtureDirs = 0;
|
|
17327
|
+
for (const root of fixtureRoots) {
|
|
17328
|
+
if (!existsSync(root)) continue;
|
|
17329
|
+
for (const entry of readdirSync(root, { withFileTypes: true })) {
|
|
17330
|
+
if (!entry.isDirectory() || entry.name.startsWith(".")) continue;
|
|
17331
|
+
fixtureDirs++;
|
|
17332
|
+
const ruleId = entry.name;
|
|
17333
|
+
if (!registeredIds.has(ruleId)) {
|
|
17334
|
+
ok = false;
|
|
17335
|
+
details.push(`orphaned fixture dir (no registered rule): ${ruleId}`);
|
|
17336
|
+
continue;
|
|
17337
|
+
}
|
|
17338
|
+
const sub = join(root, ruleId);
|
|
17339
|
+
for (const child of readdirSync(sub, { withFileTypes: true })) if (child.isDirectory()) {
|
|
17340
|
+
const files = nonHiddenFiles(join(sub, child.name));
|
|
17341
|
+
fixtureFiles += files.length;
|
|
17342
|
+
if (files.length === 0) {
|
|
17343
|
+
ok = false;
|
|
17344
|
+
details.push(`empty fixture dir: ${ruleId}/${child.name}`);
|
|
17345
|
+
}
|
|
17346
|
+
} else if (!child.name.startsWith(".")) fixtureFiles++;
|
|
17347
|
+
}
|
|
17348
|
+
}
|
|
17349
|
+
const allowlistPath = join(fixturesRoot, "typecheck-allowlist.json");
|
|
17350
|
+
if (existsSync(allowlistPath)) try {
|
|
17351
|
+
const parsed = JSON.parse(readFileSync(allowlistPath, "utf8"));
|
|
17352
|
+
if (Array.isArray(parsed.entries)) details.push(`Layer A typecheck allowlist: ${parsed.entries.length} justified entr(ies)`);
|
|
17353
|
+
else {
|
|
17354
|
+
ok = false;
|
|
17355
|
+
details.push("typecheck-allowlist.json entries is not an array");
|
|
17356
|
+
}
|
|
17357
|
+
} catch {
|
|
17358
|
+
ok = false;
|
|
17359
|
+
details.push("typecheck-allowlist.json is unreadable/malformed");
|
|
17360
|
+
}
|
|
17361
|
+
details.unshift(`fixture trees: ${fixtureDirs} rule dirs, ${fixtureFiles} fixture files — orphaned dirs and empty dirs are blocking`);
|
|
17362
|
+
return {
|
|
17363
|
+
name: "fixture-integrity",
|
|
17364
|
+
ok,
|
|
17365
|
+
details
|
|
17366
|
+
};
|
|
17367
|
+
}
|
|
17301
17368
|
function runDoctorSelfAudit(fixturesRoot) {
|
|
17302
17369
|
const verdictsDir = join(fixturesRoot, "..", "corpus", "verdicts");
|
|
17303
17370
|
const checks = [
|
|
@@ -17307,7 +17374,8 @@ function runDoctorSelfAudit(fixturesRoot) {
|
|
|
17307
17374
|
checkEvidenceHonesty(),
|
|
17308
17375
|
checkTierEnforcement(verdictsDir),
|
|
17309
17376
|
checkAntiCreep(),
|
|
17310
|
-
checkQuarantineEnforcement()
|
|
17377
|
+
checkQuarantineEnforcement(),
|
|
17378
|
+
checkFixtureIntegrity(fixturesRoot)
|
|
17311
17379
|
];
|
|
17312
17380
|
return {
|
|
17313
17381
|
checks,
|
|
@@ -17700,7 +17768,7 @@ const { runScan, buildUniversalRules, fallbackWorkspace, pathMatchesGlob, isVali
|
|
|
17700
17768
|
* `scripts/sync-sarif-version.cjs` on release and guarded by
|
|
17701
17769
|
* `tests/version-consistency.spec.ts` locally.
|
|
17702
17770
|
*/
|
|
17703
|
-
const CLI_VERSION = "0.5.
|
|
17771
|
+
const CLI_VERSION = "0.5.17";
|
|
17704
17772
|
function parseArgs(argv, onError) {
|
|
17705
17773
|
const args = {
|
|
17706
17774
|
target: ".",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mjolnir-qa",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.17",
|
|
4
4
|
"description": "Mjölnir — the Verification Trust Engine for QA. Audits test suites and CI pipelines, reports a worthiness score and prioritized findings.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"test:watch": "vitest",
|
|
29
29
|
"lint": "eslint . --max-warnings=0 && prettier --check .",
|
|
30
30
|
"format": "prettier --write .",
|
|
31
|
-
"typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.test.json",
|
|
31
|
+
"typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.test.json && tsx scripts/typecheck-fixtures.ts",
|
|
32
32
|
"golden:update": "tsx tests/golden/gen.ts",
|
|
33
33
|
"corpus:regression": "tsx tests/corpus/audit.ts",
|
|
34
34
|
"corpus:regression:update": "tsx tests/corpus/audit.ts --update",
|
|
@@ -87,7 +87,6 @@
|
|
|
87
87
|
"@playwright/test": "^1.50.0",
|
|
88
88
|
"@types/node": "^26.4.0",
|
|
89
89
|
"@vitest/coverage-istanbul": "^4.1.11",
|
|
90
|
-
"@vitest/coverage-v8": "^4.1.11",
|
|
91
90
|
"axe-core": "^4.13.0",
|
|
92
91
|
"eslint": "^10.9.1",
|
|
93
92
|
"eslint-plugin-regexp": "^3.3.0",
|