mjolnir-qa 0.5.33 → 0.5.34

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -9,6 +9,12 @@ 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.34] — 2026-09-08
13
+
14
+ ### Changes since 0
15
+
16
+ - p2: structural anti-dilution — deduction-mass ceilings close the padding vector (plan 1788853205786 P2, decision 4) (#64)
17
+
12
18
  ## [0.5.33] — 2026-09-08
13
19
 
14
20
  ### Changes since 0.5.32
package/dist/cli.d.mts CHANGED
@@ -201,6 +201,15 @@ interface ScanResult {
201
201
  testDeclarationCount?: number;
202
202
  /** Raw deduction total before normalization (Phase 5 — transparency). */
203
203
  rawDeductions?: number;
204
+ /**
205
+ * The evidence-discounted deduction mass the P2 anti-dilution ceiling
206
+ * caps against (equals rawDeductions today — E0 charges 0, E1 halves;
207
+ * the ceiling input is deliberately the full discount surface).
208
+ * Additive within schemaVersion 1; present so consumers recompute the
209
+ * ceiling from docs/SCORING.md formula v2 without re-deriving
210
+ * evidence levels.
211
+ */
212
+ effectiveDeductions?: number;
204
213
  /** Number of findings suppressed by active config entries (suppression transparency). */
205
214
  suppressionCount?: number;
206
215
  /**
@@ -728,7 +737,7 @@ declare const runScan: typeof runScan$1, buildUniversalRules: typeof buildUniver
728
737
  * `scripts/sync-sarif-version.cjs` on release and guarded by
729
738
  * `tests/version-consistency.spec.ts` locally.
730
739
  */
731
- declare const CLI_VERSION = "0.5.33";
740
+ declare const CLI_VERSION = "0.5.34";
732
741
  /** A usage-error detail: the offending token, when one exists. */
733
742
  interface UsageErrorDetail {
734
743
  /** The unknown flag or rejected value (e.g. `--nope`, `loud`). */
package/dist/cli.mjs CHANGED
@@ -10074,6 +10074,58 @@ function computeDimensions(findings) {
10074
10074
  for (const dim of byCategory.values()) dim.score = Math.max(0, 100 - deductions.get(dim.category));
10075
10075
  return [...byCategory.values()].sort((a, b) => a.category.localeCompare(b.category));
10076
10076
  }
10077
+ /**
10078
+ * Deduction-mass ceilings (product-gap-remediation master plan P2,
10079
+ * plan 1788853205786 — structural anti-dilution, decision 4).
10080
+ *
10081
+ * The Goodhart vector the density formula permits: fixed deduction mass
10082
+ * ÷ padded denominator → score → 99. A repo with 80 warning-points of
10083
+ * real findings and 10,000 test declarations reads 99+ ("one minor
10084
+ * issue") because the RATE is tiny — the padding is free. Density stays
10085
+ * as the legitimate differentiator for small masses (a lone warning in
10086
+ * a 10k suite must still read ≥ 95), but it can no longer dilute a
10087
+ * large mass: each absolute deduction-mass band caps the score. Padding
10088
+ * cannot move a ceiling — the vector is structurally closed. 80
10089
+ * warning-pts read ≤ 75 in ANY suite size.
10090
+ *
10091
+ * The error-severity floor (95) is the ≥ 8 band seen from the other
10092
+ * side: 1 error ≥ 8 pts, so it is subsumed and left standing as a named
10093
+ * special case for prose continuity. The > 0 band is the honesty guard
10094
+ * (clampWithFindings) — subsumed as well.
10095
+ *
10096
+ * Calibration (P2.4): these boundaries were chosen to hold the three
10097
+ * known data points (self 100, golden 49 via suiteVoided, demo 67) and
10098
+ * are then FROZEN with a changelog entry. Never tune them to flatter a
10099
+ * repo (docs/SCORING.md Correction section is the precedent).
10100
+ */
10101
+ const DEDUCTION_MASS_CEILINGS = [
10102
+ {
10103
+ minMass: 160,
10104
+ ceiling: 65
10105
+ },
10106
+ {
10107
+ minMass: 80,
10108
+ ceiling: 75
10109
+ },
10110
+ {
10111
+ minMass: 40,
10112
+ ceiling: 85
10113
+ },
10114
+ {
10115
+ minMass: 8,
10116
+ ceiling: 95
10117
+ }
10118
+ ];
10119
+ /**
10120
+ * The score ceiling for a given evidence-discounted deduction mass.
10121
+ * 0 → no ceiling (the 100 honesty guard lives in clampWithFindings);
10122
+ * below the smallest band → no ceiling beyond the named guards.
10123
+ * Exported for the JSON contract (effectiveDeductions) and the tests.
10124
+ */
10125
+ function massCeiling(totalDeduction) {
10126
+ for (const band of DEDUCTION_MASS_CEILINGS) if (totalDeduction >= band.minMass) return band.ceiling;
10127
+ return null;
10128
+ }
10077
10129
  function computeTotal(dimensions, findings, exposure) {
10078
10130
  if (findings.length === 0) return 100;
10079
10131
  let totalDeduction = 0;
@@ -10086,6 +10138,8 @@ function computeTotal(dimensions, findings, exposure) {
10086
10138
  const rate = totalDeduction / (declarations + 1);
10087
10139
  score = clampWithFindings(100 - Math.min(100, rate * 5), totalDeduction);
10088
10140
  } else score = clampWithFindings(100 - totalDeduction, totalDeduction);
10141
+ const ceiling = massCeiling(totalDeduction);
10142
+ if (ceiling !== null && score > ceiling) score = ceiling;
10089
10143
  if (findings.some((f) => f.severity === "error" && deductionFor(f) > 0) && score > 95) score = 95;
10090
10144
  return suiteVoided ? Math.min(score, 49) : score;
10091
10145
  }
@@ -12322,6 +12376,7 @@ async function runScan$1(args, hooks = {}) {
12322
12376
  });
12323
12377
  const dimensions = computeDimensions(findings);
12324
12378
  const rawDeductions = findings.reduce((sum, f) => sum + deductionFor(f), 0);
12379
+ const effectiveDeductions = rawDeductions;
12325
12380
  const total = computeTotal(dimensions, findings, {
12326
12381
  testDeclarations: testDeclarationCount,
12327
12382
  testFileCount,
@@ -12346,6 +12401,7 @@ async function runScan$1(args, hooks = {}) {
12346
12401
  testFileCount,
12347
12402
  testDeclarationCount,
12348
12403
  rawDeductions,
12404
+ effectiveDeductions,
12349
12405
  suppressionCount,
12350
12406
  ...pluginsLoaded.length > 0 ? { plugins: pluginsLoaded } : {},
12351
12407
  agenticProfile: computeAgenticProfile(fileProvenance, findings),
@@ -12900,6 +12956,10 @@ function appendScoreSection(lines, result, p, width, ascii) {
12900
12956
  lines.push(` ${scoreGauge(result.score, p, gaugeWidth, ascii)}`);
12901
12957
  lines.push(` ${p.dim(headlineFor(state, result.findings.length))}`);
12902
12958
  if (result.rawDeductions !== void 0 && result.testDeclarationCount) lines.push(` ${p.dim(`(${result.rawDeductions} raw pts / ${result.testDeclarationCount} test declarations — normalized)`)}`);
12959
+ if (result.effectiveDeductions !== void 0 && result.score !== null) {
12960
+ const ceiling = massCeiling(result.effectiveDeductions);
12961
+ if (ceiling !== null && result.score <= ceiling && result.rawDeductions !== void 0) lines.push(` ${p.dim(`(capped: deduction mass ${result.effectiveDeductions} pts — absolute ceiling ${ceiling})`)}`);
12962
+ }
12903
12963
  if (result.suppressionCount && result.suppressionCount > 0) lines.push(` ${p.dim(`(${result.suppressionCount} finding(s) suppressed by config)`)}`);
12904
12964
  lines.push("");
12905
12965
  }
@@ -13290,7 +13350,7 @@ function renderSarif(result, repoRootUri) {
13290
13350
  tool: { driver: {
13291
13351
  name: "Mjölnir",
13292
13352
  informationUri: "https://github.com/Sergey-Bar/Mjolnir",
13293
- version: "0.5.33",
13353
+ version: "0.5.34",
13294
13354
  rules: [...rules.values()].map((r) => {
13295
13355
  const meta = RULES.find((x) => x.id === r.id);
13296
13356
  return {
@@ -18788,7 +18848,7 @@ const { runScan, buildUniversalRules, fallbackWorkspace, pathMatchesGlob, isVali
18788
18848
  * `scripts/sync-sarif-version.cjs` on release and guarded by
18789
18849
  * `tests/version-consistency.spec.ts` locally.
18790
18850
  */
18791
- const CLI_VERSION = "0.5.33";
18851
+ const CLI_VERSION = "0.5.34";
18792
18852
  function parseArgs(argv, onError) {
18793
18853
  const args = {
18794
18854
  target: ".",
@@ -10073,6 +10073,58 @@ function computeDimensions(findings) {
10073
10073
  for (const dim of byCategory.values()) dim.score = Math.max(0, 100 - deductions.get(dim.category));
10074
10074
  return [...byCategory.values()].sort((a, b) => a.category.localeCompare(b.category));
10075
10075
  }
10076
+ /**
10077
+ * Deduction-mass ceilings (product-gap-remediation master plan P2,
10078
+ * plan 1788853205786 — structural anti-dilution, decision 4).
10079
+ *
10080
+ * The Goodhart vector the density formula permits: fixed deduction mass
10081
+ * ÷ padded denominator → score → 99. A repo with 80 warning-points of
10082
+ * real findings and 10,000 test declarations reads 99+ ("one minor
10083
+ * issue") because the RATE is tiny — the padding is free. Density stays
10084
+ * as the legitimate differentiator for small masses (a lone warning in
10085
+ * a 10k suite must still read ≥ 95), but it can no longer dilute a
10086
+ * large mass: each absolute deduction-mass band caps the score. Padding
10087
+ * cannot move a ceiling — the vector is structurally closed. 80
10088
+ * warning-pts read ≤ 75 in ANY suite size.
10089
+ *
10090
+ * The error-severity floor (95) is the ≥ 8 band seen from the other
10091
+ * side: 1 error ≥ 8 pts, so it is subsumed and left standing as a named
10092
+ * special case for prose continuity. The > 0 band is the honesty guard
10093
+ * (clampWithFindings) — subsumed as well.
10094
+ *
10095
+ * Calibration (P2.4): these boundaries were chosen to hold the three
10096
+ * known data points (self 100, golden 49 via suiteVoided, demo 67) and
10097
+ * are then FROZEN with a changelog entry. Never tune them to flatter a
10098
+ * repo (docs/SCORING.md Correction section is the precedent).
10099
+ */
10100
+ const DEDUCTION_MASS_CEILINGS = [
10101
+ {
10102
+ minMass: 160,
10103
+ ceiling: 65
10104
+ },
10105
+ {
10106
+ minMass: 80,
10107
+ ceiling: 75
10108
+ },
10109
+ {
10110
+ minMass: 40,
10111
+ ceiling: 85
10112
+ },
10113
+ {
10114
+ minMass: 8,
10115
+ ceiling: 95
10116
+ }
10117
+ ];
10118
+ /**
10119
+ * The score ceiling for a given evidence-discounted deduction mass.
10120
+ * 0 → no ceiling (the 100 honesty guard lives in clampWithFindings);
10121
+ * below the smallest band → no ceiling beyond the named guards.
10122
+ * Exported for the JSON contract (effectiveDeductions) and the tests.
10123
+ */
10124
+ function massCeiling(totalDeduction) {
10125
+ for (const band of DEDUCTION_MASS_CEILINGS) if (totalDeduction >= band.minMass) return band.ceiling;
10126
+ return null;
10127
+ }
10076
10128
  function computeTotal(dimensions, findings, exposure) {
10077
10129
  if (findings.length === 0) return 100;
10078
10130
  let totalDeduction = 0;
@@ -10085,6 +10137,8 @@ function computeTotal(dimensions, findings, exposure) {
10085
10137
  const rate = totalDeduction / (declarations + 1);
10086
10138
  score = clampWithFindings(100 - Math.min(100, rate * 5), totalDeduction);
10087
10139
  } else score = clampWithFindings(100 - totalDeduction, totalDeduction);
10140
+ const ceiling = massCeiling(totalDeduction);
10141
+ if (ceiling !== null && score > ceiling) score = ceiling;
10088
10142
  if (findings.some((f) => f.severity === "error" && deductionFor(f) > 0) && score > 95) score = 95;
10089
10143
  return suiteVoided ? Math.min(score, 49) : score;
10090
10144
  }
@@ -12321,6 +12375,7 @@ async function runScan$1(args, hooks = {}) {
12321
12375
  });
12322
12376
  const dimensions = computeDimensions(findings);
12323
12377
  const rawDeductions = findings.reduce((sum, f) => sum + deductionFor(f), 0);
12378
+ const effectiveDeductions = rawDeductions;
12324
12379
  const total = computeTotal(dimensions, findings, {
12325
12380
  testDeclarations: testDeclarationCount,
12326
12381
  testFileCount,
@@ -12345,6 +12400,7 @@ async function runScan$1(args, hooks = {}) {
12345
12400
  testFileCount,
12346
12401
  testDeclarationCount,
12347
12402
  rawDeductions,
12403
+ effectiveDeductions,
12348
12404
  suppressionCount,
12349
12405
  ...pluginsLoaded.length > 0 ? { plugins: pluginsLoaded } : {},
12350
12406
  agenticProfile: computeAgenticProfile(fileProvenance, findings),
@@ -12899,6 +12955,10 @@ function appendScoreSection(lines, result, p, width, ascii) {
12899
12955
  lines.push(` ${scoreGauge(result.score, p, gaugeWidth, ascii)}`);
12900
12956
  lines.push(` ${p.dim(headlineFor(state, result.findings.length))}`);
12901
12957
  if (result.rawDeductions !== void 0 && result.testDeclarationCount) lines.push(` ${p.dim(`(${result.rawDeductions} raw pts / ${result.testDeclarationCount} test declarations — normalized)`)}`);
12958
+ if (result.effectiveDeductions !== void 0 && result.score !== null) {
12959
+ const ceiling = massCeiling(result.effectiveDeductions);
12960
+ if (ceiling !== null && result.score <= ceiling && result.rawDeductions !== void 0) lines.push(` ${p.dim(`(capped: deduction mass ${result.effectiveDeductions} pts — absolute ceiling ${ceiling})`)}`);
12961
+ }
12902
12962
  if (result.suppressionCount && result.suppressionCount > 0) lines.push(` ${p.dim(`(${result.suppressionCount} finding(s) suppressed by config)`)}`);
12903
12963
  lines.push("");
12904
12964
  }
@@ -13289,7 +13349,7 @@ function renderSarif(result, repoRootUri) {
13289
13349
  tool: { driver: {
13290
13350
  name: "Mjölnir",
13291
13351
  informationUri: "https://github.com/Sergey-Bar/Mjolnir",
13292
- version: "0.5.33",
13352
+ version: "0.5.34",
13293
13353
  rules: [...rules.values()].map((r) => {
13294
13354
  const meta = RULES.find((x) => x.id === r.id);
13295
13355
  return {
@@ -18424,7 +18484,7 @@ const { runScan, buildUniversalRules, fallbackWorkspace, pathMatchesGlob, isVali
18424
18484
  * `scripts/sync-sarif-version.cjs` on release and guarded by
18425
18485
  * `tests/version-consistency.spec.ts` locally.
18426
18486
  */
18427
- const CLI_VERSION = "0.5.33";
18487
+ const CLI_VERSION = "0.5.34";
18428
18488
  function parseArgs(argv, onError) {
18429
18489
  const args = {
18430
18490
  target: ".",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mjolnir-qa",
3
- "version": "0.5.33",
3
+ "version": "0.5.34",
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": {