UNPKG

signalk-server

Version:

An implementation of a [Signal K](http://signalk.org) server for boats.

261 lines 8.63 kB
"use strict"; /* * Copyright 2026 Signal K contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 */ Object.defineProperty(exports, "__esModule", { value: true }); exports.computeIndicators = computeIndicators; // Per-check weight by check id (kebab-case to match the ids the // scoring functions emit). Single source of truth for the composite. const WEIGHTS = { 'tests-pass': 30, 'open-issues': 20, 'actively-maintained': 15, documentation: 15, 'community-adoption': 10, 'visual-assets': 10 }; // Per-check thresholds. Pulled out so reviewers can tune the scoring // rules without touching the rule bodies, and so a future calibration // pass can see all the dials in one place. const OPEN_ISSUES_WARN_THRESHOLD = 50; const MAINTENANCE_OK_DAYS = 180; const MAINTENANCE_WARN_DAYS = 365; const README_OK_LENGTH = 2000; const README_WARN_LENGTH = 500; const COMMUNITY_OK_THRESHOLD = 4; const COMMUNITY_WARN_THRESHOLD = 2; const UNKNOWN_SUBTITLE = 'Unable to determine — no signal available'; function neutralCheck(id, title) { // Marked unknown so the UI doesn't render a green pass and so the // scoring layer excludes it from the denominator. return { id, status: 'warn', title, subtitle: UNKNOWN_SUBTITLE, unknown: true }; } function daysSince(iso) { const t = Date.parse(iso); if (Number.isNaN(t)) return undefined; return (Date.now() - t) / (1000 * 60 * 60 * 24); } function scoreTestsPass(inputs) { if (inputs.testsPass === undefined) { return neutralCheck('tests-pass', 'Plugin test suite'); } if (inputs.testsPass) { return { id: 'tests-pass', status: 'ok', title: 'Plugin test suite', subtitle: 'Signal K plugin test suite passes' }; } return { id: 'tests-pass', status: 'fail', title: 'Plugin test suite', subtitle: 'Plugin test suite is failing or has not run' }; } function scoreOpenIssues(inputs) { if (!inputs.hasRepository || inputs.openIssues === undefined) { return neutralCheck('open-issues', 'Open issues'); } if (inputs.openIssues > OPEN_ISSUES_WARN_THRESHOLD) { return { id: 'open-issues', status: 'warn', title: 'Open issues', subtitle: `${inputs.openIssues} open issues reported` }; } return { id: 'open-issues', status: 'ok', title: 'Open issues', subtitle: inputs.openIssues === 0 ? 'No open issues reported' : `${inputs.openIssues} open issues reported` }; } function scoreActivelyMaintained(inputs) { if (!inputs.lastReleaseDate) { return neutralCheck('actively-maintained', 'Actively maintained'); } const days = daysSince(inputs.lastReleaseDate); if (days === undefined) { // The release date came back unparseable — neutral (unknown) is // closer to the truth than rendering "Last release Infinity days // ago" and downgrading the maintenance check. return neutralCheck('actively-maintained', 'Actively maintained'); } if (days < MAINTENANCE_OK_DAYS) { return { id: 'actively-maintained', status: 'ok', title: 'Actively maintained', subtitle: `Last release ${Math.floor(days)} days ago` }; } if (days < MAINTENANCE_WARN_DAYS) { return { id: 'actively-maintained', status: 'warn', title: 'Actively maintained', subtitle: `Last release ${Math.floor(days)} days ago` }; } return { id: 'actively-maintained', status: 'fail', title: 'Actively maintained', subtitle: `Last release ${Math.floor(days)} days ago` }; } function scoreDocumentation(inputs) { const len = (inputs.readme || '').trim().length; const hasDesc = !!(inputs.description && inputs.description.trim().length > 0); if (len === 0 && !hasDesc) { return neutralCheck('documentation', 'Documentation coverage'); } if (len >= README_OK_LENGTH) { return { id: 'documentation', status: 'ok', title: 'Documentation coverage', subtitle: 'README provides substantial documentation' }; } if (len >= README_WARN_LENGTH) { return { id: 'documentation', status: 'warn', title: 'Documentation coverage', subtitle: 'README is present but brief' }; } if (hasDesc) { return { id: 'documentation', status: 'warn', title: 'Documentation coverage', subtitle: 'Only a package description is available' }; } // The 'len === 0 && !hasDesc' case returned neutralCheck above; by here // !hasDesc is true and len > 0, so the README is present but below the // warn threshold. return { id: 'documentation', status: 'warn', title: 'Documentation coverage', subtitle: 'README is present but very short' }; } function scoreCommunityAdoption(inputs) { if (inputs.stars === undefined && inputs.downloadsPerWeek === undefined) { return neutralCheck('community-adoption', 'Community adoption'); } const stars = inputs.stars ?? 0; const dl = inputs.downloadsPerWeek ?? 0; const score = Math.log10(1 + stars) + Math.log10(1 + dl); if (score >= COMMUNITY_OK_THRESHOLD) { return { id: 'community-adoption', status: 'ok', title: 'Community adoption', subtitle: 'Broad community usage' }; } // Moderate community usage is intentionally 'ok' (passing), not 'warn'. // Most Signal K plugins are niche-by-design (a single sensor driver, a // specific autopilot integration) and never hit broad-adoption stars/ // downloads numbers. Treating moderate adoption as a warning would // unfairly penalise the long tail of legitimate niche plugins; the // 'broad' tier remains as the upper signal for plugins that did break // out. Only the third branch below (score < WARN_THRESHOLD) actually // warns. if (score >= COMMUNITY_WARN_THRESHOLD) { return { id: 'community-adoption', status: 'ok', title: 'Community adoption', subtitle: 'Moderate community usage' }; } return { id: 'community-adoption', status: 'warn', title: 'Community adoption', subtitle: 'Limited or early-stage adoption' }; } function scoreVisualAssets(inputs) { if (inputs.hasScreenshots || inputs.hasAppIcon) { return { id: 'visual-assets', status: 'ok', title: 'Visual assets', subtitle: inputs.hasScreenshots ? 'Screenshots available' : 'App icon provided' }; } return { id: 'visual-assets', status: 'warn', title: 'Visual assets', subtitle: 'No screenshots or app icon declared' }; } const STATUS_CONTRIBUTION = { ok: 1, warn: 0.5, fail: 0 }; function computeIndicators(inputs) { const checks = [ scoreTestsPass(inputs), scoreOpenIssues(inputs), scoreActivelyMaintained(inputs), scoreDocumentation(inputs), scoreCommunityAdoption(inputs), scoreVisualAssets(inputs) ]; let totalWeight = 0; let earned = 0; for (const c of checks) { // Unknown checks contribute neither weight nor score — a plugin // with no usable signal shouldn't get a misleadingly high or low // composite, just a smaller-but-honest one. if (c.unknown) continue; const w = WEIGHTS[c.id] ?? 0; earned += w * STATUS_CONTRIBUTION[c.status]; totalWeight += w; } const score = totalWeight === 0 ? 0 : Math.round((earned / totalWeight) * 100); const rawMetrics = { stars: inputs.stars, downloadsPerWeek: inputs.downloadsPerWeek, openIssues: inputs.openIssues, contributors: inputs.contributors, lastReleaseDate: inputs.lastReleaseDate }; return { score, checks, rawMetrics }; } //# sourceMappingURL=indicators.js.map