Skip to main content

IMPROVE YOUR SCORE

This guide shows you how to raise an extension's quality score. Each section addresses one factor from the scorecard rubric. Work through the sections for factors your extension does not currently earn.

Before you start

Open the extension's detail page and look at the factor breakdown. Everything marked "earned" can be skipped — only the gaps are worth your time.

If you change a per-version factor (README content, JSDoc coverage), you will need to either publish a new version or click "Rescore" on the owner panel. Changes to per-package fields (description, platforms, repository) take effect immediately; no rescore needed.

Ship a README

Worth: 2 points.

Create a README.md at the root of your extension and list it under additionalFiles in the manifest:

additionalFiles:
  - README.md

The scorer accepts the file either at the extension root or under files/ in the packaged tarball. Keeping it at repo root also means GitHub (and the other allowlisted hosts) render it on the repository page.

The analyzer accepts README.md, README.MD, readme.md, and Readme.md. Any other casing (for example ReadMe.md) is not detected.

The entry must be a bare basename. Each additionalFiles entry is copied verbatim as the archive sub-path under extension/files/, and the analyzer does not recurse — additionalFiles: [docs/README.md] lands at extension/files/docs/README.md and earns zero. If your repository keeps each extension self-contained in its own subdirectory (manifest beside README), opt into paths.base: manifest so the bare-basename entry resolves against the manifest's own directory.

Add a code example

Worth: 1 point.

Include at least one fenced code block in the README that shows the extension in use:

```ts
import { validate } from "@mycollective/my-ext";

validate({ region: "us-east-1" });
```

Any fenced block counts. A realistic usage snippet is more useful to readers than a trivial one.

Make the README substantive

Worth: 1 point.

The README must be at least 500 characters and contain at least two fenced code blocks. A common shape is one block showing how to invoke the extension and a second showing the expected output or a workflow snippet that consumes it.

Any README with a short intro and two real examples clears the threshold.

Document your exports

Worth: 1 point.

At least 80% of symbols exported from your entry points must carry a JSDoc comment. One sentence each is enough:

/** Validate the behavior's inputs against the schema. */
export function validate(input: Input): ValidationResult {
  // ...
}

Only exported symbols are counted. Internal helpers that are not re-exported from the entry point do not affect the percentage.

If you are close to the threshold, favour documenting the symbols that consumers will actually call — public types, factory functions, and the main entry function — rather than spreading comments thin across everything.

Slow types need no fix

Worth: 1 point, already earned.

The fast-check factor is deprecated. Every analysed version earns it, so there is nothing to fix: you do not need to run deno doc --lint, add return types to exports, or rewrite exported zod schemas and z.infer types for it.

Fix imports the scorer cannot resolve

If the scorecard says deno doc could not analyse this version's source, the documentation factors score 0 until every listed import resolves. swamp extension quality reports the same failure before you publish. An import that resolves locally only through your Deno cache or registry credentials fails on the registry; run swamp extension quality manifest.yaml --isolated to check with an empty cache and no credentials, as the registry does.

  1. Give each bare import an explicit specifier. Either resolve it through the project's Deno config (deno.json or deno.jsonc imports, an importMap file, or a scope), which push writes in its place in the shipped source, or write the specifier in the source. swamp extension push warns about each one that ships as written as bare-specifiers:

    { "imports": { "zod": "npm:zod@4" } }
    // ships as npm:zod@4 with the entry above; without it the registry cannot resolve it
    import { z } from "zod";

    In a package.json project, declare the package as an npm registry dependency and run npm install: push ships the import pinned to the installed version. A file:, link:, workspace:, git or URL dependency ships as written and also costs the dependency-trust factor; write an npm: or jsr: specifier in the source instead.

  2. For a module listed as "not in the published archive", check that it sits where the import expects it relative to the importing file. If it does and swamp extension push still leaves it out, report it with swamp issue bug.

  3. For a host the scorer may not fetch from, import the package from npm: or jsr: instead. The scorer also fetches from deno.land, esm.sh, cdn.jsdelivr.net and raw.githubusercontent.com, but a URL import from any host costs the dependency-trust factor; see Pass the dependency trust audit.

  4. Publish a new version.

Fill in the description

Worth: 1 point.

Set the description field in manifest.yaml:

description: "Manage AWS S3 buckets — create, upload, invalidate, delete."

One sentence is fine; a description of only spaces or line breaks does not count. This is a per-package field — editing it on the owner panel updates the score instantly, no republish required.

Declare platforms

Worth: 2 points.

Set the platforms field in manifest.yaml:

platforms:
  - linux-x86_64
  - darwin-aarch64
  - linux-aarch64

Or leave it empty:

platforms: []

An empty array earns the full 2 points. Any explicit platform list — even a single entry — also earns the full 2 points. Platform breadth is not a quality signal; what matters is that the manifest honestly declares what the extension supports.

