Conversation
Implements swamp-club#163 — a new `swamp extension quality <manifest>` CLI that scores an extension against the Swamp Club rubric locally, mirroring the server's scorecard pipeline byte-for-byte so local results match the score the registry will compute after publish. The scorer mirrors swamp-club/lib/domain/scorecard/analysis-factors.ts and score.ts (rubric v2, 10 factors, 12 points): extracts the built tarball to a temp dir, strips attacker-controlled config files, writes a controlled deno.json, runs `deno doc --json` + `deno doc --lint` with CWD at the extraction root, and composes the factors via the same logic the server uses. 24 parity tests (marked [parity]) port the upstream test suite so any logic drift is caught. Adds an opportunistic content-hash-keyed package cache at .swamp/cache/packages/<hash>/. Keyed on source inputs (manifest + file contents + deno config) so any source change invalidates the entry by construction. `extension quality` writes the packaged tarball to the cache; `extension push` checks the cache before packaging and reuses the bytes on a hit — so authors who run quality before push don't pay for two packaging passes, and the bytes scored equal the bytes uploaded. Caveat: `repository-verified` is a structural check on our side (HTTPS + allowlisted host). The server additionally performs an HTTP HEAD to confirm the repo is public; the log renderer surfaces that caveat so users know why a local "earned" could come back "missing" from the registry. Follow-up issue will file the structural-vs-live divergence as a known gap. The swamp-extension-publish and swamp-extension-quality skills gain brief pointers at the new CLI. `quality` is uniformly opt-in — push's behaviour is unchanged, no new flags, no mandatory gate.
There was a problem hiding this comment.
Adversarial Review
Critical / High
No critical or high severity findings.
Medium
-
Unhandled
cache.putfailure inquality.ts—src/libswamp/extensions/quality.ts:132In the quality generator,
await deps.cache.put(hash, archiveBytes, {...})is not wrapped in a try-catch. If the disk is full, permissions are denied, or any filesystem error occurs, this will throw an unhandled error that crashes the quality command after packaging succeeded but before scoring runs. By contrast, the push command (extension_push.ts:423-432) correctly wrapscache.putin a try-catch with a debug log.Breaking example: Run
swamp extension qualityon a filesystem where.swamp/cache/packages/is on a read-only mount or quota-exceeded volume. The command crashes instead of gracefully continuing to score.Suggested fix: Wrap the
cache.putcall the same way push does:try { await deps.cache.put(hash, archiveBytes, { ... }); } catch (cacheError) { ctx.logger.debug`Failed to write package cache (continuing): ${cacheError}`; }
-
packageJsonDirnot passed in quality command —src/cli/commands/extension_quality.ts:124-149The quality command's
prepareInputnever setspackageJsonDir. The push command (extension_push.ts:233-265) has an entire block to detectpackage.jsonand resolvenode_modulesfor extensions that use bare npm specifiers. The quality command skips all of this. For extensions that depend onpackage.json-based npm resolution,swamp extension qualitywill fail during bundling with a confusing module-not-found error, whileswamp extension pushsucceeds.Breaking example: An extension with
import Foo from "some-npm-package"in its entrypoint, apackage.jsonwith that dependency, and nodeno.json. Push succeeds (findspackage.json, setspackageJsonDir); quality fails.Suggested fix: Port the
findPackageJson+ bare-specifier detection block fromextension_push.tsintoextension_quality.ts, or extract it into a shared helper. -
packageJsonPathalwaysundefinedin cache hash —src/cli/commands/extension_push.ts:290,src/cli/commands/extension_quality.ts:162Both commands hardcode
packageJsonPath: undefinedin the hash input. ThePackageCacheHashInputinterface has the field andcomputePackageCacheHashwould include its contents if set. If a user changes apackage.jsondependency version (which affects bundling output), the cache hash stays the same and stale bytes are served. This is consistent between quality and push (both get the same wrong hash), but the cache contract — "any source change invalidates by construction" — is violated forpackage.jsonchanges.Suggested fix: When
packageJsonDiris set, resolve thepackage.jsonpath and pass it in the hash input.
Low
-
toFileUrlmisparses paths with#,?, or unescaped%—src/domain/extensions/extension_rubric_scorer.ts:687-688new URL("file://" + abs)treats#as a fragment delimiter and?as a query delimiter. A path like/tmp/my#project/model.tswould produce a URL with pathname/tmp/myand hash#project/model.ts, silently looking up the wrongdeno docnode. In practice this requires#in the temp dir path (created withprefix: "swamp_quality_") or in the manifest entrypoint name, which is unlikely.Suggested fix: Use Deno's
toFileUrl()from@std/path/to-file-urlwhich handles special characters correctly. -
platforms-oneis a tautology —src/domain/extensions/extension_rubric_scorer.ts:347manifest.platforms.length === 0 || manifest.platforms.length >= 1is always true for any non-negative number. This factor can never be "missing". The parity tests confirm this matches the server's behavior (the server also always earns this factor), so it's not a bug per se — but the condition is misleading. A reader might think it's possible to fail this factor. No fix needed if server parity is the priority.
Verdict
PASS — No critical or high severity issues. The medium findings are real functional gaps (cache.put crash, missing packageJson support in quality, stale cache on package.json changes) but are edge-case paths that won't affect the majority of users. The code is well-structured with thorough testing (especially the parity tests), proper path-traversal protection in collectEntrypoints, and appropriate temp-dir cleanup in finally blocks. The refactoring of push.ts to extract bundleAndArchive is clean and preserves behavior.
There was a problem hiding this comment.
Code Review
Clean, well-layered PR with excellent test coverage (80 tests across unit and integration). DDD separation is solid: pure domain logic in extension_rubric_scorer.ts, repository-pattern cache in extension_package_cache.ts, orchestration in the libswamp generator, and decoupled presentation renderers.
Blocking Issues
None.
Suggestions
-
findDenoConfigduplication —extension_quality.tsandextension_push.tseach have their ownfindDenoConfighelper (the quality command's comment acknowledges this). Consider extracting to a shared utility insrc/cli/if a third caller ever appears. -
Custom
toFileUrl/encodeHex—extension_rubric_scorer.tshas a hand-rolledtoFileUrl(new URL("file://" + abs).href) andextension_package_cache.tshas a hand-rolledencodeHex. The standard library providestoFileUrlfrom@std/pathandencodeHexfrom@std/encoding/hex, which handle edge cases (paths with spaces/special chars, etc.) more robustly. Low risk since paths are temp-dir generated, but swapping in the std-lib versions would be more idiomatic. -
packageJsonPathalwaysundefined— Both the quality command and push command passpackageJsonPath: undefinedin their cache hash inputs. If an extension project uses apackage.jsonthat affects the build, changes to it won't invalidate the cache. The field is in the interface for future use, but a comment noting the gap would help. -
platforms-oneis tautologically true —manifest.platforms.length === 0 || manifest.platforms.length >= 1is always true (any array satisfies this). This mirrors the server's behavior (the factor is effectively a free point), but a one-line comment confirming the intentional parity would help future readers not "fix" it.
There was a problem hiding this comment.
CLI UX Review
Blocking
None.
Suggestions
-
JSON mode emits two documents on failure (
src/presentation/renderers/extension_quality.ts,JsonExtensionQualityRenderer.completed). When factors are missing the output is:- stdout: score JSON object, then the framework's error JSON object (two separate top-level documents)
- The integration test acknowledges this with a custom
parseFirstJsonhelper whose docstring explicitly says "exits non-zero in JSON mode, it prints the score object followed by an error object"
A script doing
result=$(swamp extension quality manifest.yaml --json); echo "$result" | jq .factorswill get a jq parse error when any factor fails. The fix would be to buffer the score in the renderer and notconsole.logit in thecompletedhandler, then let the action handler emit one final document (either the score on pass, or an error on failure). For now, the primary CI use case—check the exit code and ignore stdout—is unaffected. -
repository-verifiedcaveat is always printed in log mode (extension_quality.ts,LogExtensionQualityRenderer.completed). The note appears unconditionally after every scoring run, even on a 12/12 perfect score. Consider printing it only whenrepository-verifiedwas earned (local structural check passed, server may still reject) rather than emitting it on every run including full-pass runs where it reads as a warning for something that didn't fail. -
Minor inconsistency in log renderer (
extension_quality.ts):packagingandscoringhandlers use the call formlogger.info("…")whilecache_hituses the template literal formlogger.info\…``. One form or the other throughout is cleaner. -
Command description "10 client-earnable factors" vs. 12 max points (
extension_quality.ts,.description(...)): the description is accurate for factor count, but users who see12/12in output may wonder why it doesn't say 12. A parenthetical like "10 factors, 12 total points" in the description would head off confusion.
Verdict
PASS — both output modes are implemented, exit codes are correct, flag names are consistent with extension push and extension fmt, error messages are actionable, and the help text and examples are clear. Suggestions above are all minor and none block merge.
Summary
Implements swamp-club#163 — a new
swamp extension quality <manifest>CLI that scores an extension against the Swamp Club rubric locally, mirroring the server's scorecard pipeline so local results match the score the registry will compute after publish.swamp extension quality manifest.yaml [--json]. Exits non-zero if any factor misses; suitable for CI self-checks.extension_rubric_scorer.tsmirrorsanalysis-factors.tsand the rubric-v2 composition fromscore.ts— same slow-type code set, same JSDoc coverage shape, same README/LICENSE lookup in the extracted tarball, same 12-point weighting.[parity]port the upstream swamp-club test suite so logic drift is caught before release..swamp/cache/packages/<hash>/: keyed on source inputs (manifest + resolved file contents + deno config).extension qualitywrites the packaged tarball;extension pushchecks the cache before packaging and reuses the bytes on a hit. Any source change invalidates the entry by construction — authors who run quality before push don't pay twice, and the bytes scored equal the bytes uploaded.Opt-in by design
qualityis uniformly opt-in: push's behaviour is unchanged, no new flags, no mandatory gate. Theswamp-extension-publishandswamp-extension-qualityskills gain brief pointers at the new CLI between states 6 and 7 of the publish flow.Known caveat
repository-verifiedis a structural check on the CLI side (HTTPS + allowlisted host). The server additionally performs an HTTP HEAD to confirm the repo is public — so a local "earned" can come back "missing" from the registry if the repo URL is malformed or private. The log renderer surfaces this caveat. Follow-up issue: narrow the gap (HTTP HEAD from the CLI, or a shared scoring package between this repo and swamp-club).Smoke test
Ran against
systeminit/swamp-extensions/datastore/s3— reports 12/12 points, 100%, all factors earned, matching the registry.Test Plan
extension_package_cache.ts(hit/miss, determinism, cache invalidation)extension_rubric_scorer.tscovering every factor and edge case[parity]tests ported from swamp-club'sanalysis-factors_test.tsandscorecard_test.tsintegration/extension_quality_test.tscovering the full CLI pipeline (package → cache → extract → deno doc → compose), including cache hit/miss, source mutation invalidating cache, log and JSON modes@si/s3-datastoreextension — 12/12 (100%), matches registrydeno checkcleandeno lintcleandeno fmt --checkcleandeno run test(4,802 passed, 1 pre-existing flaky unrelated to this change, 1 ignored)deno run compilesucceedsFollow-ups (not in this PR)
systeminit/swamp-uatfor CLI UAT coverage (tests/cli/extension/quality/)systeminit/swamp-clubto document the new command incontent/manual/how-tosysteminit/swampandsysteminit/swamp-clubto eliminate the current ~100 lines of mirrored logic🤖 Generated with Claude Code