@devmedic/severity-engine
v0.1.0
Published
Every rule declares a default severity; a project's config.rules can override it (or turn a rule off entirely). Resolves the effective severity per rule, and computes the Health Score's severity penalty from one documented, derived formula — no magic numb
Readme
@devmedic/severity-engine
Replaces DevMedic's previous, arbitrary-looking severity handling with a single source of truth: 5 severity levels, a per-rule default, a project-level override map, and a Health Score penalty formula derived from a documented rule instead of hand-picked numbers.
import { createSeverityPolicy } from '@devmedic/severity-engine';
const policy = createSeverityPolicy({
'console-log': 'warning',
'unused-dependencies': 'off',
'async-storage-token': 'critical',
});
policy.resolveSeverity({ id: 'console-log', severity: 'info' }); // 'warning' — overridden
policy.resolveSeverity({ id: 'unused-dependencies', severity: 'warning' }); // 'off' — disabled
policy.resolveSeverity({ id: 'some-other-rule', severity: 'hint' }); // 'hint' — rule's own default, untouchedSeverityPolicy is a small structural interface (resolveSeverity(rule):
Severity | 'off') — @devmedic/rule-engine's AnalyzeOptions.severityPolicy
accepts anything shaped like it without importing this package, the same
duck-typing pattern @devmedic/project-detection-engine's RuleProjectGate
uses. RuleEngine calls it once per rule before execution: an 'off'
result excludes the rule from the run entirely (it never reads or parses a
file, and shows up in ruleExecutionSummary.skipped), rather than running
it and hiding a low-severity result.
Severity levels
Ordered least-to-most severe — hint < info < warning < error < critical.
A rule declares its own default; config.rules (see the example above)
lets a project override any rule's severity, or turn it off entirely.
'off' is not a real severity — it only exists in the override vocabulary
(SeverityOverride, SEVERITY_OVERRIDE_LEVELS), never on an Issue or a
Rule itself.
Health Score penalty formula
Every severity's contribution to the Health Score penalty is:
penalty(rank) = 2^rank − 1where rank is the severity's 0-indexed position in SEVERITY_LEVELS
(hint=0, info=1, warning=2, error=3, critical=4). This gives:
| Severity | rank | penalty (2^rank − 1) |
| ---------- | ---- | -------------------- |
| hint | 0 | 0 |
| info | 1 | 1 |
| warning | 2 | 3 |
| error | 3 | 7 |
| critical | 4 | 15 |
No value here is picked by hand — each is computed from rank via
computeSeverityPenalty, and SEVERITY_PENALTY is built by mapping that
function over SEVERITY_LEVELS, not written out as a literal table. The
formula is a geometric progression: each step is exactly "twice the
previous penalty, plus one" (penalty(rank) = 2·penalty(rank−1) + 1),
which encodes the intent that each severity step should be a
categorically bigger deal than the one below it, not just linearly worse
— a critical issue outweighs 15 hint-level ones, not 4.
hint carries a penalty of 0 by construction (2^0 − 1 = 0): a pure
suggestion should never move the Health Score, only surface in the
report. The info/warning/error/critical values (1/3/7/15) are
exactly what the pre-existing implementation already used — this formula
is a documented derivation of those numbers, not a change to them, so
adopting it does not alter any existing project's Health Score.
@devmedic/report-engine consumes SEVERITY_PENALTY from this package
as the weight in its own Health Score formula rather than declaring a
second, independent copy of the same table.
API
SEVERITY_LEVELS/Severity— the 5 ordered levels.SEVERITY_OVERRIDE_LEVELS/SeverityOverride— the 5 levels plus'off'.SeverityOverrideMap— a project'sconfig.rulesshape,Record<ruleId, SeverityOverride>.SEVERITY_RANK—Record<Severity, number>, each severity's 0-indexed position inSEVERITY_LEVELS.compareSeverity(a, b)— a sort comparator ordering severities least-to-most severe.computeSeverityPenalty(rank)/SEVERITY_PENALTY— the formula above.createSeverityPolicy(overrides?)— builds aSeverityPolicyfrom aSeverityOverrideMap; no argument (or{}) leaves every rule at its own default.isSeverity(value)/isSeverityOverride(value)— type guards.