Registry search matches the platforms you list. An extension with an empty list appears only when no platform filter is set, so list every platform you want it found under.

Declare a license

Worth: 2 points, one for each of these. A license is optional; nothing is refused or hidden without one.

  • License declared: set license: in manifest.yaml to an SPDX expression. Only SPDX ids count. An invalid expression is reported as a warning at push and earns nothing.

    license: MIT
  • LICENSE file included: ship the license text in a non-empty license file. The analyzer looks at the extension root and under files/ for any of these filenames:

    • LICENSE, LICENSE.md, LICENSE.txt
    • License, License.md, License.txt
    • license, license.md, license.txt
    • COPYING, COPYING.md, COPYING.txt

    List it under additionalFiles:

    additionalFiles:
      - README.md
      - LICENSE

    The entry must be a bare basename — the same rule that applies to README. A nested entry like additionalFiles: [legal/LICENSE] lands at extension/files/legal/LICENSE and earns zero. Use paths.base: manifest if your LICENSE sits beside a per-directory manifest.

Both are read from the version, so they change with a new version. See Declare an Extension License.

Worth: 2 points (server-verified for a public repository; provisional in the CLI until publish).

Only the registry can verify a public repository. When the CLI expects the registry to award the factor, the CLI marks it as provisional, shown with a ~ marker: 0 points locally, still counted in the divisor. An extension that earns everything else scores 87% (Grade B) locally, and the rubric line adds 100% (A) if the registry verifies them. The full 2 points are awarded server-side on publish.

Set the repository field in manifest.yaml to a public repository on an allowlisted host:

repository: https://github.com/mycollective/swamp-s3

Allowlisted hosts are github.com, gitlab.com, codeberg.org, and bitbucket.org. Verification runs automatically within a few seconds of publish and re-runs when the URL changes. A verified result is cached for 7 days and an unverified one for an hour. A successful check applies to every extension whose repository is set to the same URL — if several of your extensions live in one repo, publishing or rescoring any one of them verifies the rest.

Private or self-hosted source

If your collective publishes private extensions from a self-hosted instance (GitHub Enterprise, self-hosted GitLab, Gitea, Forgejo) or from private repositories, ask Swamp Club to mark the collective for private source hosting. Every private extension the collective owns earns the 2 points, and the scorecard reads Private repository (collective attested). A public extension earns them only through a verified public repository.

swamp extension quality scores the factor earned with that label for an extension that will publish private, whatever its repository URL, so a local 100% gate passes. It needs a login to ask Swamp Club about the collective. To publish a new extension private, set visibility: private in manifest.yaml, or publish it to a private namespace. A republish keeps the visibility already published. See Publishing § quality for how the CLI decides.

If you have set the URL but the factor is not earned, check that:

  • The URL points to a public repository on one of the four allowlisted hosts, or the extension is private and your collective has private source hosting.
  • When the remediation says private source hosting could not be checked, you are logged in (swamp auth login) and can reach Swamp Club.
  • The URL uses https.
  • The owner and repository path are correct — GitLab nested group paths must resolve to a real project.

Pass the dependency trust audit

Worth: 2 points.

The scorer extracts every npm: and jsr: import from your code and audits each at the version that ships. Test files are not audited, and an import in a comment is not a dependency. A blocker or a dependency that was not audited costs both points; a loose specifier costs one.

Pin exact versions or commit a lock. An unpinned, range or dist-tag specifier that no lock fixed costs one of the two points, however many there are. Either write npm:express@4.21.1 instead of npm:express@^4.18.0, or commit the deno.lock beside your deno.json; in a package.json project, commit a package-lock.json that matches node_modules. A lock also fixes the transitive versions the bundle holds. npm:zod@4 is the exception; zod is not audited.

Replace code from outside npm and JSR. A URL import, a git dependency or a local file: dependency costs both points: no registry audit covers it. Publish the code as an npm: or jsr: package and import that instead, or move it into the repository and import it relatively.

Publish with swamp extension push. Push writes every import at the exact version it bundled. A source edited by hand after push, so that it writes a range or a version dependency-audit.json does not list, is not audited.

Fix a module that does not parse. Its imports cannot be read, so the factor is withheld until it parses.

Fix blockers first — each costs both points:

  1. Replace deprecated packages with their maintained successors, and move a pin to a deprecated version onto one that is not deprecated. A deprecated pin only warns at push, but it costs both points.
  2. Update packages that have HIGH or CRITICAL severity advisories on OSV.dev. If no patched version exists, switch to an alternative package.
  3. Fix a jsr: import whose package or version does not exist: check the name and the version against the package's page on jsr.io.
  4. Move a jsr: import off a yanked version onto one that is not yanked. A yanked version only warns at push, but it costs both points.

