npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@devmedic/report-engine

v0.1.0

Published

Normalizes and aggregates findings, computes HealthScore, dispatches to reporter adapters.

Readme

@devmedic/report-engine

Turns Normalized Issues (from @devmedic/issue-engine) into scores, summaries, and six output formats. No auto-fix, no AI, no UI — only reporting.

import { generateReport, renderReport } from '@devmedic/report-engine';

const report = generateReport({
  issues, // NormalizedIssue[] — the only required input
  rules, // optional: include zero-issue rules in the rule summary
  plugins, // optional: enables the plugin summary
  execution, // optional: enables execution summary + performance metrics, and a more accurate health score
});

renderReport(report, 'cli'); // colored terminal output
renderReport(report, 'json'); // for a future dashboard API
renderReport(report, 'markdown'); // PR comment / docs
renderReport(report, 'html'); // self-contained static page
renderReport(report, 'sarif'); // GitHub code scanning et al.
renderReport(report, 'github-actions-summary'); // $GITHUB_STEP_SUMMARY

issues is the only required field — a report can always be generated from Normalized Issues alone. Everything else is optional enrichment: without rules, a rule that found nothing won't appear in the rule summary; without execution, there's no execution/performance section and the health score falls back to a less accurate file-count estimate (see below).

Scores

Health Score and the four Category Scores (feeding the named architectureScore / securityScore / performanceScore convenience fields) all use the same formula: severity-weighted issue density, decayed exponentially into 0–100.

penaltyPerFile = Σ(severityPenalty) / fileCount
score = round(100 × e^(-penaltyPerFile / decayConstant))

Bounded and monotonic — more or worse issues never raise the score, and it can never go negative the way a linear subtraction could. fileCount prefers an explicit override, then execution.filesAnalyzed (the real number the Rule Engine looked at), and only falls back to counting distinct files with an issue — which is always an undercount (clean files are invisible to it) and therefore understates the score. Pass execution for an accurate one.

The two constants, and why they're not magic numbers

severityPenalty is @devmedic/severity-engine's SEVERITY_PENALTY, re-exported here rather than duplicated: penalty(rank) = 2^rank − 1, where rank is the severity's 0-indexed position among hint < info < warning < error < critical. That gives { hint: 0, info: 1, warning: 3, error: 7, critical: 15 } — each severity step is exactly twice the previous penalty plus one, so a critical issue outweighs 15 hint-level ones categorically, not just linearly. hint is 0 by construction: a pure suggestion never moves the score. See @devmedic/severity-engine's README for the full derivation.

decayConstant (SCORE_DECAY_CONSTANT = 10) controls how sharply the score falls off as penaltyPerFile rises — it is not derived from the penalty table, it is the exponential's own rate parameter. Its precise meaning: the score crosses 100/e ≈ 36.8 exactly when penaltyPerFile equals decayConstant — e.g. at the current value of 10, a codebase averaging one error-severity issue (penalty 7) plus a bit more per file has already crossed that point. Raising it makes the score more lenient (the same issue density produces a higher score); lowering it makes the score fall off faster for the same issues.

What's computed

| Responsibility | Where | | --------------------------------------- | --------------------------------------------------------------------------------------------------- | | Health Score / Category Scores | scoring.ts | | Issue Statistics | statistics.ts — totals, by severity, by category, fixable count, file count | | Rule Summary | rule-summary.ts — per-rule issue/fixable counts, sorted most-impactful-first | | Plugin Summary | plugin-summary.ts — per-plugin rule/issue counts, from a caller-supplied rule-id → plugin mapping | | Execution Summary / Performance Metrics | execution-summary.ts — duration, files analyzed, rule errors, slowest rule invocations |

Rule Metadata

RuleMetadataInput (the rules field above) carries the full @devmedic/rule-engine Rule Metadata contract, not just id/title/category/severity/documentationUrl: fixable, sinceVersion, minimumRNVersion, estimatedFixTime, tags, and references all flow through to the matching RuleSummaryEntry when a rule is seeded from rules — they're absent for a rule seeded only from issues, since those fields don't exist on Issue itself.

apps/cli maps @devmedic/rule-engine's Rules (via extractRuleMetadata) onto this shape before calling generateReport — previously, rules was never populated by any CLI command at all, so a rule that ran and found nothing never appeared in the rule summary, and none of this metadata reached a report regardless of what a rule declared. formats/markdown.ts adds a "Tags" column to the Rule Summary table when at least one rule in the report declares tags.

Rule Failures

execution.ruleFailures (from @devmedic/rule-engine's AnalyzeResult.errors — pass it straight through) and execution.ruleExecutionSummary (from AnalyzeResult.ruleExecutionSummary) drive a dedicated Rule Failures section — every format that has an execution summary at all renders the full detail (rule, file, phase, reason, and stack when available), never just a bare "N rule error(s)" count with nothing behind it:

const report = generateReport({
  issues,
  execution: {
    durationMs,
    filesAnalyzed,
    ruleFailures: analyzeResult.errors, // full RuleExecutionFailure[] detail
    ruleExecutionSummary: analyzeResult.ruleExecutionSummary, // successful/failed/skipped rule ids
  },
});

report.execution?.ruleFailures; // readonly RuleFailureInput[]
report.execution?.ruleExecutionCounts; // { successful, failed, skipped, total }

| Format | Rule Failures rendering | | ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | cli | A Rule Failures (N) section per failure — rule, file, phase, reason, and the full stack, dimmed. | | markdown | A ## Rule Failures table (Rule / File / Phase / Reason), with each stack trace in its own <details>. | | html | A table matching the per-file issue tables, with the stack in a scrollable <pre>. | | sarif | runs[].invocations[0]executionSuccessful and one toolExecutionNotification per failure, the SARIF-native way to represent a tool's own execution errors (distinct from results, which are findings). | | github-actions-summary | The rule error count (previously silently dropped from this format entirely) plus a collapsible table. | | json | The full RuleExecutionFailure shape, serialized — error is already { name, message, stack }, not a raw Error (which loses those properties to JSON.stringify). |

Output formats

| Format | Notes | | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------ | | cli | Chalk-colored, degrades automatically in non-TTY environments. Score bars, severity-colored issue lines, grouped by file. | | json | The Report as-is, pretty-printed — the shape a future dashboard API would ingest directly. | | markdown | Full report: scores, statistics, rule/plugin summaries, issues grouped by file under headings. | | html | Self-contained (no external stylesheets/scripts), light/dark via prefers-color-scheme. | | sarif | SARIF 2.1.0. Severity maps to SARIF's 3 levels: info→note, warning→warning, error/critical→error (SARIF has no "critical"). | | github-actions-summary | Markdown for $GITHUB_STEP_SUMMARY; per-file issues in a collapsible <details> block, since a job summary is scanned, not read top to bottom. |

Future dashboard APIs

generateReport(input, { dashboard }) accepts a dashboard?: { projectId?, ingestUrl? } that's carried onto the report but otherwise inert — reserved so a report generated today won't need to reshape once a dashboard API exists. Until then, the json format itself is the dashboard-consumable shape.

Depends on

  • @devmedic/core
  • @devmedic/telemetry
  • @devmedic/issue-engine — the source of NormalizedIssue, and reused directly for grouping issues by file in the Markdown/GitHub Actions formats
  • chalk