Skip to main content

SCORECARD RUBRIC

The scorecard is a weighted rubric applied to every published extension. Each factor contributes a fixed number of points. The total is divided by the sum of all weights and expressed as a percentage and letter grade.

Factors

The current rubric, version 6, defines twelve active factors. The server-side divisor is 16. The registry and swamp extension quality score with the same package, @swamp-club/extension-scoring, so an archive gets the same rows from both.

# Factor ID Label Points Earned when
1 has-readme Has README or module doc 2 The tarball contains a README file (README.md, README.MD, readme.md, or Readme.md) at the extension root or under files/.
2 readme-example README has a code example 1 The README contains at least one fenced code block.
3 rich-readme README is substantive 1 The README is at least 500 characters and contains at least two fenced code blocks. A version with no README file shows the factor missing.
4 symbols-docs Most symbols documented 1 At least 80% of exported symbols across all entry points carry a JSDoc comment.
5 fast-check No slow types (deprecated) 1 Deprecated and not evaluated: earned by every analysed version.
6 description Has description 1 The extension's description field contains a character other than whitespace.
7 platforms Platform support declared (or universal) 2 The platforms array has one or more entries, or is empty. An empty array matches no platform filter in registry search. Single-platform manifests earn full credit — platform breadth is not a quality proxy.
8 has-license License declared 1 The manifest's license: is a valid SPDX expression. Only SPDX ids count; an invalid expression earns nothing.
9 license-file LICENSE file included 1 A non-empty license file (LICENSE, LICENSE.md, LICENSE.txt, COPYING, and common variants) is present at the tarball root or under files/.
10 repository-verified Verified public repository 2 The repository URL is set and points to a publicly accessible repository on an allowlisted host, confirmed via the host's public API — or the extension is private and its collective has private source hosting.
11 dependency-trust Dependencies pass trust audit 2 Every npm: and jsr: dependency is audited at the version that ships. Both points with no blocker, unaudited dependency or loose specifier; one point when loose specifiers are the only cost.
12 secret-safe-docs Docs keep secrets in a vault 1 No secret-unsafe-docs finding is left unaccepted in the packaged Markdown, the manifest description, or source describe strings.

The search API returns only extensions whose scored version ships a license file when called with licensed=true; an extension not yet scored, or whose scoring failed, is still returned.

The extension page shows has-license and license-file as a single License line scored out of 2, naming the part that is missing. The scorecard API reports them separately.

The maximum server-side score is 16/16 = 100% (Grade A). Every factor above is earnable by any publisher.

A scorecard is composed under the rubric version it was computed with until it is republished or rescored, which scores it under the current rubric. A version scored under rubric 3 has no secret-safe-docs row and a divisor of 15. The description and rich-readme rules above apply to every stored scorecard as it is read, whichever rubric scored it.

Factor status

Each row of the breakdown has one status:

Status Meaning
earned All of the factor's points.
partial Some of its points. Only dependency-trust can be partial: 1/2 when loose specifiers cost.
missing None of its points.
pending Not scored yet: the version has not been analysed. It shows every current factor, out of 16.
provisional Shown only by the CLI before publish; see below. It earns 0 until the registry verifies it.

The percentage is the earned points times 100 divided by the maximum, rounded down. The registry and swamp extension quality both divide by the full maximum.

Provisional factors in the CLI

