Skip to content

Prepare registry and manifest seams for multi-ecosystem support - #37

Merged
adelrodriguez merged 1 commit into
mainfrom
08-11-prepare_registry_and_manifest_seams_for_multi-ecosystem_projects
Aug 11, 2026
Merged

Prepare registry and manifest seams for multi-ecosystem support#37
adelrodriguez merged 1 commit into
mainfrom
08-11-prepare_registry_and_manifest_seams_for_multi-ecosystem_projects

Conversation

@adelrodriguez

@adelrodriguez adelrodriguez commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator

This PR restructures the module boundaries to prepare the registry and manifest systems for projects that use multiple package ecosystems.

Registry and manifest seams

ProjectDependencyReader replaces the previous getManifestAdapter and readProjectDependencies free functions with a service that iterates all registered manifest adapters and merges their results. Adapters that do not detect a matching manifest are skipped; the service returns Option.none only when no adapter detects any manifest at all, distinguishing that case from a detected but empty manifest. layerWithAdapters allows tests and future callers to supply an explicit adapter list without touching the default registration. The group field on ManifestDependency is widened from a fixed union to string so adapters outside the JavaScript ecosystem can declare their own dependency group names.

RemoteTagReader service

The CommandRunner service is removed. Its only consumer was git ls-remote in the repository tag resolution path. That logic is now encapsulated in RemoteTagReader, which exposes a list method and ships its own layer (backed by ChildProcessSpawner) and layerWithCommand for test injection. Tests that previously constructed a CommandRunner layer now use RemoteTagReader.layerWithCommand.

PackrefHome converted to Context.Reference

PackrefHome moves from src/lib/services/ to src/lib/workspace/home.ts and is implemented as a Context.Reference with a default value of homedir(). This removes it from the required service set for store and workspace operations, eliminating the need to thread it through test effect type signatures.

Module relocations

  • Reflinker moves from src/lib/services/ to src/lib/workspace/.
  • Prompter and the title utilities move from src/lib/services/ and src/lib/shared/ to src/terminal/.
  • store.ts is renamed to src/lib/store/index.ts.
  • The integration test for addPackageReference is consolidated into add.test.ts under a with production adapters describe block.

Tarball extraction

Tarball extraction tests now cover crate-style (name-version/) and sdist-style (Name-Version/) archive layouts, in addition to the existing package/ convention, and verify that archives with more than one top-level directory are rejected.

Summary by CodeRabbit

  • New Features
    • Added support for reading dependencies from multiple project manifests and package ecosystems.
    • Added clearer initialization feedback when non-interactive mode is required.
    • Added more actionable messages for unsupported registries.
  • Improvements
    • Added bounded retries for transient registry, repository, and archive download failures.
    • Improved performance by processing independent package, store, and archive operations concurrently.
    • Improved repository source handling with tarball fallback.
  • Bug Fixes
    • Improved file, lockfile, archive, and reference error reporting.
    • Preserved correct handling of empty, missing, and malformed project manifests.

@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 5680a729-cf70-474d-b4b2-c3eb75412028

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Walkthrough

The change reorganizes Effect services into terminal, workspace, manifest, and source modules. It adds typed file errors, bounded retries, concurrent I/O, Option-based results, repository tag services, and multi-manifest dependency reading.

Changes

Packref service and adapter foundation

