Skip to main content
← Back to list
01Issue
BugTriagedSwamp CLI
Assigneesstack72

Relationships

#505 extension quality fails to resolve bare specifiers — contradicts fmt no-import-prefix rule

Opened by webframp · 5/31/2026

Description

swamp extension quality fails when an extension uses bare specifiers (e.g., import { z } from "zod") because the quality scorer extracts the extension tarball to a temp directory without a deno.json import map. The deno doc --json call in the temp dir cannot resolve the bare specifier.

This contradicts swamp extension fmt, which enforces the no-import-prefix lint rule requiring bare specifiers (rejecting "npm:zod@4"). It also contradicts swamp extension push --dry-run, which succeeds because the bundler resolves against the repo's deno.json.

Steps to Reproduce

  1. Create an extension with import { z } from "zod" in mod.ts
  2. Add a deno.json at the repo root with { "imports": { "zod": "npm:zod@4" } }
  3. Run swamp extension fmt manifest.yaml --check — passes
  4. Run swamp extension push manifest.yaml --dry-run — passes
  5. Run swamp extension quality manifest.yaml — fails with:
Warning Import "zod" not a dependency
  hint: If you want to use the npm package, try running `deno add npm:zod`
error: Failed resolving 'zod' from '.../swamp_quality_.../extract/extension/models/mod.ts'

Expected Behavior

quality should resolve bare specifiers the same way push does — either by generating a deno.json in the temp extraction directory from the manifest's dependency metadata, or by running deno doc against the bundled output instead of raw source.

Environment

  • swamp version: 20260531.214843.0-sha.ec32aaab
  • OS: macOS (Darwin 24.6.0)
  • deno: $(deno --version | head -1)

Notes

The dependencies manifest field now validates that entries must include a slash (e.g., @collective/name), so it cannot be used as a workaround to declare npm deps for quality resolution purposes.

02Bog Flow
OPENTRIAGEDIN PROGRESSSHIPPED+ 1 MOREASSIGNEDCLASSIFICATION

Triaged

6/1/2026, 12:28:45 AM

Click a lifecycle step above to view its details.

03Sludge Pulse
keeb assigned keeb6/1/2026, 12:26:44 AM
keeb assigned stack726/1/2026, 12:36:02 AM
keeb unassigned keeb6/1/2026, 12:36:02 AM
Editable. Press Enter to edit.

webframp commented 6/1/2026, 12:17:38 AM

Follow-up: this also blocks push, not just quality.

The contradiction is tighter than originally reported:

  • push enforces no-import-prefix → rejects import { z } from "npm:zod@4"
  • The scorer (and quality) runs deno doc without an import map → rejects import { z } from "zod"

So there is currently no valid import specifier for zod that satisfies both the push linter and the scorer. The v2 publish at 2026.05.31.2 went through because the push linter was not yet enforcing no-import-prefix in that version, but the scorer still fails on it.

Workaround: publish with bare "zod" (passes push), accept the scorer failure, and wait for the fix.

webframp commented 6/1/2026, 12:19:57 AM

Confirmed this also affects the swamp.club post-publish scorer (the "Stats" panel on the extension page). After successfully pushing @twonines/coder-* extensions with bare "zod", the scorer shows:

deno doc failed (code 1): Warning Import "zod" not a dependency

So the contradiction is: push requires bare specifiers (no-import-prefix lint), but the post-publish scorer runs deno doc without an import map. No valid specifier satisfies both — extensions that push successfully still get a scorer failure on swamp.club.

Affected: all 5 @twonines/coder-* extensions at v2026.05.31.3.

keeb commented 6/1/2026, 12:35:07 AM

Triage: confirmed bug — root cause + decision needed

Classified as a bug (high confidence, not a regression). Handing the fix decision to @stack72.

Root cause

scoreExtensionTarball (src/domain/extensions/extension_rubric_scorer.ts:580-654) deliberately strips the extension's deno.json (STRIPPED_CONFIG_FILES, lines 545-552) and writes a hermetic CONTROLLED_DENO_JSON containing only { "nodeModulesDir": "auto" } with no imports map (lines 539-543). It then runs deno doc --json/--lint against the raw .ts source in that temp dir with no --config and no import map, so bare specifiers like zod can't resolve.

By contrast push's bundleExtension (src/domain/models/bundle.ts:340-344) passes --config <repo deno.json>, so the repo import map resolves the same bare specifiers — which is why push --dry-run and fmt (which requires bare specifiers via no-import-prefix) both pass while quality fails. They're genuinely contradictory today.

Not a regression: CONTROLLED_DENO_JSON has lacked an imports map since the quality command landed in #1218.

The catch: server-parity invariant

quality.ts:90-96 guarantees the local score matches what the swamp-club registry computes server-side, and the scorer explicitly mirrors swamp-club's DenoDocExtensionTarballAnalyzer. Since that analyzer applies the same strip-and-control-config hermeticity, the server fails on bare specifiers too — so a purely-local fix would make quality pass locally while the registry still fails, breaking parity. Whatever we pick likely needs a coordinated swamp-club change.

Three candidate approaches

  1. Local import map + swamp-club follow-up. Thread the already-discovered denoConfigPath (extension_quality.ts:123-126) into scoreExtensionTarball and merge the repo deno.json imports into the controlled config. Smallest change, fixes the command immediately; parity restored only once swamp-club mirrors it (file a follow-up there).
  2. Embed a swamp-generated import map in the tarball. At push/prepare time capture the resolved external specifiers the extension actually imports and write a swamp-controlled map into the tarball; both the local mirror and swamp-club read it. Parity-preserving without needing the repo deno.json; medium change here + swamp-club consumer change.
  3. Manifest-declared npm/jsr deps. First-class npm/jsr dependency declaration in the manifest schema; both sides build the import map from the (trusted, validated) manifest. Most architecturally correct (and the reporter's first suggestion), largest blast radius: schema + validation + docs + swamp-club.

Note: the existing dependencies manifest field can't be used as a workaround — it validates that entries must contain a slash (@collective/name), so it can't express zod -> npm:zod@4.

The fork is mostly a parity-vs-blast-radius tradeoff, plus the swamp-club coordination call. Over to you, @stack72.

Sign in to post a ripple.