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 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
- # [0.5.4] — 2026-09-06
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.15";
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.15",
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.15";
18135
+ const CLI_VERSION = "0.5.17";
18068
18136
  function parseArgs(argv, onError) {
18069
18137
  const args = {
18070
18138
  target: ".",
@@ -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.15",
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.15";
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.15",
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",