Layer / File(s) Summary
Service boundaries and application wiring
docs/architecture.md, docs/plans/*, oxlint.config.ts, src/commands/*, src/index.ts, src/lib/core/*, src/terminal/*, .changeset/*, .packref/packref-lock.json
The module layout now separates terminal and workspace responsibilities. Application layers use ProjectDependencyReader, RemoteTagReader, Reflinker, and Prompter. Commands use the renamed candidate API and terminal paths. Initialization uses a tagged error and Match formatting.
Manifest reader and JavaScript resolution
src/lib/manifests/*
ProjectDependencyReader aggregates detected manifests. JavaScript lockfile parsers return Option values, use shared decoders and ancestor lookup, and resolve dependencies concurrently.
Repository and registry adapters
src/lib/sources/repository/*, src/lib/registries/npm/*, src/lib/registries/__tests__/*
Repository tag lookup now uses RemoteTagReader with bounded retries. Repository normalization uses shared mappings and Option-based parsing. npm metadata retries transient failures, and version resolution returns Option.
Reference workflows and source materialization
src/lib/references/*, src/lib/sources/tarball/*, src/lib/store/*
Add supports repository-to-tarball fallback. Install, sync, prune, remove, store traversal, and tarball extraction use bounded concurrency. Filesystem failures map to domain errors.
Workspace services and typed file errors
src/lib/workspace/*
PackrefHome and Reflinker are workspace services. Configuration, integration, and lockfile effects use reusable decoders, Option, Effect.fn, and typed error mapping.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant CLI
  participant ProjectDependencyReader
  participant PackageManagerResolver
  participant RemoteTagReader
  participant Store
  CLI->>ProjectDependencyReader: read detected manifests
  ProjectDependencyReader->>PackageManagerResolver: resolve dependency versions
  PackageManagerResolver-->>ProjectDependencyReader: Option-based dependency results
  CLI->>RemoteTagReader: resolve repository tags
  RemoteTagReader-->>CLI: matching tag or network error
  CLI->>Store: materialize or update package references
  Store-->>CLI: completed operation or typed file error
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the PR's primary goal of preparing registry and manifest module boundaries for multi-ecosystem support.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch 08-11-prepare_registry_and_manifest_seams_for_multi-ecosystem_projects

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

adelrodriguez commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator Author

@adelrodriguez
adelrodriguez force-pushed the 08-11-prepare_registry_and_manifest_seams_for_multi-ecosystem_projects branch from ac0cccd to 99de96d Compare August 11, 2026 04:41
@adelrodriguez
adelrodriguez marked this pull request as ready for review August 11, 2026 04:45

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

🧹 Nitpick comments (7)
src/lib/references/install.ts (1)

72-77: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Add parentheses around the conditional operand of yield*.

yield* reusedStoreEntry ? readStoreEntry(entry)... : fetchLockedStoreEntry(entry) parses as yield* (reusedStoreEntry ? ... : ...), which is the intended behavior. The precedence is not obvious to a reader and invites an incorrect edit later.

♻️ Proposed readability fix
-  const storeEntry = yield* reusedStoreEntry
-    ? readStoreEntry(entry).pipe(
-        Effect.flatMap((storedEntry) => ensureMatchingSource(entry, storedEntry))
-      )
-    : fetchLockedStoreEntry(entry)
+  const storeEntry = yield* (reusedStoreEntry
+    ? readStoreEntry(entry).pipe(
+        Effect.flatMap((storedEntry) => ensureMatchingSource(entry, storedEntry))
+      )
+    : fetchLockedStoreEntry(entry))
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/references/install.ts` around lines 72 - 77, In the store-entry
assignment around reusedStoreEntry, wrap the conditional expression passed to
yield* in explicit parentheses while preserving the existing
readStoreEntry/ensureMatchingSource and fetchLockedStoreEntry branches.
src/lib/sources/tarball/__tests__/fetch.test.ts (1)

153-162: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use a rejection assertion instead of the try/catch sentinel.

If run resolves, line 155 throws the sentinel Error inside the same try, so the catch block receives it. The test then fails on toBeInstanceOf(TarballFetchError) with a message that points at the sentinel rather than at the missing rejection. The same pattern appears at lines 232-245.

♻️ Proposed change
-    try {
-      await run(fetchTarballSnapshot(identity, tarballUrl), home, () => Effect.succeed(archive))
-      throw new Error("Expected tarball extraction to fail.")
-    } catch (error) {
-      expect(error).toBeInstanceOf(TarballFetchError)
-
-      if (error instanceof TarballFetchError) {
-        expect(error.cause).toBe("Package archive must contain exactly one top-level directory")
-      }
-    }
+    const promise = run(
+      fetchTarballSnapshot(identity, tarballUrl),
+      home,
+      () => Effect.succeed(archive)
+    )
+
+    await expect(promise).rejects.toBeInstanceOf(TarballFetchError)
+    await expect(promise).rejects.toMatchObject({
+      cause: "Package archive must contain exactly one top-level directory",
+    })
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/sources/tarball/__tests__/fetch.test.ts` around lines 153 - 162,
Replace the try/catch sentinel pattern around run in the tarball extraction
failure tests with a rejection assertion that verifies run rejects with
TarballFetchError and the expected cause. Apply the same change to the
corresponding test block around lines 232–245, avoiding any catch that could
intercept a locally thrown sentinel.
src/lib/sources/repository/__tests__/tags.test.ts (1)

103-115: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Prefer expect(...).rejects over the try/catch guard.

Both tests place throw new Error("Expected remote tag listing to fail.") inside the try block. The catch block then receives that guard error and the toBeInstanceOf assertion fails, so the test still reports a failure. The behavior is correct, but the intent is clearer with the rejection matcher, and the assertion count stays deterministic.

♻️ Proposed refactor for the bounded-retry test
-    try {
-      await runWithRemoteTagCommand(listRemoteTags(), () => {
-        commandCount += 1
-        return Effect.succeed({
-          exitCode: 128,
-          stderr: "fatal: repository not found",
-          stdout: "",
-        })
-      })
-      throw new Error("Expected remote tag listing to fail.")
-    } catch (error) {
-      expect(error).toBeInstanceOf(NetworkError)
-    }
+    await expect(
+      runWithRemoteTagCommand(listRemoteTags(), () => {
+        commandCount += 1
+        return Effect.succeed({
+          exitCode: 128,
+          stderr: "fatal: repository not found",
+          stdout: "",
+        })
+      })
+    ).rejects.toBeInstanceOf(NetworkError)

Also applies to: 139-156

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/sources/repository/__tests__/tags.test.ts` around lines 103 - 115,
Refactor the rejection assertions in both affected tests to use Jest’s
expect(...).rejects matcher instead of a try/catch with a manual guard error.
Assert that runWithRemoteTagCommand(listRemoteTags(), ...) rejects with
NetworkError while preserving the existing commandCount and mocked
Effect.succeed behavior.
src/lib/registries/npm/__tests__/resolver.test.ts (1)

4-9: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Add coverage for the latest branch of resolveVersion.

The two new tests cover the exact-version and range branches. The latest branch in src/lib/registries/npm/resolver.ts lines 11-15 is new behavior: it filters out a dist-tags.latest value that has no matching entry in metadata.versions. That filter has no test.

💚 Proposed additional tests
     it("returns None for a missing version", () => {
       expect(Option.isNone(resolveVersion(baseMetadata, "20.0.0"))).toBe(true)
     })
+
+    it("resolves the latest dist-tag", () => {
+      expect(Option.getOrThrow(resolveVersion(baseMetadata, "latest"))).toBe("19.0.0")
+    })
+
+    it("returns None when the latest dist-tag has no published version", () => {
+      const metadata = {
+        ...baseMetadata,
+        "dist-tags": { latest: "99.0.0" },
+      } satisfies NpmPackageMetadata
+
+      expect(Option.isNone(resolveVersion(metadata, "latest"))).toBe(true)
+    })

Also applies to: 68-76

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/registries/npm/__tests__/resolver.test.ts` around lines 4 - 9, Add a
test for the latest branch of resolveVersion, covering metadata where
dist-tags.latest does not match any key in metadata.versions and verifying the
unmatched latest value is filtered out. Keep the existing exact-version and
range tests unchanged.
src/lib/sources/repository/tags.ts (1)

113-135: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Add a timeout to the git ls-remote subprocess.

The default layer spawns git ls-remote --tags <url> with no time bound. If the remote host accepts the connection and then stalls, the effect waits indefinitely, and the retry policy at lines 92-95 never engages. The npm client in src/lib/registries/npm/client.ts bounds its remote calls through the HTTP client, so this path is the only unbounded remote call in the cohort.

♻️ Proposed change
       return makeRemoteTagReader((source) =>
         Effect.scoped(
           Effect.gen(function* () {
             // ...
           })
-        )
+        ).pipe(Effect.timeoutFail({
+          duration: REMOTE_TAG_TIMEOUT,
+          onTimeout: () => new NetworkError({ cause: "git ls-remote timed out", url: source.url }),
+        }))
       )

Note that Effect.timeoutFail must produce an error the list signature already declares, so NetworkError is the correct choice. It also becomes retryable through the existing while predicate.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/sources/repository/tags.ts` around lines 113 - 135, Apply a bounded
timeout to the remote `git ls-remote` effect in the `makeRemoteTagReader`
implementation, including subprocess spawning and output collection. Use
`Effect.timeoutFail` with the existing timeout configuration and produce a
`NetworkError` so the `list` signature remains valid and its existing retry
predicate handles timeout failures.
src/lib/manifests/index.ts (1)

10-27: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚖️ Poor tradeoff

Consider generic parameters on layerWithAdapters instead of the derived default types.

DefaultManifestError and DefaultManifestRequirements are derived from manifestAdapters, which currently holds one adapter. The parameter type therefore pins every injected adapter to the JavaScript adapter's error and requirement types. When a second ecosystem adapter is registered, this constraint blocks adapters that declare narrower requirements.

Generic parameters keep the seam open for the multi-ecosystem goal stated in the PR objectives.

♻️ Proposed refactor
-  static readonly layerWithAdapters = (
-    adapters: ReadonlyArray<ManifestAdapter<DefaultManifestError, DefaultManifestRequirements>>
-  ) =>
+  static readonly layerWithAdapters = <
+    E extends DefaultManifestError,
+    R extends DefaultManifestRequirements,
+  >(
+    adapters: ReadonlyArray<ManifestAdapter<E, R>>
+  ) =>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/manifests/index.ts` around lines 10 - 27, Update
ProjectDependencyReader.layerWithAdapters to introduce generic error and
requirements parameters derived from the supplied adapter collection instead of
using DefaultManifestError and DefaultManifestRequirements. Ensure each injected
ManifestAdapter is accepted according to its own generic types, while preserving
the existing service and layer behavior.
src/lib/sources/repository/normalize.ts (1)

101-127: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Simplify the Option handling in the two regular-expression capture sites.

Both sites build an Option of the named-group object, then immediately unwrap it twice with Option.getOrUndefined and test for undefined. The Option adds no safety here, and each unwrap re-reads the same value. Option.match or a direct nullish check on exec reads more clearly and keeps a single code path.

♻️ Proposed refactor for `normalizeFromScpLikeUrl`
 const normalizeFromScpLikeUrl = (candidate: RepositorySourceCandidate, rawUrl: string) => {
-  const groups = Option.fromNullishOr(
-    /^(?:[^@]+@)?(?<host>[^:]+):(?<repositoryPath>.+)$/u.exec(rawUrl)
-  ).pipe(Option.flatMap((match) => Option.fromNullishOr(match.groups)))
-  const host = Option.getOrUndefined(groups)?.host
-  const repositoryPath = Option.getOrUndefined(groups)?.repositoryPath
-
-  if (host === undefined || repositoryPath === undefined) {
-    return Effect.fail(invalidRepositoryUrl(candidate, "unsupported repository URL format"))
-  }
-
-  return makeNormalizedSource(candidate, host.toLowerCase(), repositoryPath)
+  const groups = /^(?:[^@]+@)?(?<host>[^:]+):(?<repositoryPath>.+)$/u.exec(rawUrl)?.groups
+
+  if (groups?.host === undefined || groups.repositoryPath === undefined) {
+    return Effect.fail(invalidRepositoryUrl(candidate, "unsupported repository URL format"))
+  }
+
+  return makeNormalizedSource(candidate, groups.host.toLowerCase(), groups.repositoryPath)
 }

Apply the same shape to the shorthand capture at lines 119-123.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/sources/repository/normalize.ts` around lines 101 - 127, In
normalizeFromScpLikeUrl and normalizeRepositorySource, simplify the regex
capture handling by avoiding the intermediate Option pipelines and repeated
Option.getOrUndefined calls. Use a single direct nullish check or Option.match
on each exec result, while preserving the existing invalid-format failure and
shorthand-provider branching behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/lib/references/__tests__/add.test.ts`:
- Around line 575-614: Gate the network-dependent suite named
“addPackageReference with production adapters” behind the project’s explicit
integration-test flag, so both live-service tests are skipped during the default
bun test run and execute only when that flag is enabled. Use the existing test
gating convention or flag symbol used elsewhere in the repository, without
changing the assertions or test behavior.

In `@src/lib/references/add.ts`:
- Around line 81-85: Replace the catch-all domain-error mappings for raw
filesystem PlatformError values with filesystem/workspace-specific errors. In
src/lib/references/add.ts lines 81-85, use that error for realPath and
ensureDirectory; in src/lib/references/prune.ts lines 52-65, use it for
project-directory exists/stat while retaining toLockfileError only for
readProjectLockfile at line 71; in src/lib/store/index.ts line 65, reserve
StoreCorruptedError for metadata read/decode failures; in
src/lib/references/remove.ts lines 66-71 and src/lib/references/sync.ts lines
90-100, add or reuse a removal/operation-specific ReflinkError instead of using
the target as the source.

In `@src/lib/references/install.ts`:
- Around line 95-110: Update the failure handling after the Effect.forEach call
in the install flow to collect all attempts with type "failure" instead of
retaining only attempts.find(...). Keep the first failure as the returned error,
and log or attach every subsequent failure so callers can discover all broken
entries in one run.

In `@src/lib/references/remove.ts`:
- Around line 118-126: Update the removal flow around Effect.forEach and
removePackageEntries so filesystem removals are attempted without aborting or
interrupting remaining references. Collect each removal’s success or failure,
pass only successfully removed entries to removePackageEntries, then propagate
the collected removal failures after the lockfile update while preserving the
existing concurrency and error conversion.

In `@src/lib/registries/__tests__/index.test.ts`:
- Around line 21-24: Update the test’s error handling to assert that the caught
error is an UnsupportedRegistryError before checking its message, ensuring
unexpected error types fail the test rather than skipping assertions. Preserve
the existing expected message assertion for the valid error type.

In `@src/lib/registries/npm/client.ts`:
- Around line 27-32: Add an Effect.timeout to each npm HTTP client attempt in
the HttpClient construction before applying HttpClient.retryTransient, using the
project’s intended request-timeout duration. Keep the existing exponential retry
schedule and retry count unchanged, and do not use HttpClient.withTimeout.

In `@src/lib/sources/tarball/fetch.ts`:
- Around line 91-93: Update the chmod handling in the tarball extraction flow to
parse the archive mode, reject non-integer results such as NaN, and mask the
value to permission bits before passing it to fs.chmod. Preserve the existing
behavior of applying chmod only when entry.attrs.mode is defined, while
preventing setuid, setgid, and sticky bits from being applied.

In `@src/lib/store/index.ts`:
- Around line 99-148: Limit aggregate directory-read concurrency in the registry
traversal instead of applying independent limits at each nested Effect.forEach
level. Introduce one shared semaphore sized by STORE_TRAVERSAL_CONCURRENCY, have
every listDirectoryOrEmpty call in this traversal acquire it (or use
listDirectory through the semaphore), and set the nested Effect.forEach
operations to unbounded so the semaphore is the sole concurrency bound.

---

Nitpick comments:
In `@src/lib/manifests/index.ts`:
- Around line 10-27: Update ProjectDependencyReader.layerWithAdapters to
introduce generic error and requirements parameters derived from the supplied
adapter collection instead of using DefaultManifestError and
DefaultManifestRequirements. Ensure each injected ManifestAdapter is accepted
according to its own generic types, while preserving the existing service and
layer behavior.

In `@src/lib/references/install.ts`:
- Around line 72-77: In the store-entry assignment around reusedStoreEntry, wrap
the conditional expression passed to yield* in explicit parentheses while
preserving the existing readStoreEntry/ensureMatchingSource and
fetchLockedStoreEntry branches.

In `@src/lib/registries/npm/__tests__/resolver.test.ts`:
- Around line 4-9: Add a test for the latest branch of resolveVersion, covering
metadata where dist-tags.latest does not match any key in metadata.versions and
verifying the unmatched latest value is filtered out. Keep the existing
exact-version and range tests unchanged.

In `@src/lib/sources/repository/__tests__/tags.test.ts`:
- Around line 103-115: Refactor the rejection assertions in both affected tests
to use Jest’s expect(...).rejects matcher instead of a try/catch with a manual
guard error. Assert that runWithRemoteTagCommand(listRemoteTags(), ...) rejects
with NetworkError while preserving the existing commandCount and mocked
Effect.succeed behavior.

In `@src/lib/sources/repository/normalize.ts`:
- Around line 101-127: In normalizeFromScpLikeUrl and normalizeRepositorySource,
simplify the regex capture handling by avoiding the intermediate Option
pipelines and repeated Option.getOrUndefined calls. Use a single direct nullish
check or Option.match on each exec result, while preserving the existing
invalid-format failure and shorthand-provider branching behavior.

In `@src/lib/sources/repository/tags.ts`:
- Around line 113-135: Apply a bounded timeout to the remote `git ls-remote`
effect in the `makeRemoteTagReader` implementation, including subprocess
spawning and output collection. Use `Effect.timeoutFail` with the existing
timeout configuration and produce a `NetworkError` so the `list` signature
remains valid and its existing retry predicate handles timeout failures.

In `@src/lib/sources/tarball/__tests__/fetch.test.ts`:
- Around line 153-162: Replace the try/catch sentinel pattern around run in the
tarball extraction failure tests with a rejection assertion that verifies run
rejects with TarballFetchError and the expected cause. Apply the same change to
the corresponding test block around lines 232–245, avoiding any catch that could
intercept a locally thrown sentinel.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 0813309d-5470-48d7-986c-ab69b3291249

📥 Commits

Reviewing files that changed from the base of the PR and between 54bd616 and 99de96d.

📒 Files selected for processing (62)
  • .changeset/calm-stores-retry.md
  • .changeset/tough-hounds-juggle.md
  • .packref/packref-lock.json
  • docs/architecture.md
  • docs/plans/10-multi-registry-groundwork.md
  • docs/plans/11-direct-repository-sources.md
  • oxlint.config.ts
  • src/commands/add.ts
  • src/commands/clean.ts
  • src/commands/init.ts
  • src/commands/install.ts
  • src/commands/list.ts
  • src/commands/prune.ts
  • src/commands/remove.ts
  • src/commands/sync.ts
  • src/index.ts
  • src/lib/core/errors.ts
  • src/lib/core/packages.ts
  • src/lib/manifests/__tests__/index.test.ts
  • src/lib/manifests/__tests__/javascript.test.ts
  • src/lib/manifests/index.ts
  • src/lib/manifests/javascript.ts
  • src/lib/manifests/manifest.ts
  • src/lib/references/__tests__/add.integration.test.ts
  • src/lib/references/__tests__/add.test.ts
  • src/lib/references/__tests__/install.test.ts
  • src/lib/references/__tests__/prune.test.ts
  • src/lib/references/__tests__/sync.test.ts
  • src/lib/references/add.ts
  • src/lib/references/install.ts
  • src/lib/references/prune.ts
  • src/lib/references/remove.ts
  • src/lib/references/sync.ts
  • src/lib/registries/__tests__/index.test.ts
  • src/lib/registries/npm/__tests__/client.test.ts
  • src/lib/registries/npm/__tests__/resolver.test.ts
  • src/lib/registries/npm/client.ts
  • src/lib/registries/npm/resolver.ts
  • src/lib/services/__tests__/command-runner.test.ts
  • src/lib/services/command-runner.ts
  • src/lib/services/packref-home.ts
  • src/lib/sources/repository/__tests__/fetch.test.ts
  • src/lib/sources/repository/__tests__/tags.test.ts
  • src/lib/sources/repository/fetch.ts
  • src/lib/sources/repository/normalize.ts
  • src/lib/sources/repository/tags.ts
  • src/lib/sources/tarball/__tests__/fetch.test.ts
  • src/lib/sources/tarball/fetch.ts
  • src/lib/store/__tests__/store.test.ts
  • src/lib/store/index.ts
  • src/lib/store/paths.ts
  • src/lib/workspace/__tests__/project.test.ts
  • src/lib/workspace/config.ts
  • src/lib/workspace/home.ts
  • src/lib/workspace/integration.ts
  • src/lib/workspace/lockfile.ts
  • src/lib/workspace/project.ts
  • src/lib/workspace/reflinker.ts
  • src/terminal/__tests__/prompter.test.ts
  • src/terminal/prompter.ts
  • src/terminal/title.macro.ts
  • src/terminal/title.ts
💤 Files with no reviewable changes (4)
  • src/lib/services/tests/command-runner.test.ts
  • src/lib/services/command-runner.ts
  • src/lib/services/packref-home.ts
  • src/lib/references/tests/add.integration.test.ts

Comment thread src/lib/references/__tests__/add.test.ts Outdated
Comment thread src/lib/references/add.ts Outdated
Comment thread src/lib/references/install.ts
Comment thread src/lib/references/remove.ts Outdated
Comment thread src/lib/registries/__tests__/index.test.ts
Comment thread src/lib/registries/npm/client.ts
Comment thread src/lib/sources/tarball/fetch.ts
Comment thread src/lib/store/index.ts Outdated

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Important

The manifest seam this PR is named for does not actually open. layerWithAdapters is typed against the JavaScript adapter's concrete error union, so a second ecosystem adapter is rejected at compile time. Details inline on src/lib/manifests/index.ts.

Reviewed changes

  • Read the full diff end-to-end (62 files, 1 commit).
  • Verified on disk at 99de96d: bunx tsc --noEmit clean, bun test 268 pass / 0 fail.
  • Confirmed no stale references remain to #lib/services/*, #lib/store/store.ts, or #lib/shared/title* after the relocations, and that the new core/errors.tscore/registry.ts import is not a cycle.
  • Empirically probed the new adapter seam with a scratch second-ecosystem adapter to check the extensibility claim.

🧩 The manifest seam is typed against the one adapter it has

This is the headline concern, so restating it outside the inline thread: the PR's stated goal is preparing the manifest system "for projects that use multiple package ecosystems," but DefaultManifestError and DefaultManifestRequirements in src/lib/manifests/index.ts are inferred from manifestAdapters, which contains only javascript. Every adapter passed to layerWithAdapters must therefore fail with exactly ManifestParseError | ManifestResolutionError.

Worth noting the registry side of the same PR gets this right — src/lib/registries/index.ts uses satisfies Record<Registry, RegistryAdapter<unknown, unknown>>, which keeps the adapter types open. The manifest side dropped that satisfies clause. The two seams the PR title pairs together are not equally open.

🏷️ Filesystem errors are relabelled as domain errors whose remedies don't apply

The reliability pass narrows several error channels by mapping PlatformError into domain errors. That's a reasonable goal, but three of the mappings produce user-facing guidance that is wrong for the failure that actually occurred. Each error class has a get message() in src/lib/core/errors.ts that prescribes a specific remedy, so the mislabel is not cosmetic — it is the text the user reads and acts on.

Flagged inline at references/add.ts, store/index.ts, and references/remove.ts. The store/index.ts one is the most user-hostile: a clean that fails on a permissions error tells the user to run clean.

🧪 A concurrency regression test was deleted without replacement

src/lib/services/__tests__/command-runner.test.ts is removed along with CommandRunner. That test drove 256 KiB through the spawner to prove stdout and stderr are drained concurrently — without that, a child process filling one pipe while the reader blocks on the other deadlocks.

The equivalent logic now lives inside RemoteTagReader.layer (Effect.all([...], { concurrency: 3 }) over handle.stdout, handle.stderr, handle.exitCode). But every test in src/lib/sources/repository/__tests__/tags.test.ts injects layerWithCommand with a fake, bypassing that code entirely. The only reference to the real RemoteTagReader.layer is src/lib/references/__tests__/add.test.ts:155, where it sits in a layer stack that never reaches git.

So the deadlock guard is now untested. The logic moving house is fine; the test not moving with it is the gap. Porting the old large-output test onto RemoteTagReader.layer would restore it.

🏠 PackrefHome as a Context.Reference trades a compile error for a silent write to the real $HOME

Moving PackrefHome off the required service set genuinely does simplify test signatures — that part works. The cost is that defaultValue: () => ({ path: homedir() }) makes omission legal.

Previously, a test (or a new code path) that forgot to provide PackrefHome failed typechecking. Now it compiles and resolves to the developer's actual home directory, so a store test missing its PackrefHome.at(...) would create and later fs.remove(..., { recursive: true }) real paths under ~/.packref. All twelve current call sites do provide it, so nothing is broken today; the concern is that the guardrail against the thirteenth is gone, and the failure mode is destructive rather than loud.

If the ergonomics win is worth keeping, a default that points at an obviously-invalid sentinel path outside test/production wiring would preserve most of it while making an unprovided PackrefHome fail fast instead of silently.

ℹ️ Nitpicks

ReflinkError with source === target. In src/lib/references/remove.ts, toReflinkError sets both source and target to referencePath, so an fs.exists failure renders as "Failed to materialize project reference from X to X" — during a removal, which materializes nothing. Same category as the mappings above, just lower impact since the path shown is at least the right one.

The tarball section of the PR description doesn't match the diff. The body says top-level stripping "now handles crate-style (name-version/) and sdist-style (Name-Version/) archive layouts … and rejects archives that contain more than one top-level directory." getArchivePrefix in src/lib/sources/tarball/fetch.ts is unchanged context in this diff — it already derived the prefix from the single observed root and already rejected roots.size !== 1. What the PR adds is test coverage for those layouts, which is a genuine improvement, just not the behavior change described. Worth correcting before this text becomes the changelog entry.

404 coverage was dropped from the tarball tests. In src/lib/sources/tarball/__tests__/fetch.test.ts the existing rejection test was retargeted from 404 to 503 so it could assert requestCount === 3 against the new retryTransient. That's a good test to have, but it replaced rather than joined the 404 case — there's now no assertion that a permanent 404 fails without burning retries, which is the more common failure and the one where retrying is pure latency. Adding a second case pinning requestCount === 1 for 404 would cover both halves of the retry policy.

Schedule.upTo({ times: 2 }) checks out. Verified against node_modules/effect/src/Schedule.ts — valid v4 API, and 2 retries means 3 total attempts, which is what the tarball tests assert. No action needed; noting it so it isn't re-litigated.

Concurrent tar entry writes are safe. TAR_ENTRY_WRITE_CONCURRENCY = 8 looked like an ordering hazard (a file entry landing before its parent directory entry), but each file write does its own fs.makeDirectory(dirname, { recursive: true }) first, so there's no dependency on entry order. Noting it as checked-and-fine.

Pullfrog  | Fix all ➔Fix 👍s ➔View workflow run | Using Claude Opus𝕏

Comment thread src/lib/manifests/index.ts Outdated
Comment thread src/lib/references/add.ts Outdated
Comment thread src/lib/store/index.ts Outdated
Comment thread src/lib/sources/repository/tags.ts Outdated
@adelrodriguez
adelrodriguez force-pushed the 08-11-prepare_registry_and_manifest_seams_for_multi-ecosystem_projects branch 2 times, most recently from fcdeedd to 3dc2c7a Compare August 11, 2026 15:25

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Important

All four of my prior inline threads are genuinely fixed. One fix over-corrected: git ls-remote now never retries any failure that surfaces as a non-zero exit code, which includes the transient ones the changeset promises to retry. The test that was supposed to guard the fix cannot fail.

Reviewed changes

  • manifests/index.ts — the adapter seam is back: layerWithAdapters<E, R> is generic over foreign adapters, makeLayer is private, and index.test.ts exercises a real foreign adapter (CargoManifestError / CargoManifestEnvironment) rather than asserting on the default one. Resolved.
  • references/add.tsrealPath and ensureDirectory map to the new ProjectFilesystemError with an operation: "access" | "prepare" | "resolve" discriminant. Resolved.
  • sources/repository/tags.ts — the exit-code check moved outside Effect.retry, so fatal: repository not found is no longer retried three times. Resolved, but see the inline note: the pendulum swung too far.
  • store/index.tscleanStore raises GlobalStoreFilesystemError (operation: "clean") instead of mislabelling I/O failures as corruption; StoreCorruptedError is now reserved for metadata decode. Resolved.
  • Restored coverage I'd flagged as dropped: the git ls-remote stdout/stderr deadlock guard is back as tags.test.ts "drains large stdout and stderr from the live command concurrently" (real fake git on PATH, 12k lines to both streams), and the 404 tarball case is back in tarball/__tests__/fetch.test.ts.
  • Integration tests moved to src/__tests__/integration.test.ts behind PACKREF_INTEGRATION with a dedicated test:integration script, so the default bun test no longer hits the network.
  • Aggregate error handling in install.ts and remove.ts now collects every failure via Effect.match + Array.partition/Result instead of aborting on the first one; prune.ts and sync.ts fan out at concurrency 8; store/index.ts bounds its traversal with a Semaphore at 16; npm/client.ts gained a 30s per-attempt timeout plus one transient retry; tarball/fetch.ts validates archive modes against /^[0-7]+$/u and masks with & 0o777.

Verification

bunx tsc --noEmit is clean. bun test is 274 pass / 1 fail. The one failure — store > checks, lists, and removes scoped and unscoped entries — reproduces identically on origin/main in a clean worktree, so it is not a regression from this PR. Details inline on store/index.ts.

Not re-raised

PackrefHome's Context.Reference default still silently falls back to the real $HOME when no layer is provided. I raised this last round, it's unchanged, and it's a design call rather than a defect — noting it only so it isn't mistaken for something I stopped caring about.

Pullfrog  | Fix all ➔Fix 👍s ➔View workflow run | Using Claude Opus𝕏

Comment thread src/lib/sources/repository/tags.ts Outdated
Comment thread src/lib/sources/repository/__tests__/tags.test.ts Outdated
Comment thread src/lib/store/index.ts Outdated
@adelrodriguez
adelrodriguez force-pushed the 08-11-prepare_registry_and_manifest_seams_for_multi-ecosystem_projects branch from 3dc2c7a to dfa04b7 Compare August 11, 2026 15:49

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ All three prior findings are genuinely fixed and the suite is fully green. One suggestion inline, one nit below.

Reviewed changes

Delta since my last review (3dc2c7adfa04b7), three files.

  • Restored bounded retries for transient git ls-remote failures. The exitCode !== 0 check moved back inside the Effect.retry pipeline, and the while predicate now classifies on stderr via TRANSIENT_GIT_FAILURE_PATTERN — permanent failures (fatal: repository not found) still fail on the first attempt, DNS/connection failures get the bounded schedule. Non-string causes (spawner PlatformError, Effect.timeout) remain retryable, which is right.
  • Made the retry tests able to fail. "does not retry a permanent command failure" and "reports an actionable error without retry when the git executable is missing" now await the rejection before asserting commandCount, and the counter increments through Effect.sync inside the command rather than in the factory closure. A new "retries a transient command failure with a bounded policy" pins commandCount === 3 on Could not resolve host.
  • Sorted listStoreEntries output. registryEntries.flat().toSorted(Order.mapInput(packageIdentityOrder, …)) makes the ordering a contract instead of an accident of fs.readDirectory, retiring the previously-red store test.

Verification

bunx tsc --noEmit clean. bun test276 pass / 0 fail (was 274 / 1). I re-ran the mutation that defeated the old test: relaxing the while predicate to Predicate.isTagged(error, "NetworkError") alone now produces 13 pass / 1 fail, so the guard is real. tags.ts was restored afterwards; the tree is clean.

ℹ️ Nitpicks

  • src/lib/sources/repository/__tests__/tags.test.ts:299-300"skips tag discovery for unsupported repository hosts" still has the unawaited shape the two fixed tests had: expect(resolution).rejects.toBeInstanceOf(...) is not awaited, so expect(commandWasRun).toBe(false) on the next line races the effect. It passes today because resolveRepositoryRef rejects before reaching the command, but the assertion isn't pinning that. awaiting the rejection first would make it match its siblings.

Pullfrog  | Fix all ➔Fix 👍s ➔View workflow run | Using Claude Opus𝕏

Comment thread src/lib/sources/repository/tags.ts Outdated
@adelrodriguez
adelrodriguez force-pushed the 08-11-prepare_registry_and_manifest_seams_for_multi-ecosystem_projects branch from dfa04b7 to e3c47f4 Compare August 11, 2026 16:08

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ No new issues found. The one open finding from my last review is fixed, and the fix is guarded by a test that can actually fail.

Reviewed changes

Delta since my last review (dfa04b7e3c47f4), two files, nine lines.

  • Pinned the git ls-remote child locale. src/lib/sources/repository/tags.ts:122-125 now spawns with { env: { LC_ALL: "C" }, extendEnv: true }, so TRANSIENT_GIT_FAILURE_PATTERN classifies untranslated stderr regardless of the invoking user's locale. extendEnv: true merges over globalThis.process.env with env winning, so PATH and the rest of the environment survive — which the live-command test depends on to find its fake git. LC_ALL=C also suppresses GNU gettext's LANGUAGE priority list, so there's no residual leak through that variable.
  • Guarded it in the live-command test. The fake git in src/lib/sources/repository/__tests__/tags.test.ts:177-180 exits 1 with a diagnostic when LC_ALL is not C. That message doesn't match the transient pattern, so it fails on the first attempt rather than burning retries, and the test's expect(tags).toEqual(["v19.0.0"]) never sees a value.

Verification

bunx tsc --noEmit clean. bun test276 pass / 0 fail, unchanged from the prior head. I mutation-tested the new guard: reverting tags.ts to the bare ChildProcess.make("git", ["ls-remote", "--tags", source.url]) produces 13 pass / 1 fail in tags.test.ts, so the assertion is load-bearing rather than incidental. tags.ts was restored afterwards and the tree is clean.

Pullfrog  | View workflow run | Using Claude Opus𝕏

@adelrodriguez
adelrodriguez merged commit de6e0cb into main Aug 11, 2026
9 checks passed
@adelrodriguez
adelrodriguez deleted the 08-11-prepare_registry_and_manifest_seams_for_multi-ecosystem_projects branch August 11, 2026 16:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant