Update Adamantite and satisfy anti-slop checks - #45
Conversation
There was a problem hiding this comment.
ℹ️ No behavior changes found — the refactor is faithful. A few rough edges around the type-safety of the new idioms.
Reviewed changes — the adamantite 0.34.4 → 0.36.0 bump and the mechanical refactor that satisfies its new antislop ruleset, across all 17 files.
- Enable
antislop+ignorePatterns—oxlint.config.tsextends the newadamantite/lint/antisloppreset and adopts the package's shared ignore list. - Conditional object spreads → conditional assignment — the dominant change:
...(x === undefined ? {} : { key: x })becomes a base object plus anif-guarded write, incore/packages.ts,references/add.ts,references/remove.ts,sources/repository/normalize.ts,sources/repository/fetch.ts, and three test helpers. typeofchecks →effect/Match—registries/npm/resolver.tsnormalizes the npmrepositoryfield andterminal/prompter.tsresolves its success message viaMatchinstead oftypeof x === "function".- Widened dictionaries →
satisfies—REPOSITORY_PROVIDER_HOSTSandpackageManagerLockfileStrategiesdrop theirRecordannotations, and the latter gains an explicitgetPackageManagerLockfileStrategylookup. - Untyped rejection handlers →
try/catchandEffect.flip—add.test.tsandtags.test.ts; the tarball test'sstatusparam narrows to() => number. - CI runtime discovery —
adamantite.ymlreads Node from.node-versionand drops itsbun-versionpin.
I re-ran validation on the PR tree: bun run typecheck (tsc) is clean and bun test is 316 pass / 0 fail. Note the PR body lists bun run check, which is oxlint-only and never type-checks — typecheck is the one that matters here, and it passes.
I also audited the spread rewrite site by site for the failure mode it invites, namely a key flipping from absent to present-with-undefined. It does not happen anywhere; every guard is the identical !== undefined test. The two most suspicious sites are both provably safe: resolveDirectRepositoryRef's { ...source } can never arrive already carrying a requestedRef (the repository field has no such property), and fetch.ts's includeDirectory !== false && directory !== undefined guard is byte-identical to the one it replaced. Insertion order does shift in add.ts and normalize.ts, but every persistence path goes through Schema.encodeEffect and errors are field-accessed, so it is unobservable.
Worth calling out as a real improvement: swapping await listing.catch((e: unknown) => e) for Effect.flip in tags.test.ts makes those assertions strictly stronger. The old form would have quietly fed a success value into expect(error).toBeInstanceOf(NetworkError) if the effect ever stopped failing; Effect.flip fails the test instead.
ℹ️ Builder types are hand-duplicated instead of derived, so they can drift from the types they mirror
Seven new local interfaces mirror an existing canonical type by hand: RegistryPackageSpecBuilder, RepositoryBuilder, RepositoryPackageSpecBuilder (core/packages.ts), ProjectRepositorySource, RepositoryDirectoryConflict (references/add.ts), PackageReferenceIdentity (references/remove.ts), and TestRepositorySource (install.test.ts). Interestingly this PR already contains the better pattern — add.test.ts derives VersionMetadata from NpmPackageMetadata with a -readonly mapped type, so it cannot drift. The hand-written ones can.
The practical consequence is modest and I want to be precise about it: adding a required field to a canonical type still errors, just at the call site rather than at the definition. Removing or renaming one goes unenforced. Since a mutable-mapped-type alias is roughly the same number of characters, it seems worth converging on the one you already wrote.
Technical details
# Derive builder types from their canonical types
## Affected sites
- `src/lib/core/packages.ts:72-90` — `RegistryPackageSpecBuilder` / `RepositoryBuilder` /
`RepositoryPackageSpecBuilder` duplicate `RegistryPackageSpec` / `RepositoryPackageSpec["repository"]` /
`RepositoryPackageSpec` (declared just above at lines 44-68).
- `src/lib/references/add.ts:69-83` — `RepositoryDirectoryConflict` duplicates the
`RepositoryDirectoryConflictError` payload; `ProjectRepositorySource` duplicates `RepositorySource`
(Schema-derived, `src/lib/core/source.ts:16`).
- `src/lib/references/remove.ts:29-33` — `PackageReferenceIdentity` duplicates the
`PackageNotReferencedError` payload.
- `src/lib/references/__tests__/install.test.ts:24-30` — `TestRepositorySource` duplicates `RepositorySource`.
## Required outcome
- Each builder interface stays automatically in sync with the type it mirrors, so a field added to,
removed from, or retyped on the canonical type is reflected without a manual edit.
- `parsePackageSpec` remains checked against the exported `ParsedPackageSpec` at its own definition
rather than only at its four call sites.
## Suggested approach (optional)
Reuse the mapped-type idiom this PR already introduces at `src/lib/references/__tests__/add.test.ts:44-48`:
```ts
type Mutable<T> = { -readonly [K in keyof T]: T[K] }
type RepositoryPackageSpecBuilder = Mutable<RepositoryPackageSpec>
type RepositoryBuilder = Mutable<RepositoryPackageSpec["repository"]>
type ProjectRepositorySource = Mutable<RepositorySource>
```
For `parsePackageSpec`, a `satisfies` on each return statement restores the link to the canonical type
without reintroducing a conditional spread (`satisfies` is not what the new lint rules object to — this PR
adds two of them elsewhere):
```ts
return packageSpec satisfies RegistryPackageSpec
```
## Open questions for the human
- Are the `*Builder` types meant to be a lasting seam, or purely a workaround for the anti-slop rules?
If the latter, a single shared `Mutable<T>` helper is probably the whole fix.ℹ️ Nitpicks
src/lib/references/add.ts:7— removing theRepositorySourcetype import left a stray blank line splitting theeffect/*imports from the#lib/*ones. Harmless, but the formatter won't collapse it for you.src/lib/manifests/javascript.ts:304—Match.orElse(() => void 0)reads a little obliquely;() => undefinedsays the same thing.
Claude Opus | 𝕏
| const source: NormalizedRepositorySource = { | ||
| fetchSource: provider === undefined ? undefined : `${provider}:${fetchRepositoryPath}`, | ||
| ...(candidate.requestedRef === undefined ? {} : { requestedRef: candidate.requestedRef }), | ||
| host, | ||
| type: "repository", | ||
| url: `https://${host}/${repositoryPath}`, | ||
| } satisfies NormalizedRepositorySource) | ||
| } | ||
|
|
||
| if (candidate.directory !== undefined) { | ||
| Object.assign(source, { directory: candidate.directory }) |
There was a problem hiding this comment.
Object.assign here is doing more damage than the readonly workaround it was reached for. Because its signature is assign<T, U>(target: T, source: U): T & U with no constraint tying U back to T, the assigned property is not checked against the target type at all — I verified on this branch that a typo'd key, a wrong-typed value, and a wholly bogus property each compile with zero diagnostics at these sites.
That is a genuine regression: the old satisfies NormalizedRepositorySource form did catch a wrong value type. Declaring the local as a -readonly mapped type and assigning directly restores the check (and catches typos too, which the old form missed).

Adamantite 0.36.0 enables stricter anti-slop checks that the existing code did not satisfy. This prevented
bun run checkfrom completing successfully after the tooling update.This change updates the managed lint configuration and CI runtime discovery, then replaces conditional object spreads, runtime representation checks, widened dictionaries, and untyped rejection handlers with explicit typed constructions. Package source reference behavior remains unchanged.
Validation
bun run formatbun run checkbun run test— 316 passed, 0 failedChanges made with GPT-5.6 Sol through the Zed coding agent.