Only the registry can confirm that a repository URL is a public repository (it calls the host's public API). When the URL is HTTPS on an allowlisted host, the CLI marks repository-verified as provisional: it earns 0 points locally and its 2 points stay in the divisor. An extension that earns every other factor scores 14/16 = 87% (Grade B) in the CLI, and the rubric line names what the pending points would raise it to:

Rubric v6 — 14/16 points (87%, grade B, +2 pending server verification: 100% (A) if the registry verifies them, all factors earned)

The registry awards the 2 points once verification succeeds. --json reports the pending points as provisionalPoints and the raised score as ifProvisionalEarned.

A private extension whose collective has private source hosting needs no verification: the CLI scores the factor earned, labelled "Private repository (collective attested)", whatever the repository URL. Otherwise the factor is missing. See Publishing § quality.

File placement for has-readme and license-file

Both file lookups are exact at the extension root and under files/ — the analyzer does not recurse. Entries in additionalFiles are preserved verbatim as the archive sub-path under extension/files/, so a nested entry like additionalFiles: [docs/README.md] lands at extension/files/docs/README.md and earns zero. Use a bare basename in additionalFiles. If your repository keeps each extension self-contained in its own subdirectory (manifest beside README beside LICENSE), opt into paths.base: manifest so the bare-basename entry resolves against the manifest's own directory.

When deno doc cannot analyse the source

The analyzer runs deno doc on the manifest's entry points with no import map. Push writes each bare import the project's Deno config resolves as its target specifier, and each bare import package.json mode resolves to an installed package as npm:<name>@<installed version>, so those resolve. When an import does not resolve — a bare import that shipped as written, a local module missing from the archive, or a remote host the scorer may not fetch from — the version is still scored:

  • symbols-docs earns 0.
  • has-readme is earned only by a README file, since module docs cannot be read.
  • fast-check is not evaluated and keeps its deprecated full credit.
  • Every other factor, including has-license and license-file, is scored as usual.

The reason appears under the factor breakdown on the extension page, and as docAnalysisError in the scorecard API response (null when the source resolved). It names at most three unresolved imports, with paths relative to the extension root.

Verified by Swamp badge

The Verified by Swamp badge is displayed as a shield pill next to the score ring on the extension detail page. It is awarded when either of the following is true:

  • The extension is published under the first-party @swamp namespace.
  • The latest version meta has verifiedBySwamp: true, set by a System Initiative admin. The admin curation endpoint is planned and not yet available.

The badge does not contribute points to the score. An extension can score 100% without the badge, and an extension can carry the badge at any score. See About the Extension Scorecard for why verification is a badge rather than a scored factor.

Grade thresholds

Grade Minimum score
A 90%
B 75%
C 60%
D 40%
F < 40%

Repository verification

The repository-verified factor is earned by verification for repositories hosted on one of the following allowlisted hosts:

Host API endpoint
github.com GET https://api.github.com/repos/:owner/:repo
gitlab.com GET https://gitlab.com/api/v4/projects/:url-encoded-path
codeberg.org GET https://codeberg.org/api/v1/repos/:owner/:repo
bitbucket.org GET https://api.bitbucket.org/2.0/repositories/:workspace/:repo

Verification calls the host's public API with the owner and repository path — the owner-supplied URL is not fetched directly. A 200 response from the host API with the repository marked public counts as verified. Any other outcome (404, private repository, rate limit, network error, malformed URL) counts as not verified. The result is cached either way; see the caching rules below for when a negative result is re-checked.

GitLab verification supports nested group paths by URL-encoding the full project path. The gitlab.com public SaaS instance is the only GitLab host recognised — self-hosted GitLab instances are covered by private source hosting instead.

Verification results are cached on the extension record: a verified result for 7 days, an unverified result for one hour. The cache is cleared and re-checked when the owner changes the repository URL. It is also re-checked when a scoring event fires (a new version is published or the owner triggers a manual rescore) and the stored result is older than its window. A successful verification of a URL is applied to every extension whose current repository field equals that URL, across all owners — one publish or rescore verifies every extension sharing the repo. A failed verification is recorded only on the extension that triggered the check; failures can be transient and never overwrite a sibling's verified state. The score worker does not re-verify on its own schedule — if no scoring event fires for any extension on the URL, a stale cache stays as-is until the next publish or rescore.

Private source hosting

A collective whose extension source lives on a private or self-hosted forge (GitHub Enterprise, self-hosted GitLab, Gitea, Forgejo, or a private repository on a public host) can be marked by Swamp Club for private source hosting. Every private extension that collective owns then earns repository-verified. The factor reads Private repository (collective attested) on the scorecard and in GET /api/v1/extensions/:name/score, and is worth the same 2 points.

Public extensions earn the factor only through a verified public repository, whether or not their collective has private source hosting. Swamp Club staff set private source hosting; ask through your account contact or the Swamp Club Discord to enable it for a collective.

Dependency trust audit

The dependency-trust factor reads every npm: and jsr: specifier the extension's code imports and audits each dependency at one version: the version that ships. See Version resolution.

Code means every .ts, .tsx, .mts, .cts, .js, .jsx, .mjs and .cjs file in the archive except tests: a file whose name ends _test or .test before its extension is not read. Only import and export statements and dynamic import() calls count. An import inside a comment, and a string that merely looks like a specifier, is not a dependency. A module that does not parse is not audited: its imports are unknown.

Source Data queried
OSV.dev Known npm vulnerabilities and their severity (LOW, MEDIUM, HIGH, CRITICAL, UNKNOWN)
npm registry Published versions, and the resolved version's deprecation, license, maintainers
npm downloads API Weekly download count
JSR (jsr.io) The package's published versions, and whether each is yanked

Two specifiers of one package that name different versions are audited separately. npm:zod is neither listed nor audited: the host provides it.

Version resolution

How the version is chosen depends on the archive's dependency-audit.json, which push writes at the archive root; see Publishing Commands § Dependency audit record. The registry reads it only as a regular file of at most 1 MiB.

Archives with a recordVersion: 2 record

Push writes every npm: and jsr: import in the archive's source at one exact version, which the record lists (the canonical form). The registry audits only an exact specifier the record lists, at that version, with resolvedFrom set to the entry's from. It reads no lockfile and resolves no range against the registry. A range, a dist-tag, an unpinned import, an exact version the record does not list, or one whose version the record does not match is not audited.

OSV, deprecation and yank checks still run on the listed version. Each entry in the record's unaudited list (a URL import, a git dependency, or a local package.json dependency) is not audited and named by its origin.

Archives with a recordVersion: 1 record, or none

Specifier Version audited resolvedFrom
Exact pin (1.2.3, =1.2.3, v1.2.3) That version exact
Any other specifier with a usable record entry The version the record lists for it The entry's from, or registry when it names no known source
Range, or no version, with a matching deno.lock entry The version the lock records for that specifier; otherwise the highest locked version the specifier allows lockfile
Range (^4.18.0, ~1.2, 4) npm: the highest published version it allows. JSR: the highest one that is not yanked, else a yanked match registry
Dist-tag (next) npm: the version the dist-tag points to. JSR has no dist-tags: version <tag> not found on JSR registry
No version npm: latest. JSR: the package's latest registry

Prereleases never satisfy a range. The rows apply in order: the first that matches decides.

A v1 record is a JSON object with recordVersion: 1 and a dependencies array; anything that is neither v1 nor v2 counts as no record. The registry uses a v1 entry for an import only when all of these hold:

  • registry is npm or jsr, and name is not empty.
  • version is a plain semver version, with no v or =.
  • The entry's spec is the import's version exactly as the source writes it (^2 and ^2.0.0 are different), and it admits the version: an exact spec names it, a range contains it, and a dist-tag or no spec admits any version.
  • It is the first usable entry for that registry, name and spec.
  • The registry publishes that version.

An entry that fails any of these is ignored, and the dependency resolves by the rows below it. A recorded JSR version is audited as recorded even when it is yanked. An archive with no record resolves every dependency by the other rows.

Only a deno.lock at the archive root is read, in lockfile format 4 or 5; a lockfile in a subdirectory belongs to another project and is ignored, as is a lock path in deno.json.

Findings

Blockers (any one costs both points):

  • The resolved version is deprecated on the npm registry, and so is latest: package is deprecated.
  • The resolved version is deprecated but latest is not: version 1.0.0 is deprecated: <npm notice>. A pin to a good version is not flagged when only latest is deprecated.
  • The resolved version has a HIGH, CRITICAL, or UNKNOWN severity vulnerability in OSV.dev.
  • The JSR package does not exist (JSR package not found), its name is not a valid JSR name (invalid JSR package name), or no published version matches the specifier (version 0.0.999 not found on JSR).
  • The resolved JSR version is yanked: version 0.225.0 was withdrawn (yanked) by its author on JSR. Newest version that isn't yanked: 1.0.0. The newest version is the newest stable release on the yanked version's major line (its minor line for 0.x), else the newest overall. With no other version, the message ends No other version is available.

Loose specifiers (cost one of the two points, however many there are):

  • A specifier that is unpinned, a range or a dist-tag, and that no valid lock fixed. A version a deno.lock decided, or a package-lock.json that node_modules matches, costs nothing: pinning exact versions and committing a lock score the same.

    • In a v1 archive, or one with no record: npm:express@^4.18.0 is a range and no lock fixed it; audited 4.21.1, the version the registry resolved, or the version push recorded when the record decided.
    • In a v2 archive, the record entry's from is registry, or installed with locked: false, and the finding names the spelling the author wrote: npm:ms@^2 is not an exact version and no lock fixed it; audited 2.1.3, the version push recorded.

    Listed with the warnings.

Warnings (recorded on the scorecard; cost no points):

  • MEDIUM severity vulnerability. A LOW one is not reported.
  • License of the resolved version not in the allowlist: license "GPL-3.0" not in allowlist. The license is read as an SPDX expression, and passes when every license in it is one of MIT, Apache-2.0, BSD-2-Clause, BSD-3-Clause, ISC, 0BSD, MPL-2.0, Unlicense and CC0-1.0, so MIT OR Apache-2.0 passes. A license with an exception (WITH), an -or-later or + license, an expression that does not parse, and no license at all are warned about.
  • No maintainers listed for the resolved version on the npm registry.
  • Fewer than 1,000 weekly downloads.
  • The package's latest version was published more than 24 months ago, a month being 30.44 days.

License and maintainers come from the resolved version's npm entry, falling back to the package-level field when that entry has none.

Not audited (any one costs both points, and is listed by name with the reason on the scorecard and as dependencyTrust.notAudited in GET /api/v1/extensions/:name/score):

  • The npm registry lookup failed, including a package npm does not know.
  • The package name is not a valid npm name: invalid npm package name. It is not looked up.
  • The resolved version is not one npm lists, or npm lists it without a manifest: npm lists no version 9.9.9.
  • The OSV.dev lookup failed, OSV.dev answered with a vulns field that is not a list, or OSV.dev could not be asked because no published version matches the specifier.
  • The JSR lookup failed: JSR lookup failed (HTTP 503).
  • The dependency is past the 50-dependency query limit. The 50 audited are the first in registry, name and version order.
  • In a v2 archive, a specifier breaks the canonical form: npm:ms@^2.1.0 is not in the canonical form: it must name one exact version, such as npm:ms@1.2.3, or npm:ms@2.1.3 is not in the canonical form: dependency-audit.json lists no npm:ms@2.1.3.
  • In a v2 archive, code from outside npm and JSR, listed under its origin: a URL import, from outside npm and JSR: no registry audit covers it. Replace it with an npm: or jsr: package. A git dependency (git…, github:, gitlab:, bitbucket: or user/repo) reads a git dependency, and a file:, link:, workspace:, portal: or path dependency a local package.json dependency.
  • A code module that does not parse, listed under its archive path: Its imports cannot be read: the module does not parse, so its dependencies were not audited.

An exact pin is checked against OSV.dev even when the npm registry lookup fails, so a known vulnerability in it is listed with the blockers as well as the dependency being not audited.

A failed weekly download lookup does not leave a dependency unaudited; the download warning is skipped instead.

Score API fields

dependencyTrust in GET /api/v1/extensions/:name/score carries:

Field Content
dependencies Each specifier found. npm entries are unprefixed (ms@2.1.3); JSR entries are prefixed (jsr:@std/assert@1). npm:zod is neither listed nor audited.
audited One entry per dependency: registry, packageName, specifier, version (null when none resolved) and resolvedFrom (exact, installed, lockfile, registry, or null; a version the record decided reports the entry's from).
blockers packageName, reason, and cause where one applies: deprecated-version, jsr-yanked, jsr-missing.
warnings packageName, reason, and cause where one applies: loose-specifier.
notAudited packageName and reason. packageName is the package, or the archive path of a module that does not parse, or the origin of code from outside npm and JSR.
passed Whether there is no blocker and no dependency not audited. true when a loose specifier costs one point.
earnedPoints The factor's points: 2, 1 or 0. null while the factor is pending.

At push

swamp extension push and swamp extension quality run the same audit with the same rules, at the version the push ships, and push records that version in the archive so the registry audits the same one. The loose specifier warning reads no lock: ^2.1.0 resolved to 2.1.3 today; commit a lock or pin exact versions (costs 1 point). Code from outside npm and JSR is not audited there too: not audited: https://example.com/x.ts is a URL import, from outside npm and JSR, so no registry audit covers it.

In package.json mode a bare import is audited at the version installed in node_modules, each installed copy separately. A package-lock.json counts as a lock only when node_modules matches it, transitive packages included, and only when the project has no deno.json; otherwise push warns with package-lock-mismatch.

The CLI reads the lockfile its bundler honours, not one at the archive root:

Project Lockfile read
deno.json with lock set to a path That path, relative to the deno.json
deno.json with lock: { "path": … } That path, relative to the deno.json
deno.json with lock: false None
deno.json with no lock The deno.lock beside it
A config that cannot be parsed None
A workspace member The workspace root's lock
package.json mode The deno.lock in the package.json dir
Neither None; the bundler runs with --no-lock

deno.json here also means deno.jsonc; both are read as JSONC. In package.json mode the lock applies only to npm: and jsr: imports written in the source; a bare import resolved to an installed package is audited at the installed version. With no lockfile, Deno resolves a loose specifier from its cache or the registry, and push ships and records that version.

The CLI scores the factor on the same findings as the registry: both points are lost to any error, a deprecated-version or jsr-yanked warning, or a dependency not audited; otherwise one point is lost when any dependency has a loose-specifier warning, however many do. A deprecated pin therefore shows the factor as 0/2 with 1 dependency blocker(s) found, and a loose specifier alone as 1/2, while dependencyTrust.passed in --json stays true: passed says only that nothing blocks the push.

A push prompts before publishing an unaudited dependency or any warning with a cause (deprecated-version, loose-specifier, jsr-yanked); the other dependency warnings are advisory. See Waived warnings.

The audit runs at publish time and again on a rescore, which re-queries every dependency and replaces the findings. A rescore reads the same archive, so a version the record decided stays the version audited while the registry still publishes it. Up to 50 dependencies are queried per extension. An extension with no npm: or jsr: dependencies earns the factor, unless a module does not parse or the record lists code from outside npm and JSR.

Secret-safe docs

The scorer reads the documentation a user copies from: every .md file in the archive, the manifest description, and the strings passed to .describe(...) or set as description: in every .ts file that is not a test, include files and modules outside the kind directories among them. Three kinds of Markdown are not documentation and are not read: a binaries: entry, an include: entry, and a .md file a module imports by a relative path, which is data. Lines longer than 2,000 characters are skipped. A scan that does not read everything is incomplete and does not earn the factor: a file over 1 MiB is not read, an archive with more than 10,000 such files is not read past them, a file or directory the scorer cannot open is skipped, and the scan stops at 1,000 unaccepted findings.

It reports a secret-unsafe-docs finding for each line that:

  • uses vault reference syntax Swamp does not resolve: vault://, <vault:, {{ vault ... }} without the leading $, or swamp vault set
  • sets a secret --global-arg or --input to anything other than a ${{ ... }} expression. An argument is a secret when its schema marks it .meta({ sensitive: true }), or when no schema declares it and its name reads as a token, key, secret or password. The schemas read are the models' global arguments and method arguments, the methods an extension module adds to another model type, and vault and datastore config fields, as push sends them and as the registry reads them from the published archive. A field either marks sensitive is a secret, so a version stored before the registry kept the mark, or pushed by a CLI that does not send it, is judged by its schema on a rescore. true and false values pass.
  • passes the value to swamp vault put on the command line, unless the command uses --server or the value is a shell variable ($TOKEN, "${TOKEN}")
  • contains a full-length credential: a GitHub, GitLab, Anthropic, AWS, Slack, HashiCorp Vault or Swamp API key, a bearer token, or a user:pass@ URL. A run of one repeated character, and a URL on localhost, a loopback address or an example.com, example.org or example.net host, pass.

In Markdown, credentials count anywhere; the other patterns count only inside fenced code blocks. A command continued with a trailing backslash is checked as one line and reported on its first line.

A finding is accepted when the push that published the version declared it accepted: the scorecard reads the acceptances from the version's content metadata. A Markdown or source finding is accepted by an entry for the same file and line; a manifest description finding by any secret-unsafe-docs acceptance on the manifest. The factor is earned when no finding is left.

secretDocs in GET /api/v1/extensions/:name/score carries:

Field Content
passed Whether no finding is left unaccepted.
findings Up to 50 unaccepted findings: file (the archive path, absent for the manifest description), line, and message. A description finding's message starts The description.
count The number of unaccepted findings, including any past the first 50.
truncated true when the scan was incomplete; the factor is then not earned and count is a lower bound.

secretDocs is absent on a scorecard computed under rubric 3. swamp extension quality and swamp extension push report the same findings with the same wording; see Publishing § quality.

Scoring locally as the registry does

swamp extension quality scores with the same rubric. Its --isolated flag runs deno doc with an empty dependency cache and no registry credentials, as the registry does, so an import that resolves only locally fails there too. See Publishing § quality.

Factor data sources

Factors are evaluated against two kinds of data:

Data scope Stored on Updated by
Per-version Version meta record Tarball analyzer at publish time; re-run on the owner's "Rescore" action.
Per-package Extension record Read live from the extension's metadata. Edits to description, platforms, repository and homepage update the score instantly.
Verification Extension record Repository verifier. Verified results kept 7 days, unverified one hour; re-checked on URL change, publish, or rescore.

Per-version factors: has-readme, readme-example, rich-readme, symbols-docs, fast-check, has-license, license-file, dependency-trust, secret-safe-docs.

Per-package factors: description, platforms.

Verification factor: repository-verified.

The Verified by Swamp badge is not a factor and does not appear in the breakdown. It is evaluated from the extension's namespace and the admin-set verifiedBySwamp flag on the latest version meta.

Rubric stability

Extensions published under an older rubric version are not re-analysed when the rubric changes. Their stored per-version meta is composed on each page load under the rubric version it was scored with: its factors, and its divisor. A new publish or a rescore analyses the version again under the current rubric.