The other warnings cost no points, but swamp extension quality reports them so you can assess the risk:

  • MEDIUM severity advisories.
  • A license that is not an SPDX expression of allowlisted licenses (MIT, Apache-2.0, BSD-2-Clause, BSD-3-Clause, ISC, 0BSD, MPL-2.0, Unlicense, CC0-1.0), or no license at all. MIT OR Apache-2.0 passes.
  • Fewer than 1,000 weekly downloads on npm.
  • Last published more than 24 months ago.
  • No maintainers listed on the npm registry.

Rescore a dependency that was not audited. When the npm registry, OSV.dev or JSR lookup for a dependency fails, the scorecard lists it as not audited and the factor is withheld, the same as a blocker. swamp extension quality marks it ? with not audited: and the failed lookup. Once npm, OSV.dev and JSR are reachable, press Rescore on the extension page, or run swamp extension quality again locally. If the reason is HTTP 404 from the npm registry, check the package name in the import — npm does not know it.

If the reason is OSV not queried (no published version matches <spec>), rescoring does not help: no version of the package satisfies the specifier. Change the specifier to a version the package has published, then publish a new version.

If your extension has no npm: or jsr: dependencies, no code from outside npm and JSR, and every module parses, the factor is earned automatically.

After fixing blockers, publish a new version or click "Rescore" on the owner panel.

Keep secrets out of the docs

Worth: 1 point.

The extension page lists each secret-unsafe-docs finding under Docs keep secrets in a vault, with its file and line. swamp extension quality reports the same findings before you publish.

Store the secret with a prompt. Replace a command that puts the value on the command line with one that prompts for it:

swamp vault put my-vault api-token

Pass the secret as a vault expression. Replace a literal, a placeholder, or vault:// syntax with a ${{ ... }} expression:

swamp behavior create @acme/artifactory my-artifactory \
  --global-arg 'token=${{ vault.get(my-vault, api-token) }}'

Use swamp vault put, not swamp vault set.

Remove real credentials. Replace a token, key or password in the docs with a placeholder of one repeated character (glpat-xxxxxxxxxxxxxxxxxxxx), or a URL on an example.com host.

Mark secret arguments sensitive. An argument named like a secret is treated as one unless its schema declares it. Declare the argument, and mark it .meta({ sensitive: true }) when it holds a secret.

Split a Markdown file over 1 MiB. A file that size is not read, and docs the scorer has not read do not earn the factor.

Accept a line that shows the wrong form on purpose. For a line inside a fenced code block, add a quality.yaml entry beside manifest.yaml naming the file and line:

version: 1
accept:
  - rule: secret-unsafe-docs
    file: README.md
    line: 42

For a manifest description finding, add an entry naming manifest.yaml. See Publishing § quality for the other acceptance forms.

Publish a new version. A rescore reads the acceptances the published version was pushed with, so an acceptance added after publishing counts only from the next version.

Troubleshooting: "My score did not move"

The most common cause is the tarball layout. The Swamp CLI packages a published extension like this:

extension/
  manifest.yaml
  models/                ← behavior files land here
    your_behavior.ts
  files/                 ← additionalFiles land here
    README.md
    LICENSE

Files you list under additionalFiles end up under files/ in the archive. The scorer looks for README.md and license files at both the extension root and under files/, so either layout works — but if your local filesystem has a README.md that is not listed in additionalFiles, it will not be in the tarball at all.

The lookups at both locations are exact — the analyzer does not recurse into subdirectories under files/. Each additionalFiles entry is copied verbatim as the archive sub-path, so a nested entry like additionalFiles: [docs/README.md] lands at extension/files/docs/README.md and earns zero. The fix is almost always to rewrite the entry as a bare basename. For per-extension-subdir layouts, opt into paths.base: manifest. An entry must name a file: listing a directory such as docs stops quality and push with Additional file is a directory: docs (...), so list each file you want shipped.

If has-readme is reporting 0/2 and you are sure you shipped one:

  1. Confirm the manifest lists README.md under additionalFiles.
  2. Confirm the entry is a bare basename, not a nested path like docs/README.md.
  3. Republish, or click "Rescore" on the owner panel.
  4. Recheck the detail page — the factor should update within a few seconds.

Apply the same steps for license-file if the license file factor is not earning.

Rescore after edits

Per-version factors (has-readme, readme-example, rich-readme, symbols-docs, fast-check, has-license, license-file, dependency-trust, secret-safe-docs) are only re-evaluated when a new version is published or when the owner clicks "Rescore" on the detail page. Editing per-package fields on the owner panel does not retrigger per-version analysis.

Per-package factors (description, platforms) update instantly on edit.

The repository-verified factor re-runs immediately when the URL changes. Otherwise it re-checks at the next publish or rescore, but only if the stored result is more than 7 days old (one hour for an unverified result). A successful check fans out to every extension sharing the same repository URL, so one publish or rescore heals all siblings in a shared repo. There is no separate periodic sweep — if nothing triggers a scoring event for any extension on the URL, a stale cache stays put.