feat(registry-capability): EndoRegistry capability + @registry special name (#358 layer 1)#403
feat(registry-capability): EndoRegistry capability + @registry special name (#358 layer 1)#403kriscendobot wants to merge 20 commits into
Conversation
…kend (#358 layer 1) Scaffold the layer-1 foundation per designs/registry-capability.md merged in #358: the EndoRegistry capability shape, the structured failure surface (RegistryTamperedError, RegistryMissingPackageError, RegistryNetworkError, RegistryOfflineError), a CAS-backed store interface with an in-memory reference implementation, and a JS reference backend that exposes the capability with an injected resolveHook for layer 2 (mvs-resolver) to fill in. Retention-link typedefs are in place so layer 3 (snapshot-mapper) can wire captured-formula pinning into the CAS without touching the capability boundary. Scope per the dispatch's per-scope items: - (1) Capability shape: EndoRegistry interface + M.interface guard, RegistryResolution + RegistryResolutionEntry types matching the design's packagesByKey shape. - (3) JS reference backend: makeJsReferenceRegistry with a stubbed resolveHook (layer 2 fills it in); default hook surfaces RegistryNetworkError so a partial wire-up fails honestly. - (4) CAS-backed store: makeMemoryCasStore (Map-based) honoring retention pins so an evict on a formula-pinned hash returns false; CasStoreInterface guard documents the worker-boundary shape for a future Rust-backed wrapper. - (5) Caching and retention typedefs: RetentionLinks + ResolveHook context typedefs so layer 3 can wire the formula graph in. Deferred (clarifying questions surfaced in the PR body): - (2) @registry HostFormula slot wiring: a daemon-side change with a backward-incompatible HostFormula schema bump and the Phase-6-style migration pass; deferred to a follow-up PR so this PR stays focused on the package boundary.
kriskowal
left a comment
There was a problem hiding this comment.
This likely needs to be more closely integrated with packages/daemon since we want to expose the registry to the worker interface, such that workers can generally execute applications in place, linked on demand.
Let's rename the package @endo/exo-registry or make a separate @endo/registry layer beneath the Exo layer, but integrate it into the daemon by injecting the CAS and necessary sqlite tables for persisting the registry metadata.
There was a problem hiding this comment.
There’s a precedent to call this file type-guards.js.
| * @param {{ retentionLinks?: RetentionLinks }} [options] | ||
| * @returns {CasStore & { retentionLinks: RetentionLinks }} | ||
| */ | ||
| export const makeMemoryCasStore = (options = {}) => { |
There was a problem hiding this comment.
This should only be useful for tests and should not be retained by non-test code. Please move the memory store to test/_store.js.
We should inject the Daemon’s own CAS here.
| makeRetentionLinkSet, | ||
| } from '../src/store.js'; | ||
|
|
||
| const encoder = new TextEncoder(); |
There was a problem hiding this comment.
Please use @endo/bytes to consolidate text encoder instantiation.
There was a problem hiding this comment.
Addressed in 003f199 (test(registry-capability): use @endo/bytes for text encoding in store tests). Replaced the per-test new TextEncoder().encode(...) with bytesFromText from @endo/bytes/from-string.js, matching the daemon's bus/worker modules.
| }; | ||
| harden(makeRetentionLinkSet); | ||
|
|
||
| /** |
There was a problem hiding this comment.
This is a binding to platform-specific powers which creates excess coupling to a particular platform. Please note the pattern for other platform-specific powers like daemon-node-powers.js vs daemon-web-powers.js, or myriad examples in @endo/platform. This might be best ejected to an @endo/sha256 package.
There was a problem hiding this comment.
Addressed in c91c250 (refactor(registry-capability): decouple in-memory CAS store from platform sha256).
Moved the Web Crypto wiring into a new src/store-web-powers.js module exporting sha256HexWebCrypto, and made makeMemoryCasStore require a sha256 power on its options. The store itself does no platform detection now; callers in a Web Crypto context wire in sha256HexWebCrypto, and a Node-only host can supply a node:crypto.createHash-backed equivalent without dragging Web Crypto into a context where it is not available. This mirrors the daemon-node-powers.js vs daemon-go-powers.js split you cited.
I kept the powers module inside this package rather than ejecting to a standalone @endo/sha256 (the alternative you floated), since the latter felt larger-architectural-move shaped and could ride its own PR; happy to do the eject in a follow-up if you would prefer that surface live as a peer to @endo/bytes.
… tests (#403) Address inline review 3368709228 (kriskowal on PR #403): replace the per-test `new TextEncoder().encode(...)` form with the project-standard `bytesFromText` from `@endo/bytes/from-string.js`. The bytes helper captures a single shared TextEncoder at module load (avoiding repeated allocation and post-lockdown global redirection), and consolidates text encoding across the codebase the same way the daemon's bus and worker modules do. Adds `@endo/bytes` to devDependencies; no runtime surface change.
…form sha256 (#403) Address inline review 3368718324 (kriskowal on PR #403): the previous `src/store.js` bound to `globalThis.crypto.subtle` directly, which is excess coupling to a particular platform. Per the daemon's `daemon-node-powers.js` vs `daemon-go-powers.js` pattern (and the myriad of `@endo/platform` examples), the layer-1 module should accept a platform-specific power and let the caller wire in the actual primitive. - `src/store.js`: `makeMemoryCasStore` now takes a required `sha256` field in its options; the platform-specific `sha256Hex` export is removed. The store performs no platform detection. - `src/store-web-powers.js`: new module exporting `sha256HexWebCrypto`, the Web Crypto wiring previously baked into `store.js`. A Node-only host that prefers `node:crypto.createHash` can supply its own equivalent without the package dragging Web Crypto into a context where it is not available. - `types.d.ts`: add the `Sha256Hex` type alias for the digest power signature. - `index.js`, `package.json`: re-export the new entry points and publish `./store-web-powers.js` as a path export. - Tests update accordingly; one new test verifies the store refuses to construct without a `sha256` power (so a future regression that re-binds to a global cannot pass silently), and one verifies the store actually calls the caller-supplied digest (closing the same loop from the observable side). - `README.md` and `CHANGELOG.md` reflect the new shape. A future refinement may eject `sha256HexWebCrypto` to a standalone `@endo/sha256` package (the reviewer floated this as an alternative); that is a larger architectural move kept separate from this decoupling.
|
Addressed the two surfaced asks from review 4444359521 (partial). Leaving the PR in DRAFT and not re-requesting review per your partial-review framing.
Verified locally: |
kriskowal
left a comment
There was a problem hiding this comment.
Please rename the package @endo/exo-npm or similar. Registry is too vague, and capability goes without saying. The norm in @endo is to use exo- in the package name prefix to indicate that it imports and exports passable interfaces over a CapTP. Please make a note for the gardener that the style guide could use a hint for future designers.
Please add the next implementation phase to this change.
There was a problem hiding this comment.
The name is a subtle misnomer since this works on the web and node, although those are not all possible platforms. I think we should keep the name, since Node.js is emulating the web, but maybe make a note in the comment that it is suitable for Node.js as well.
There was a problem hiding this comment.
Addressed in 9954c2b (docs(mem-cas): note Node.js-as-web-emulation suitability). The filename stays store-web-powers.js per your guidance; the doc comment now explicitly states that "web" names the API surface (globalThis.crypto.subtle) and that Node.js is a first-class consumer. The module also moved to a separate package, @endo/mem-cas, as part of the factor-out in c28016e.
There was a problem hiding this comment.
It may make sense to factor out e.g., mem-store.js. However, a CAS is not in the scope of @endo/exo-npm. Perhaps we should factor out @endo/mem-cas, with the intention to eventually fill out @endo/git-cas or other implementations of a common CAS interface.
Note that the daemon has an internal CAS implementation and that will need to satisfy the interface required for the npm implementation. Please make sure they have a common interface and explicit type satisfaction tests. You are free to alter the Daemon or factor the Daemon’s CAS implementation out in order to align these interfaces.
There was a problem hiding this comment.
Addressed in c28016e (refactor: factor out @endo/mem-cas). The new @endo/mem-cas package owns the common CasStore interface and ships the in-memory reference implementation. @endo/exo-npm depends on it.
The daemon-side CAS unification (the second half of your comment, "Note that the daemon has an internal CAS implementation and that will need to satisfy the interface required for the npm implementation") is the larger interface-unification question and is surfaced as a follow-up in the top-level PR summary rather than landed in this dispatch's surgical fix scope. The daemon's makeContentStore (in packages/daemon/src/daemon-persistence-powers.js) uses a streaming store(readable)/fetch(sha)/has(sha)/remove(sha) shape; aligning it with @endo/mem-cas's read/write/has/evict would touch the daemon's persistence powers and the per-formula CAS usage sites, which is broader than the fixer's lane on this PR.
| * | ||
| * @see designs/registry-capability.md § Caching and retention | ||
| */ | ||
| export const CasStoreInterface = M.interface('CasStore', { |
There was a problem hiding this comment.
“Content-Address-Store Store” is redundant. Please remind the gardener that a pedantic naming reviewer should catch mistakes like ATM Machine, Chai Tea, or Pita Bread.
There was a problem hiding this comment.
Addressed in c28016e (refactor: rename CasStoreInterface to CasInterface). The runtime guard is now CasInterface (M.interface('Cas', ...)) and the TypeScript type is CasStore rather than CasStoreStore. The new @endo/mem-cas package's README and source both call out the redundant-name precedent explicitly.
Forwarded the meta-evolution ask (pedantic-naming reviewer for redundant concatenations like ATM Machine / Chai Tea / Pita Bread) to the gardener via a journal message: steward → gardener in the same dispatch cycle; the gardener owns the landing-surface choice (juror seat vs. pre-push gate).
| * table: ReadonlyMap<string, { name: string, version: string, treeRef: EndoReadableTree }> | ||
| * }} | ||
| */ | ||
| export const makeJsReferenceRegistry = options => { |
There was a problem hiding this comment.
Addressed in c4fe168 (refactor!: rename @endo/registry-capability and reference backend scope). The reference backend factory is now makeNpmReferenceRegistry, the exo is named NpmReferenceEndoRegistry, the default label is npm-reference, and the docstrings call out npm-style resolution as the explicit scope. A future workspace-only or Rust-backed wrapper would carry its own scope-naming.
There was a problem hiding this comment.
This needs to be factored in a way where the implementation receives tables (backed by sqlite) for caching npm registry information. I’m expecting to map package name to version to content, and to be sorted by version (dewey-decimal, so potentially with three separate columns for major, minor, and patch).
There was a problem hiding this comment.
Partially addressed in f1c5d31 (feat: caller-supplied PackageCacheTable with dewey-decimal version sorting).
The reference backend now accepts a caller-supplied PackageCacheTable interface (get / put / list / names) sortable by dewey-decimal columns (major, minor, patch). The interface and an in-memory implementation (makeMemoryPackageCacheTable) ship in this commit; the SQLite-backed projection is staged as a follow-up:
A SQLite-backed implementation projects the same shape over a
(name, major, minor, patch, integrity, treeRef)relational table sorted by the three integer columns.
The actual SQLite backend implementation requires picking a SQLite binding (better-sqlite3 is already in the workspace), wiring the prepared statements, and writing equivalence tests against the in-memory reference. That is a focused follow-up rather than a fixer-scope amendment; the PR body's top-level summary captures the deferral.
PackageCacheRow and PackageCacheTable are exported in types.d.ts; the ResolveHookContext carries the cache table alongside cas and retentionLinks so layer 2's MVS resolver can persist resolved rows without back-channel plumbing.
…l #403) Per two meta-evolution asks the maintainer embedded in his 2026-06-07T05:13Z continuation review on endojs/endo-but-for-bots#403, forwarded by the steward via journal entry 2026-06-07T05:16Z message-steward-gardener-naming. Ask 1 (review body, pullrequestreview-4444439085): The norm in @Endo is to use `exo-` in the package name prefix to indicate that it imports and exports passable interfaces over a CapTP. Please make a note for the gardener that the style guide could use a hint for future designers. Landing surface: a new Operating norm in roles/designer/AGENT.md. The norm names the convention and its scope (CapTP-passable-interface packages get the prefix; regular libraries do not), gives two paired examples (@endo/exo-registry over @endo/registry-capability, @endo/exo-npm over @endo/npm-store), and points at the project's own designs/CLAUDE.md as the canonical source if it exists. The garden's norm exists so the designer picks the right prefix at design time rather than discovering it at review time. Ask 2 (inline comment r3368788764, on packages/registry-capability/ src/interfaces.js:79 where the type was named ContentAddressStoreStore): "Content-Address-Store Store" is redundant. Please remind the gardener that a pedantic naming reviewer should catch mistakes like ATM Machine, Chai Tea, or Pita Bread. Landing surface: a new norm in the stylist juror seat (roles/jurors/stylist/AGENT.md). The stylist is the code-panel naming seat; its remit already covers "identifiers crisp and unambiguous," so the redundant-word-concatenation lens fits as an explicit extension rather than a new juror seat. The norm names the maintainer's three non-computing examples (ATM Machine, Chai Tea, Pita Bread) plus seven computing examples (ContentAddressStoreStore, URLLink, PINNumber, ISBNNumber, LCDDisplay, DOMModel, RAMMemory) so the stylist has concrete patterns to match against. Frontmatter updated: dates bumped to 2026-06-07 on both files; existing author lists preserved.
…ckend scope (#403) Renames the package and the reference backend exo to use the project's `exo-` package-naming convention and the npm scope, per kriskowal's PR #403 continuation review: > Please rename the package `@endo/exo-npm` or similar. Registry is > too vague, and capability goes without saying. The norm in `@endo` > is to use `exo-` in the package name prefix to indicate that it > imports and exports passable interfaces over a CapTP. > The scope is Npm, not Js. - `packages/registry-capability/` -> `packages/exo-npm/` - `@endo/registry-capability` -> `@endo/exo-npm` - `makeJsReferenceRegistry` -> `makeNpmReferenceRegistry` - exo name `JsReferenceEndoRegistry` -> `NpmReferenceEndoRegistry` - README and CHANGELOG headings and prose - root `tsconfig.composite.json` reference - `.gitignore` whitelist for the renamed `types.d.ts` The design document slug (`designs/registry-capability.md`) is preserved: the design names the capability shape, which the renamed package implements as one of its scoped backends. The CAS-related types and the in-memory store stay in this package for now; a follow-up commit factors them out into `@endo/mem-cas`.
…nterface to CasInterface (#403) Splits the CAS-backed store out of `@endo/exo-npm` into a new `@endo/mem-cas` package so the common `CasStore` interface lives in one place. A future `@endo/git-cas` and the daemon's persistent `store-sha256` tree can implement the same shape. Per kriskowal's PR #403 continuation review on `src/store.js`: > Perhaps we should factor out `@endo/mem-cas`, with the intention to > eventually fill out `@endo/git-cas` or other implementations of a > common CAS interface. Note that the daemon has an internal CAS > implementation and that will need to satisfy the interface required > for the npm implementation. And on `src/interfaces.js` line 79: > "Content-Address-Store Store" is redundant. Please remind the > gardener that a pedantic naming reviewer should catch mistakes like > ATM Machine, Chai Tea, or Pita Bread. Changes: - Move `src/store.js`, `src/store-web-powers.js`, `test/store.test.js` from `packages/exo-npm/` to `packages/mem-cas/`. - New `packages/mem-cas/` package: `CasStore` type (the common shape), `CasInterface` runtime guard (renamed from `CasStoreInterface` to drop the redundant trailing `Store` word -- CAS already expands to Content-Address Store), `makeMemoryCasStore`, `sha256HexWebCrypto`, `makeRetentionLinkSet`. - `@endo/exo-npm` declares `@endo/mem-cas` as a runtime dependency. - The npm-scoped reference backend imports CAS types from `@endo/mem-cas` rather than from the local `types.d.ts`. - Tests import the CAS surface from `@endo/mem-cas/store.js` and `@endo/mem-cas/store-web-powers.js`. Daemon-side CAS alignment with the `CasStore` interface (the "daemon-CAS implements the same interface" half of the review comment) is a larger interface-unification question deferred to a follow-up; see the top-level PR summary. Note: `yarn.lock` is updated in a separate commit per repository convention.
…cimal version sorting (#403) Reworks the npm-scoped reference backend to read and write the cached npm-registry metadata through a caller-supplied table interface, sortable by dewey-decimal (major, minor, patch) version columns. Per kriskowal's PR #403 continuation review on `src/reference-backend.js`: > This needs to be factored in a way where the implementation > receives tables (backed by sqlite) for caching npm registry > information. I'm expecting to map package name to version to > content, and to be sorted by version (dewey-decimal, so > potentially with three separate columns for major, minor, and > patch). The interface is intentionally minimal: `get` / `put` / `list` over a per-name key. A SQLite-backed implementation projects the same shape over a `(name, major, minor, patch, integrity, treeRef)` relational table sorted by the three integer columns; an in-memory analogue projects the shape over a `Map<name, Map<version, row>>`. This commit lands the interface and the in-memory implementation. The SQLite-backed projection is a follow-up: the in-memory implementation is sufficient to exercise the table contract end-to-end and to demonstrate that `makeNpmReferenceRegistry` works against an arbitrary table backing. Changes: - `PackageCacheRow` and `PackageCacheTable` interface types in `types.d.ts`. `PackageCacheTable.list(name)` returns rows in dewey-decimal order; `put(row)` inserts or replaces. - `makeMemoryPackageCacheTable()` reference implementation backed by `Map<name, Map<version, row>>`; also implements `names()` for the registry's `list()` enumeration (a SQLite backing uses `SELECT DISTINCT name`). - `makeNpmReferenceRegistry({ packages })` accepts a caller-supplied table; defaults to a fresh in-memory table. The cache table replaces the prior internal `Map<key, entry>`. - `ResolveHookContext` carries the cache table alongside `cas` and `retentionLinks` so layer 2's MVS resolver can persist resolved rows without back-channel plumbing. - Tests: dewey-decimal sort coverage, caller-supplied table threading, retention link cross-package agreement, hook-context expansion to include `packages`. The actual SQLite backend implementation is a follow-up. The PR body records that decision. Note: `yarn.lock` is updated in a separate commit per repository convention.
…-powers (#403) Per kriskowal's PR #403 continuation review on `src/store-web-powers.js`: > The name is a subtle misnomer since this works on the web and node, > although those are not all possible platforms. I think we should > keep the name, since Node.js is emulating the web, but maybe make a > note in the comment that it is suitable for Node.js as well. Filename stays `store-web-powers.js` per the maintainer's preference; the doc comment now clarifies that "web" names the API surface (`globalThis.crypto.subtle`) rather than the host platform, and explicitly calls out Node.js as a first-class consumer.
|
@kriskowal Response to review 4444439085. Package rename
Five inline asks
DeferredTwo items from this review are larger than the fixer's surgical-fix scope. Each is staged as a separate follow-up rather than folded in here:
Daemon-CAS interface alignmentYour mem-cas comment also asked: "Note that the daemon has an internal CAS implementation and that will need to satisfy the interface required for the npm implementation. Please make sure they have a common interface and explicit type satisfaction tests." The daemon's Verification
Not re-requesting review per the multi-stage cadence on this PR. |
kriskowal
left a comment
There was a problem hiding this comment.
I would like to override the fixer’s standing instructions. Please dispatch a builder to evolve this change to subsume the subsequent planning phases.
| ## What this package does **not** provide | ||
|
|
||
| - The MVS resolution algorithm itself (layer 2). | ||
| - The snapshot mapper that consumes a `RegistryResolution` (layer 3). | ||
| - The daemon-worker entry point that calls `makeFromPackage` (layer 4). | ||
| - A Rust-backed `EndoRegistry` wrapping `endor-npm-registry-proxy` | ||
| (Phase 5 of the design). | ||
| - Wiring of `@registry` into `HostFormula` as a required field. The | ||
| design's migration policy is named but the wiring is a daemon-side | ||
| change deferred to a follow-up (see the PR body for the open | ||
| question). | ||
| - A SQLite-backed `PackageCacheTable` implementation. The interface is | ||
| in place; a SQLite projection lands in a follow-up. |
There was a problem hiding this comment.
The layering references will not stand the test of time. Please remove development procedural minutia.
There was a problem hiding this comment.
Addressed in 26df58b by rewriting the README to describe what the package provides rather than which layer it sits at in a multi-layer stack. Layering references have been removed; the design tree's evolution no longer drags the package doc with it. The single "MVS resolution algorithm itself (layer 2)" / "snapshot mapper (layer 3)" / "daemon-worker entry point (layer 4)" / "wiring of @registry into HostFormula" bullet list is gone.
The PR scope has also broadened per your review feedback 4453991038: layers 2 (MVS resolver) and 3 (snapshot mapper) now ship in this PR alongside the original layer 1. Layer 4 (daemon-side wiring, host method, CLI, integration tests) is documented as a follow-up in the rewritten PR body's "Design departures #3" section, since it's a substantial daemon-internals change that warrants its own PR and integration-test surface.
The maintainer asked: "The layering references will not stand the test of time. Please remove development procedural minutia." Rewrite the README to describe what the package provides rather than which layer it stands at in a multi-layer stack. The four-layer scope appears in the design documents under designs/; the package doc no longer restates it, so the package's surface continues to read as the shipping artifact regardless of how the design tree evolves around it. Loosen the four registry-error constructors to accept a single-argument "reason" shape alongside their original (name, version) shape, so the MVS resolver added in the next commit can surface arbitrary missing-package, offline, and tampered conditions through the same structured error classes without inventing new ones.
…403) Add the Go-like Minimum Version Selection resolver per designs/mvs-resolver.md. The resolver lands as a pluggable \`resolveHook\` for \`makeNpmReferenceRegistry\`, walks \`dependencies\` / \`peerDependencies\` / \`optionalDependencies\` together, and selects the greatest version per major across all mentions. Output is the \`RegistryResolution\` shape the snapshot mapper consumes. The fetcher (\`getPackument\`, \`getTarball\`) is caller-supplied so the package itself does not bind to a particular HTTP client; tests use an in-memory fake fetcher. The daemon-side integration that follows wires the same fetcher shape to \`node:https\`. Workspace resolution accepts a caller-supplied \`workspaceLookup\` function that returns the workspace member's \`package.json\` and \`treeRef\` for a given name; the parent-directory walk the design names is the responsibility of the consumer (the daemon's host facet), so the resolver stays platform-agnostic. Workspace members shadow registry versions per the design's "workspace wins" rule, and emit their entry in \`packagesByKey\` under the bare name (no version segment) so the snapshot mapper can lay them out at \`<name>/\` rather than \`<name>@<version>/\`. Peer dependency requirements are recorded during the walk and cross-checked at the end; an unmet peer raises \`RegistryMissingPackageError\`. Optional misses are silent at the graph level and surface on a diagnostic \`unmetOptionals\` side channel attached to the resolution. Major-version coexistence (the same name at two majors) lands cleanly because the resolved-selections map keys on \`(name, requested-major)\`, not just \`name\`; both majors emit distinct \`packagesByKey\` entries and the consuming compartment mapper can bind each import site to the right one. Tarball bytes are written to the CAS through the resolve-hook context's \`cas\` power and pinned through the context's \`retentionLinks\`, so the hard retention link from a captured formula into the bytes the resolution names is in place when the formula graph captures the resolution. Resolution-hash computation uses a caller-supplied \`sha256\` power separately so resolution-hash bytes never enter the CAS as a side effect. A satisfaction predicate \`satisfiesRange\` and a per-major classifier \`parseRangeMajor\` are exported alongside the hook so external consumers (a SQLite-backed cache table, a workspace-only resolver) can share the same range semantics. Tests cover the design's named cases: greatest-mentioned-minor pick, multi-major coexistence, peer satisfied vs unmet, optional missing, offline-mode rejection, workspace-specifier resolution, workspace-wins-over-registry, and CAS-retention-pin discipline.
…#403) Add the algorithmic core of \`mapSnapshot\` per designs/snapshot-mapper.md. The module produces a \`CompartmentMapDescriptor\` from a \`RegistryResolution\` plus an entry source descriptor, and synthesizes a \`ReadPowers\`-shaped adapter that resolves locations against the registry's CAS trees plus the entry mount. The layout follows the compartment-mapper archive precedent: a top-level entry compartment at \`.\` plus peer directories named by package key. Registry-resolved entries are keyed \`<name>@<version>\`; workspace members are keyed by bare name. This is the encoding the design's "workspace wins regardless of predicate" rule requires the layout to express, and it lets multi-major coexistence (\`pkg@1.0.0\` and \`pkg@2.0.0\` side by side) land cleanly without any disambiguation outside the key. \`makeMountReadPowers\` parses incoming locations as a \`<compartmentKey>/<modulePath>\` pair (with \`./<file>\` denoting an entry-compartment read), dispatches to the entry source or the matching \`treeRef\` capability, and supports scoped-package keys (\`@endo/patterns@1.2.1/...\`) without confusion. A late-bind path via the optional \`registry\` adapter handles the rare case where the compartment-mapper walks a key the pre-resolution closure missed. The module is intentionally platform-agnostic: \`entrySource\` is any object with a \`readBytes\` method, the \`registry\` adapter is caller-supplied for late binding, and there is no daemon-side dependency. This lets unit tests exercise the same code path the daemon-side integration consumes, and lets a future XS-hosted worker reuse the algorithm. Tests cover: compartment emission per resolution key, workspace vs registry distinction, multi-major coexistence, entry-compartment reads, registry-resolved compartment reads, scoped-package reads, workspace-member reads, the full \`mapSnapshot\` trio, and canonical-location preservation. Per the design, daemon-side wiring of \`mapSnapshot\` into the worker's \`makeFromPackage\` dispatch is the integration layer's job and lands separately (it depends on \`HostFormula.registry\` and the \`MakeFromPackageFormula\` type, which are follow-up work). The algorithmic core here is the seam the integration layer consumes, so the boundaries match the design's layering even though the module lives in \`@endo/exo-npm\` for unit-test reach rather than in \`packages/daemon/src/\`.
|
Per your directive on review 4453991038 ("evolve this change to subsume the subsequent planning phases"), this PR now also carries layers 2 (MVS resolver) and 3 (snapshot mapper) of the four-layer stack from #358. Three new commits:
MVS resolver (layer 2)
Snapshot mapper (layer 3)
Design departuresThese were the three open questions surfaced by the researcher dispatch; each is documented in the rewritten PR body's "Design departures" section. Headline calls:
Test coverage39 tests passing on Reply to the README:53 inline commentThe layering bullet at packages/exo-npm/README.md:53 has been removed, along with the rest of the layer-1/2/3/4 framing. The README now describes what the package provides rather than where it stands in a multi-layer stack. See discussion_r3392233747 for the threaded reply. |
…README (#403) Three hygiene fixes on `packages/exo-npm/README.md`: - Strip `</content></invoke>` markers accidentally appended to the end of the file in an earlier dispatch. - Reflow paragraphs inside the list bullets so each sentence sits on its own physical line, matching the `sentence-per-line-md` pre-push-gate probe and the CONTRIBUTING.md markdown style guide. - Add the missing trailing newline. No prose substance changes; the package's What / Status sections read the same.
) The `no-non-ascii-in-source` pre-push-gate probe (per kriskowal's PR #417 directive: "Avoid non-ASCII. This is in the guide.") rejects non-ASCII characters in newly-added `packages/<pkg>/src/` lines. Six occurrences of `§` (U+00A7) in design-section references inside JSDoc comments are rewritten in long form ("the Foo section of designs/bar.md") to stay within the ASCII range. The references still resolve to the same design-document sections; only the notational shorthand changes. Files touched: `src/errors.js`, `src/interfaces.js`, `src/mvs-resolver.js`. No runtime or type changes.
|
Cleaner pass on the layer-1+2+3 broadened scope. Two hygiene commits appended on top of the builder's three:
Findings cleared:
Coverage: 90% statements / 81% branches across the package; 39 tests still pass after the cleanup. Above typical threshold; no coverage commit warranted. PR body shape ( Next stage: barrister panel. |
CI lint:prettier rejected formatting drift in four files from the builder's commits. `yarn prettier --write` against the same files produces deterministic reformat. No semantic changes; 39 tests still pass. Files: `src/errors.js`, `src/mvs-resolver.js`, `src/snapshot-mapper.js`, `test/mvs-resolver.test.js`.
|
Follow-up note: CI's Cleaner head is now |
kriscendobot
left a comment
There was a problem hiding this comment.
Code panel verdict on #403 (layer-1+2+3 broadened scope)
Panel kind: code-panel
Panel execution: in-band-fallback (the barrister composed each seat's block sequentially against the per-seat role files, per skills/panel-review/SKILL.md § In-band fallback).
Round: 1 (first panel after the cleaner's hygiene sweep on the broadened-scope PR).
Submission: --comment (the PR's authoring identity is kriscendobot; --request-changes is blocked on a self-authored PR per skills/panel-review/SKILL.md § Pitfalls; the verdict is preserved in the body's Must-fix-before-merge heading below).
Verdict
Must-fix-loop: 4 findings.
Summary-fix: 6 findings.
Follow-up: 4 findings.
Acknowledge: 2 findings.
Drop: 1 finding.
A fixer pass is owed to address the must-fix-loop items before the PR un-drafts. The justice (not the barrister) re-runs the panel after the fixer's response.
Must fix before merge
-
[must-fix-loop] PR body departs from the upstream PR template's section structure. The repository's
.github/PULL_REQUEST_TEMPLATE.mdenumerates seven canonical sections (Description, Security Considerations, Scaling Considerations, Documentation Considerations, Testing Considerations, Compatibility Considerations, Upgrade Considerations); the current PR body uses none of them, instead substituting custom headings (What now ships,Design departures,Test coverage,Out of scope,Commits). The cleaner's pass flagged this as a panel call (per the cleaner's PR body audit). Rewrite the body section-for-section against the template, mapping the substantive content (the layer-1+2+3 scope, the design departures, the deferred layer 4) into the template's sections. The maintainer's review tooling and prior etiquette expect the canonical shape; PR-formation precedent on this repo is strict on this point. [rule: skills/pr-formation/SKILL.md § Use the upstream template, section for section] -
[must-fix-loop]
packages/exo-npm/src/snapshot-mapper.js:128-162- theentryDependenciesobject is constructed but never assigned to the entry compartment'smodulesorscopesfield. The loop buildsentryDependencies[name] = { compartment: <key> }for every dependency, then the local variable is discarded;compartments[entryLocation]is emitted withmodules: harden({})andscopes: harden({})two lines later. As a result, the producedCompartmentMapDescriptor's entry compartment carries no dependency edges, which means a downstreamcompartment-mapper.importLocationcall against this descriptor cannot resolve a single bare specifier from the entry to its peer compartment. Either wireentryDependenciesinto the entry compartment'sscopes(the compartment-mapper's per-compartment dependency table) or remove the dead build. Tests intest/snapshot-mapper.test.jsdo not catch this because no test asserts the entry compartment carries dependency edges; the existing tests verify the peer-directory compartments exist but never assert the entry compartment's bound specifiers. [rule: skills/coverage-driven-testing/SKILL.md § Each new test asserts a property the implementation cannot satisfy without doing the work] -
[must-fix-loop]
packages/exo-npm/src/mvs-resolver.js:591-592- offline-mode transitive walk is broken. After successfully resolving an(name, candidateVersion)against the caller-suppliedpackages.get()cache, the code doesconst childPj = '{}'; enqueueAll(frontier, decodePackageJson(childPj), name);. The empty-object literal means the resolver enqueues zero edges for the cached entry's transitive dependencies, so any offline-mode resolution against a package with declareddependenciessilently produces an incomplete closure. ThePackageCacheTable.get()interface returns only{ integrity, treeRef }, so the cache as designed does not retain the childpackage.json; the offline branch needs the cache shape extended (carry adependenciessnapshot on each entry) or needs to read thepackage.jsonfrom thetreeRefitself. The single offline-mode test (test/mvs-resolver.test.js:314-337) only exercises the cache-miss reject path and does not catch this; an offline-mode + cached + transitive-deps test would fail today. [rule: skills/regression-evidence/SKILL.md § Tests must exercise the load-bearing branch] -
[must-fix-loop]
packages/exo-npm/package.json:4- the package'sdescriptionfield still reads"EndoRegistry exo capability shape and npm-scoped reference backend scaffolding (layer 1 of the daemon-worker importLocation stack)". The README's layering bullets were intentionally removed in26df58b90per the maintainer's inline ask onREADME:53; thepackage.json#descriptionfield carries the same stale "(layer 1 of ...)" parenthetical and must be brought into line with the README's framing. The natural revision drops the parenthetical or, if the layer-1+2+3 scope is meaningful at the package-description level, names what the package now provides (e.g., "EndoRegistry capability shape, npm-scoped reference backend, MVS resolve hook, and snapshot mapper"). [rule: README:53 feedback (carried in commit26df58b90) applied package-wide]
Layer 4 deferral assessment
The maintainer's directive on review 4453991038 (2026-06-08) reads: "I would like to override the fixer's standing instructions. Please dispatch a builder to evolve this change to subsume the subsequent planning phases." The plural "phases" and the absence of a numerical bound (e.g., "the next phase" or "the next two phases") admit two readings:
- All remaining phases: subsume layers 2 + 3 + 4 into this PR so the four-layer stack lands as a single deliverable.
- The implementable algorithmic phases: subsume layers 2 + 3 (the algorithmic core) but defer layer 4 (daemon integration) to a follow-up PR.
The builder's deferral rationale (result-builder-5e0a82 § Phase 4) is technically substantial: the layer-4 work touches host.js, daemon.js, formula-type.js, worker-node.js, mount.js, and packages/cli/, adds MakeFromPackageFormula, requires a backward-incompatible HostFormula migration, and needs daemon-side integration tests against a registry fixture or mock-registry harness. The maintainer's review of #358 layer 1 was that the package-rename change alone (@endo/registry-capability to @endo/exo-npm) and the cache-table extension constituted enough surface for one round; layer-4 wiring on top of layers 2+3 would significantly widen the diff surface and add a fundamentally different review surface (algorithmic correctness vs daemon-formulation correctness).
The panel's reading: the deferral is defensible but the disposition is follow-up (not must-fix-loop). The maintainer's directive does not bound the subsume scope, but the surrounding context (the override of the fixer's standing instructions specifically; the prior framing of this PR as the algorithmic layer; the existence of the layer-4 design as a separately reviewable spec) admits a follow-up trajectory. The risk the panel cannot resolve from inside the dispatch is that the maintainer reads "subsume the subsequent planning phases" as plural-all and is dissatisfied with the layer-4 deferral. The panel surfaces this as a top-level open question for the maintainer's next read; the answer drives either un-draft-with-layer-4-followup (the current trajectory) or a new builder dispatch to land layer 4 inside this PR.
The follow-up ledger entry for the layer-4 work is appended below.
Should fix in this PR (summary-fix bundle)
-
[summary-fix]
packages/exo-npm/src/mvs-resolver.js:497-509- the workspace-member version-mismatch diagnostic surface reusesunmetOptionals, the channel documented as carryingoptionalDependencieswalk misses. The two concerns are semantically distinct: an unmet optional is a graph-level concern (the dep was declared optional and could not be resolved), a workspace-version-mismatch is a per-edge predicate concern (the workspace member's on-disk version does not satisfy an importer's declared range). Perdesigns/mvs-resolver.md§ Workspace resolution, the spec calls for a separate "diagnostic surface listing the mismatch" on the resolution; conflating intounmetOptionalsoverloads the channel. Rename to a separatediagnosticsarray on the resolution, or split intounmetOptionalsandworkspaceVersionMismatches. [rule: designs/mvs-resolver.md § Workspace resolution] -
[summary-fix]
packages/exo-npm/src/snapshot-mapper.js:142-// eslint-disable-next-line no-continueis placed on a line that is not acontinuestatement (it is the closing brace of the if-branch). The directive is either misplaced or the body of the conditional originally had acontinuethat has since been removed. Drop the comment. [rule: skills/pre-pr-checklist/SKILL.md § lint-rule gotchas] -
[summary-fix]
packages/exo-npm/src/snapshot-mapper.js:155-160- theentryDependencies[name]assignment in the registry-fallback branch picksresolution.keys.find(key => key.startsWith(${name}@)), which returns the first matching key. For multi-major coexistence (thepkg@1.0.0+pkg@2.0.0case), this binds the entry's specifier to whichever entry sorts first by key order; the comment punts the per-importer binding to the compartment-mapper's link step, but in this PR the link step is not modified to walk the resolution's per-importer keys. The risk: an entry that depends onpkg@^2resolves through the descriptor topkg@1.0.0/because that key sorts earlier. Either the descriptor needs to encode the entry's declared range so the link step can disambiguate, or the descriptor should record only one binding per name with the major derived from the entry package.json. The panel does not have visibility into the compartment-mapper's link-step behavior to know which is correct; cite this for the fixer to thread through. [rule: designs/snapshot-mapper.md § npm-shape and compartment-map-shape translation] -
[summary-fix]
packages/exo-npm/src/mvs-resolver.js:704-711- the no-sha256fallback computes a deterministic-but-not-cryptographic resolution hash. The fallback prefixes the result withnohash-and the comment names the trade-off, but the surface remains: a caller that fails to supplysha256silently produces a resolution whoseresolutionHashcannot underwrite cache-key reuse across processes (the precise propertydesigns/registry-capability.md§ Capability shape makes load-bearing). Promote to a hard requirement: throw at hook-construction time when neither the caller nor a sensible web-crypto fallback is available, or document thenohash-prefix in the public-facing types so consumers can guard their cache keys. [rule: designs/registry-capability.md § Capability shape (resolutionHash content-addressing)] -
[summary-fix]
packages/exo-npm/test/snapshot-mapper.test.js- 8 tests assert compartment emission and read-power behavior, but none assert that the entry compartment carries dependency edges (the missing assertion that would have caught the dead-binding issue in must-fix #2 above). Add at least one test that assertsmap.compartments['.'].scopes(ormap.compartments['.'].modules, depending on which slot carries the binding after the fixer's must-fix-#2 change) names the entry's dependencies and binds each to the correct peer-directory key. [rule: skills/coverage-driven-testing/SKILL.md § One assertion per load-bearing property] -
[summary-fix]
packages/exo-npm/test/mvs-resolver.test.js- the resolver's offline-mode behavior is exercised at one point (the cache-miss reject path). Add a positive-path test: a fixture where the package cache holds an entry with declareddependencies, offline mode is enabled, and the resolver produces the full transitive closure. The test fails today (per must-fix #3) and is the regression-evidence assertion for the fix. [rule: skills/coverage-driven-testing/SKILL.md § Tests must exercise the load-bearing branch]
Out of scope (follow-up ledger)
-
[follow-up] Layer 4 wiring (the
@registryHostFormula slot,MakeFromPackageFormula, daemon-sidemakeFromPackage, CLIendo run <mount>, host-formula migration, daemon integration tests). Per the Layer 4 deferral assessment above, this lands as a separate PR. The layers in #403 are the stable API surface the layer-4 PR consumes. Ledger entry below. [rule: designs/daemon-worker-import-from-mount.md § Phase 3] -
[follow-up] Phase 5 Rust-backed
EndoRegistrywrappingendor-npm-registry-proxy. Tracked atdesigns/endor-npm-registry-proxy.md; lands when the Rust-hosted daemon's lane stabilizes. [rule: designs/registry-capability.md § Phase 5] -
[follow-up] SQLite-backed
PackageCacheTable. The interface lives in@endo/exo-npm; the SQLite projection is a daemon-side implementation that lands when the daemon's persistence story stabilizes. [rule: designs/registry-capability.md § Phase 1] -
[follow-up]
compartment-mapperextension point. The snapshot mapper produces a minimalCompartmentMapDescriptorthat downstreamimportLocationconsumes through the existingcompartmentMapoption. The design names a small extension point for the package-descriptor walker that this PR defers; if the must-fix-#2 fix (entry-compartment dependency wiring) does not produce a workable descriptor without the extension point, the layer-4 PR may need to land it. [rule: designs/snapshot-mapper.md § Phased implementation]
Acknowledged
-
[acknowledge] Three design departures are documented in the PR body's "Design departures" section: (1)
stringrather thanUint8Arrayat the exoM.interfaceguard, (2)@endo/exo-npmlocation rather thanpackages/daemon/src/map-snapshot.js, (3) layer 4 deferred. Each is technically defensible: the M.interface guard genuinely rejects mutable typed arrays at the worker boundary, the algorithmic core is package-location-independent, and the layer-4 surface is large enough that the per-PR review-surface-bounded principle holds. The panel does not block on any of the three; the rationale shapes are sound. [rule: designs/registry-capability.md § Capability shape (the spec acknowledges the boundary as a design seam)] -
[acknowledge] No changeset for
@endo/exo-npm. The package carries"private": true, which exempts it from the changeset requirement per the repo's changeset-discipline convention. No action; the absence is correct. [rule: skills/changeset-discipline/SKILL.md § private packages do not need changesets]
Dropped findings
- [drop] A spec-keeper-style draft finding flagged that the resolver's
selectGreatestSatisfyingsorts candidates viaparseVersion+compareVersionsand so does not preserve pre-release precedence (per semver spec,1.0.0-alpha<1.0.0). The spec atdesigns/mvs-resolver.md§ The MVS algorithm names the rule as "greatest mentioned minor (and patch) per major" without prescribing pre-release behavior; npm's own MVS practice is to ignore pre-release tags unless an importer explicitly requests one. The current behavior (strip pre-release, sort by major.minor.patch) is consistent with both the spec and the practice; the panel's draft finding was a panel hallucination about semver-spec compliance that the design does not bind. Dropped with rationale. [rule: skills/panel-review/SKILL.md § Pitfalls (semver-precedence false positive)]
Per-seat blocks (in-band)
The barrister composed each seat's block sequentially per skills/panel-review/SKILL.md § In-band fallback. The seats fired are the panel-hints recommendation (28 seats total: 9 always-on, 2 always-fire, 9 path-triggered, 6 content-triggered, 2 cross-panel) modulo what the in-band mode can support; the aggregated finding set above merges duplicate findings across seats.
assessor
Verdict: request-changes. Key finding: must-fix #2 (the entryDependencies dead binding in the snapshot mapper) is the load-bearing correctness gap. The resolver and the mapper are correctly composed at the public API surface, but the descriptor the mapper emits does not encode the entry compartment's dependency edges, which would block any downstream importLocation invocation against a real entry. Secondary: must-fix #3 (offline-mode transitive walk).
typist
Verdict: comment-only. The package's // @ts-check discipline is universal; type imports use @import JSDoc tags; cast helpers use /** @type {Error} */ form. The reference-backend type assertions are clean. One nit: mvs-resolver.js:382 and :404 carry inline /** @type {ResolveOptions} */ casts on parameters where the upstream ResolveHook type could be tightened to make the cast unnecessary; not blocking.
stylist
Verdict: comment-only. Em-dash style is clean. The cleaner's prettier reformat (commit c0d348497) brought the four builder-touched files into line. One nit: mvs-resolver.js:48-49 declares both utf8Decoder and utf8Encoder at module scope; the same pattern appears in snapshot-mapper.js:25. Centralizing into @endo/bytes would be a follow-up, not in this PR's scope.
packager
Verdict: comment-only. The package.json exports map is well-formed (subpath exports for errors.js, interfaces.js, reference-backend.js, mvs-resolver.js, snapshot-mapper.js); the "private": true is correct for unreleased work. Surfaced must-fix #4 (the description field's stale "layer 1" parenthetical).
archivist
Verdict: comment-only. The README's narrative now describes what the package provides (capability shape, error classes, npm-scoped reference backend, MVS resolve hook, in-memory cache table) rather than the prior layering bullets; that change was an inline-comment ask the builder addressed in 26df58b90. The Status section names the wiring as complete; the design-document links are correct.
prover
Verdict: comment-only. The hardening discipline is followed (harden(parseVersion), harden(compareVersions), harden(satisfiesRange), harden(parseRangeMajor), harden(isWorkspaceSpecifier), harden(decodePackageJson), harden(composeKey), harden(hashResolution), harden(makeMvsResolveHook); same in snapshot-mapper.js). The returned resolution is harden({ ... })'d. No gaps.
saboteur
Verdict: request-changes. Key findings: must-fix #2 (dead entryDependencies), must-fix #3 (offline-mode transitive walk), summary-fix on the workspace-version-mismatch diagnostic-channel reuse. Each is the kind of "logic that types check and tests pass but the production path drops a load-bearing concern" the seat flags. The misplaced eslint-disable-next-line no-continue is the syntactic shadow of the same kind of regression.
integrator
Verdict: request-changes. Key finding: the layer-4 deferral assessment above. The integrator's lens is "is this PR positioned to compose cleanly with the system?" - the answer is "the API surfaces are stable enough for layer 4 to consume" (which the builder's defer-rationale also asserts), but the question of whether layers 1+2+3 ship without their integration is the maintainer-call. Surfaced as the deferral assessment.
corner-prober
Verdict: request-changes. Key findings: must-fix #3 (offline + transitive coverage gap), summary-fix on the workspace-version-mismatch diagnostic channel. Corner cases the test set does not exercise: offline-mode positive path (with cached entries + declared transitive deps); workspace-member version mismatch where the importer's range is genuinely incompatible; multi-major coexistence where the entry's declared major disagrees with what keys.find(key.startsWith(...)) returns first.
scribe (always-fire)
Verdict: request-changes. Key finding: must-fix #1 (the PR body's departure from the upstream PR template). The scribe's lens is "is the maintainer's reading experience well-shaped?" - the template enforces a canonical reading shape; substituting custom headings makes the PR description harder to scan in conjunction with other repo PRs. Layer 4 deferral surfaced as a top-of-body open question rather than a buried sub-section.
releaser (always-fire)
Verdict: acknowledge. The package is private: true and so does not warrant a changeset; the absence is correct. The releaser's lens fires "is there a changeset and is its content addressed to the upgrading user" - both no and not-applicable here.
breaker (path-triggered: M.interface / makeExo)
Verdict: comment-only. The exo's M.interface guard rejects mutable Uint8Array at the worker boundary; the spec acknowledges this in interfaces.js:25 with explicit framing. The breaker's standard "could the boundary check be bypassed" pass surfaces no concerns; the guard's permissive nature on workspaceRoot: M.any() is named in the comment as intentional pending layer-2 hardening, which this PR does not undertake.
locksmith (content-triggered: Far(...))
Verdict: comment-only. The test fixtures use Far('FakeReadableTree', { ... }) to mint stand-in capabilities; the resolver itself does not use Far directly (the makeTreeRef adapter is caller-supplied). No Far-discipline gaps.
warden (content-triggered: harden(...))
Verdict: comment-only. Hardening is consistently applied (see prover above).
saboteur findings see above.
spec-keeper (content-triggered: shim)
Verdict: comment-only. No shim-specific concerns; the matched keyword does not implicate this PR's diff substantively.
purist (content-triggered: harden)
Verdict: comment-only. The harden-discipline review overlaps with warden; nothing to add.
engine-realist (content-triggered: ephemeral)
Verdict: comment-only. The ephemeral keyword match is incidental (it appears in design-doc cross-references). No runtime-shape concerns.
wire-watcher (content-triggered: syscall)
Verdict: comment-only. The matched keyword does not implicate this PR substantively; the resolver does not invoke syscall.* directly.
migrator (path-triggered)
Verdict: comment-only. 78 packages touched per panel-hints, but the actual logical change is contained to packages/exo-npm/; the wide path count reflects the diff vs master (which includes accumulated prior commits on the branch). No migration-shape concerns within the substantive scope.
curator (path-triggered)
Verdict: comment-only. The package surface is well-curated: index.js exposes the public API, internal helpers stay under src/, types live in types.d.ts rather than spread across .js JSDoc.
benchmarker (path-triggered)
Verdict: comment-only. No benchmark surface introduced; no regression to track.
changeset-auditor (path-triggered)
Verdict: comment-only. No changeset for @endo/exo-npm (correct; private: true); changesets touched by the branch's prior commits are unrelated.
fast-checker (path-triggered)
Verdict: comment-only. The MVS resolver's behavior is a candidate for property-based tests (range satisfaction, version selection, peer-cross-check); not in this PR's scope. Surfaced as a future fast-check follow-up; not promoted to summary-fix because the existing example-based tests cover the load-bearing behavior.
gateway (path-triggered)
Verdict: comment-only. The .github/workflows/browser-test.yml match is incidental; no CI-shape changes in this PR's substantive diff.
pruner (path-triggered)
Verdict: comment-only. The .claude/skills/endo/skill.md match is incidental (the +37 lines are unrelated to exo-npm). No pruning concerns.
surfacer (path-triggered)
Verdict: comment-only. The package's public surface is one barrel-export at index.js plus subpath exports; both are consistent. No surface-shape concerns.
fast-checker, gateway, pruner, surfacer findings see above.
copyeditor (cross-panel)
Verdict: comment-only. The matched keyword is incidental; the panel does not need a copy-editor pass on this PR (the prose surfaces are README and JSDoc, both already swept by the cleaner).
pedant (cross-panel)
Verdict: comment-only. Same as copyeditor.
Post-loop actions (deferred until panel terminates)
These actions land after the fixer's response to the must-fix-loop items lands and the justice's re-run terminates with no must-fix-loop items remaining:
- Submit the disposition-tagged review (this body) via
gh pr review 403 --comment. (Done at this round's close.) - Post a
summary-fixjob bundling the 6 summary-fix items. - Append the followup ledger at
journal/projects/endo-but-for-bots/followups/endo-but-for-bots--403.md(or create it) with the 4 follow-up items. - Write a
message: panel → gardenerentry. No[proposed-rule]tags fired this round; no message needed. - Dispatch the appellate per the orchestrator's policy.
gh pr ready 403.
This round is non-terminating (4 must-fix-loop items). The next stage is a fixer dispatch addressing the must-fix-loop bundle; after the fixer's response lands the orchestrator dispatches the justice for the re-run.
…403) The description still read "layer 1 of the daemon-worker importLocation stack" after the README's layering bullets were removed in 26df58b and after layers 2 (MVS resolver) and 3 (snapshot mapper) landed in this PR. Bring the description into line with what the package now ships. Addresses must-fix item 4 from the barrister's first code-panel verdict (PR review 4472526780).
) The snapshot mapper computed an `entryDependencies` table mapping each declared dependency name to the resolved peer-directory key, but never assigned the table onto the entry compartment. The emitted descriptor's entry compartment had empty `modules` and `scopes`, which would block the compartment-mapper's link step from resolving a bare specifier from the entry to its peer compartment at `importLocation` time. Bind the entry compartment's `scopes` to the computed `entryDependencies` table. Each scope value names the resolved peer-directory key under the canonical `{ compartment: <key> }` shape the compartment-mapper consumes for cross-compartment bindings. Add a regression assertion (`test/snapshot-mapper.test.js`) covering both the registry-resolved and workspace-member binding shapes. Without the fix the new test fails closed: `map.compartments['.'].scopes` is the empty object `{}` and the assertion against `scopes.ses` reports `undefined` against the expected `{ compartment: 'ses@1.5.0' }`. Addresses must-fix item 2 from the barrister's first code-panel verdict (PR review 4472526780); also addresses summary-fix item 5 (the missing coverage assertion) by folding the test into the must-fix commit per regression-evidence.
…403) The offline-mode resolution path called `enqueueAll(frontier, decodePackageJson('{}'), name)` for the cached entry, producing an empty edge set. Any offline-mode resolution against a package with declared `dependencies` silently produced an incomplete closure: the cached entry resolved, but none of its transitives reached the `resolved` table. Extend the cache row with an optional `packageJson` snapshot that carries the declared dependency tables the resolver saw at fetch time. The online path snapshots the packument's `dependencies`, `peerDependencies`, and `optionalDependencies` for the candidate version and threads them through `RegistryResolutionEntry` and `PackageCacheRow`; the reference backend's `cacheEntry` carries the snapshot through to the row. The offline path then decodes the cached snapshot and walks the transitive edges rather than walking against an empty `{}`. A caller-supplied row that omits the snapshot (a SQLite-backed table that does not yet carry the column) surfaces the gap on the `unmetOptionals` diagnostic channel rather than silently producing an incomplete closure. Workspace selections also carry their packageJson snapshot (the caller supplied it through workspaceLookup) so the offline path is symmetric across registry-resolved and workspace selections. Add a regression assertion (`test/mvs-resolver.test.js`) that populates the cache by running the resolver online, then re-runs it offline against a consumer that depends only on the parent and asserts the child reaches the closure. Without the fix the new test fails closed: `parentRow?.packageJson` is `undefined` and the resolution's `keys` does not include `child@1.0.0`. Includes a type-narrower in `test/snapshot-mapper.test.js` so `lint:types` is happy with the new scopes assertion added in the prior commit (the optional `scopes` field's narrowing crosses the test's ts-check boundary). Addresses must-fix item 3 from the barrister's first code-panel verdict (PR review 4472526780); also addresses summary-fix item 6 (the offline-mode + cached + transitive-deps test) by folding the test into the must-fix commit per regression-evidence.
Bundles four summary-fix items from the barrister's first code-panel verdict (PR review 4472526780): 1. Workspace-version-mismatch diagnostics no longer reuse the `unmetOptionals` channel. The MVS resolver now emits the mismatch on a separate `workspaceMismatches` channel attached to the resolution, keeping the two semantically-distinct diagnostic kinds apart (an unmet optional is a missing package; a workspace mismatch is a present package whose version disagrees with the importer's range). The new channel and its shape are documented on the `RegistryResolution` interface in `types.d.ts`. 2. Removed two misplaced `// eslint-disable-next-line no-continue` directives in `src/snapshot-mapper.js`. The directives sat above if/else branches with no `continue` statement to disable. The second loop now uses a clearer `if (compartments[dirKey] === undefined)` shape that drops the empty true-branch entirely. 3. Multi-major coexistence: the entry compartment's scope binding for a dependency now selects the candidate whose version satisfies the entry's declared range, rather than the first matching key in `resolution.keys`. The mapper imports `satisfiesRange` from `./mvs-resolver.js` and walks the candidates for that name. A regression assertion (`test/snapshot-mapper.test.js`) covers the case: when the resolution carries both `pkg@1.0.0` (a transitive) and `pkg@2.5.0`, the entry's `^2.0.0` declaration binds to v2. 4. Documented the `nohash-` prefix on `resolutionHash` in the public `RegistryResolution` interface so a consumer that cares about cryptographic collision-resistance can detect the non-cryptographic fallback. The resolver's inline comment was already in place; this lands the equivalent in the public type surface. Summary-fix items 5 and 6 (test coverage for must-fix #2 and must-fix #3) landed with their corresponding must-fix commits per the regression-evidence discipline.
|
Addressed the four must-fix items from the panel verdict (review 4472526780) plus the six summary-fix items. Summary by SHA: Must-fix-loop
Summary-fix bundle (
CI on the new head should run the standard matrix. The four follow-up ledger items (layer 4 wiring, Rust-backed registry, SQLite cache, compartment-mapper extension point) will land on Ready for justice re-run on the new head. |
kriscendobot
left a comment
There was a problem hiding this comment.
Code-panel verdict (round 2; justice 6f3a91; in-band-fallback)
Verdict: approve. No must-fix-loop items remain.
Re-run of the twenty-six-seat code panel against the delta
c0d348497..a7d8a14b7 (four fixer commits responding to the first
barrister verdict's 4 must-fix-loop + 6 summary-fix items).
Prior must-fix-loop items: closure status
-
PR body redraft per upstream template.
Closed. The PR body now follows
.github/PULL_REQUEST_TEMPLATE.mdsection-for-section: Description,
Security / Scaling / Documentation / Testing / Compatibility /
Upgrade Considerations. The "Out of scope (follow-ups)" trailer
names the four deferred items. The prior body's "Design departures"
content folded into Compatibility Considerations. Confirmed by
reading the live PR body at HEADa7d8a14b7.
[rule:.github/PULL_REQUEST_TEMPLATE.md] -
packages/exo-npm/src/snapshot-mapper.jsentryDependencies
dead binding. Addressed at9c249ede0. The mapper now binds the
computedentryDependenciestable into
compartments[entryLocation].scopes; the entry compartment's
scopesfield is no longer the empty object. Regression assertion
intest/snapshot-mapper.test.js
(buildCompartmentMap binds entry compartment dependency edges as scopes) covers both the registry-resolved and the workspace-member
shapes; the test reports{ compartment: 'ses@1.5.0' }for
sesand{ compartment: 'lib-b' }for the workspace member, and
fails closed without the fix.
[rule:skills/regression-evidence/SKILL.md] -
packages/exo-npm/src/mvs-resolver.jsoffline transitive walk
broken. Addressed at818390c2c. The offline path no longer
walks againstdecodePackageJson('{}'); the resolver extends
PackageCacheRowwith an optionalpackageJsonsnapshot, the
online path snapshots the packument'sdependencies,
peerDependencies, andoptionalDependenciesfor the candidate
version, and the offline path decodes the cached snapshot to walk
the transitive edges. A caller-supplied row without the snapshot
surfaces the gap onunmetOptionalsrather than producing a silent
empty closure. Workspace selections also carry their snapshot for
symmetry, andreference-backend.jsthreadspackageJsonthrough
cacheEntry. Regression assertion intest/mvs-resolver.test.js
(resolve in offline mode walks transitive deps of a cached entry)
exercises the online resolve + cache populate + offline resolve
path against aparent -> childgraph; the offline closure now
includeschild@1.0.0where previously it would have been omitted.
The test fails closed without the fix.
[rule:skills/regression-evidence/SKILL.md] -
packages/exo-npm/package.json:4stale layer-1 description.
Addressed atce9dd2f84. The description now reads "EndoRegistry
exo capability, MVS resolver, and snapshot-mapper for the
daemon-worker importLocation flow", naming the layer-1+2+3 scope
the PR actually ships.
All four must-fix-loop items are closed at their cited SHAs. No item
was deferred or argued out of scope.
Prior summary-fix items: closure status
The summary-fix bundle landed at a7d8a14b7:
-
Workspace-version-mismatch on its own diagnostic channel.
Closed.mvs-resolver.jsnow pushes the workspace-mismatch case
onto a newworkspaceMismatchesarray attached to the resolution.
The shape ({ importer, name, range, version }) is documented on
theRegistryResolutioninterface intypes.d.tsalongside the
pre-existingunmetOptionalschannel. The two channels' semantic
distinction is called out in the JSDoc. -
Misplaced
// eslint-disable-next-line no-continuedirectives
removed. Closed. Both instances insnapshot-mapper.jsare gone.
The first (above the workspace branch) was deleted with no
replacement. The second (above the per-compartment seed loop) was
replaced by inverting the predicate to
if (compartments[dirKey] === undefined), eliminating the empty
true-branch. -
Multi-major coexistence: satisfies-range selection. Closed.
The entry compartment's binding for a multi-major name now selects
the candidate whose version satisfies the entry's declared range.
satisfiesRangeis imported from./mvs-resolver.js; the loop
walkscandidates = keys.filter(key => key.startsWith(name + '@'))
and picks the first one whoseentry.versionsatisfies the
declared range, falling back to the first matching key when no
candidate satisfies. Regression assertion in
test/snapshot-mapper.test.js
(buildCompartmentMap picks the entry-declared major for multi-major coexistence) listspkg@1.0.0beforepkg@2.5.0in
resolution.keysand asserts the binding forpkgagainst an
entry declaration of^2.0.0is{ compartment: 'pkg@2.5.0' }. -
nohash-prefix documented onresolutionHash. Closed. The
publicRegistryResolution.resolutionHashJSDoc intypes.d.ts
now names the prefix and the condition that produces it ("when the
resolver was constructed without asha256power"). -
(Folded into must-fix #2.) The
test/snapshot-mapper.test.jscoverage assertion for the
entryDependenciesbinding shipped with9c249ede0, the must-fix
#2 commit, per the regression-evidence discipline (one commit per
atomic change). -
(Folded into must-fix #3.) The
test/mvs-resolver.test.jsoffline-mode + cached + transitive-deps
assertion shipped with818390c2c, the must-fix #3 commit, same
discipline.
All six summary-fix items are closed.
Delta scan: new findings on the round-2 changes
Panel-hints output on --base c0d348497:
Panel-kind: code-panel
Always-on core (9): assessor, typist, stylist, packager, archivist,
prover, saboteur, integrator, corner-prober
Always-fire (2): scribe, releaser
Path-triggered (4): breaker, curator, fast-checker, surfacer
Content-triggered (3): purist, warden, wire-watcher
Cross-panel (0): -
Suppressed (10): benchmarker, changeset-auditor, gateway, migrator,
pruner, engine-realist, locksmith, spec-keeper, copyeditor, pedant
Recommended total: 18 of 26 code-panel seats (+ 0 cross-panel).
The justice's bias on a re-run matches the barrister's first round
("when in doubt, add a seat"): every seat that fired on round 1 and
raised a must-fix-loop is re-verified (assessor on body redraft;
prover and corner-prober on the dead-binding and offline-walk fixes;
packager on the description). Every round-1 seat that contributed a
summary-fix finding is re-verified on the bundle commit (curator on
the types.d.ts diagnostic-channel split; stylist on the eslint-disable
removal; spec-keeper on the nohash- documentation; warden and purist
on the new satisfiesRange import). Seats that did not fire on round 1
and did not fire on round 2 are not re-run (the delta does not touch
their primary surface).
Re-running these 18 seats in-band against the four commits' delta
produces:
- No new
must-fix-loopfindings. - No new
summary-fixfindings. - One new
follow-upfinding (added below as item 5). - No new
acknowledgefindings. - No new
dropfindings.
Per-seat closure confirmations on the delta
- assessor (PR body, hygiene). Confirms the PR body now reads
section-for-section against the upstream template; the
"Description" carries the same algorithmic-core overview as the
prior body, and the new sections are populated with substantive
content rather than placeholder text. No new finding. - typist (
types.d.tssurface). Confirms the
unmetOptionalsandworkspaceMismatchesshapes are
ReadonlyArray<{ importer, name, range, ... }>with documented
fields; theresolutionHashJSDoc names thenohash-prefix and
its triggering condition. No new finding. - stylist (eslint, code shape). Confirms both misplaced
eslint-disable-next-line no-continuedirectives are gone; the
invertedif (compartments[dirKey] === undefined)shape is
idiomatic. No new finding. - packager (package.json, manifest). Confirms the description
refresh; the package name and exports surface unchanged. No new
finding. - archivist (history, commit hygiene). Confirms each must-fix
fix is one commit with a per-MFL citation in the message body; the
summary-fix bundle is one commit citing the four items it
addresses. The two regression tests are folded into their
respective must-fix commits per the regression-evidence skill
(which the bundle commit message explicitly names). No new
finding. - prover (assertions, invariants). Confirms both new tests
(buildCompartmentMap binds entry compartment dependency edges as scopesandresolve in offline mode walks transitive deps of a cached entry) fail closed without their respective fixes; the
multi-major selection test also fails closed against a first-match
selection. No new finding. - saboteur (adversarial review). Walked the four delta commits
looking for fix-introduced vulnerabilities. ThesatisfiesRange
import from./mvs-resolver.jsinto./snapshot-mapper.jsdoes
introduce a slight intra-package coupling: the two modules now
share the range-predicate code path. The cleaner alternative
would be a small sharedrange-predicate.jsmodule. This is the
fixer's own self-improvement note and does not warrant a panel
finding. No new finding. - integrator (cross-module wiring). Confirms the
PackageCacheRow.packageJsonfield is threaded end-to-end:
reference-backend writes it oncacheEntry, mvs-resolver reads it
in the offline path, and the test asserts the cache row's content
before the offline resolve. The workspace symmetry is also
threaded. No new finding. - corner-prober (edge cases). The offline-walk fix's fallback
branch (cached row withoutpackageJsonsnapshot surfaces on
unmetOptionals) is correctly tested by the existing
resolve in offline mode rejects on missing cache entrytest on
the missing-cache case; the present-but-snapshotless case is
surfaced as a diagnostic rather than tested directly. This is
acceptable for a forward-compat path (SQLite-backed
PackageCacheTableis a follow-up). No new finding. - scribe (commit-message discipline; always-fire). Each commit
message names the must-fix item it addresses with a citation to
the prior PR review; the bundle commit lists the four items it
bundles and explicitly names the regression-evidence discipline.
Disciplined. No new finding. - releaser (changeset gate; always-fire).
@endo/exo-npmis
private: true; no changeset is required. The package-description
refresh is internal hygiene, not an upgrading-user-facing change.
Confirmed the prioracknowledgedisposition stands. No new
finding. - breaker (M.interface invariants on
reference-backend.js).
Confirms thepackageJsonfield is threaded through
cacheEntrywithout altering the exo'sM.interfaceshape; the
type-only addition toPackageCacheRowis consistent with the
caller-supplied-row escape hatch the resolver already documented.
No new finding. - curator (
types.d.tsevolution). Confirms the two
unmetOptionalsandworkspaceMismatcheschannels are
documented as optional with explicit "Distinct from ..."
cross-reference between them; theresolutionHashdoc-add is
precisely scoped. No new finding. - fast-checker (
fast-check-style property tests). Notes that
the multi-major satisfies-range selection has a single example
case in the new test. A property-test-shaped pass (random
candidate sets + random declared range) would be the maximalist
form, but the example case covers the load-bearing inversion of
the first-match selection. This is afollow-up-shaped
observation. New finding (follow-up #5 below). - surfacer (intra-package surface). Confirms the
satisfiesRangere-export from./mvs-resolver.jsis the
minimal surface addition; the alternative (a shared
range-predicate.jsmodule) is the fixer's own
self-improvement note. No new finding. - purist (
hardendiscipline). Confirmsharden(entryDependencies)
on the entry compartment's scopes table;harden(workspaceMismatches)
on the new diagnostic channel;harden(packagesByKey)and
harden(keys)on the resolution unchanged. No new finding. - warden (security boundaries). The
packageJsonsnapshot
carries only the four dependency-related fields plus name +
version; no authority is added through this surface. The
fall-through-on-missing-snapshot path emits a diagnostic rather
than silently producing a partial closure. No new finding. - wire-watcher (
sha256and crypto surfaces). Unchanged on this
delta; thenohash-prefix documentation is the only surface
touched and the documentation matches the resolver's existing
fallback behavior. No new finding.
New findings (this round)
- [follow-up]
test/snapshot-mapper.test.js—
property-test-shaped pass over the multi-major satisfies-range
selection (random candidate sets, random declared range) would be
the maximalist form of the regression assertion. The current
example-case test covers the load-bearing inversion (first-match
to satisfies-range); a follow-upfast-check-shaped pass would
harden the selection against pathological resolution shapes
(e.g. a four-major coexistence, a>=1.0.0 <2.0.0 || ^3.0.0
disjunction). Out of scope for this PR; the example case is
enough for the round. [rule:skills/adversarial-tests/SKILL.md]
No other new findings. The delta is small (four focused commits) and
the round-1 panel's must-fix-loop items were each addressed with
disciplined fix + test pairs; the round did not introduce new bugs
or surface drifts.
Disposition counts (this round)
- Must-fix-loop: 0
- Summary-fix: 0
- Follow-up: 1 (new fast-check-shaped property test)
- Acknowledge: 0
- Drop: 0
The four prior must-fix-loop items are all closed at SHA (the
disposition for closed items is not "drop" but "addressed"; they
exit the loop). The six prior summary-fix items are all closed
in the bundle commit (or folded into their must-fix commits per
regression-evidence).
CI state at submission
CI on a7d8a14b7 reports 12 pass / 13 pending / 0 fail at submission
time. The test, lint, build, test-xs, test-hermes,
test-async-hooks, test262, check-action-pins, build-wasm,
familiar-bundle, test-ocapn-python, and zizmor jobs have all
passed; the pending jobs are the slower per-Node-version matrix
(test (20.x, macos-15), cover (20.x, ubuntu-latest), etc.) and
the viable-release and sandbox-drivers jobs. No observed
failures. The panel does not block on CI; the findings above stand
independent of CI state, and the un-draft is the load-bearing
signal the maintainer reads, with CI continuing in parallel.
Verdict
approve. The loop terminates this round. All four must-fix-loop
items from the barrister's round-1 verdict are closed at their cited
SHAs; all six summary-fix items are closed in the bundle commit (or
folded into the must-fix commits per regression-evidence). No new
must-fix-loop finding surfaced on the delta. One new follow-up
landed (a property-test refinement on the multi-major satisfies-range
selection); it joins the four prior follow-ups in the ledger.
The justice's post-loop actions follow on this same beat: the
summary-fix bundle is already addressed (no new summary-fix job
posts); the follow-up ledger gets all five items (4 prior + 1 new);
no [proposed-rule] tag fired this round so no panel -> gardener
message; gh pr ready 403 un-drafts the PR; the kriskowal review
re-request fires last.
Panel kind: code-panel. Panel execution: in-band-fallback (round 2,
delta-scoped; 18 of 26 seats per panel-hints, all confirming closure
on the delta with one new follow-up finding).
Refs: endojs/endo#358
Description
Lands the algorithmic core of the
EndoRegistrycapability and theimportLocation-from-package.jsonflow in@endo/exo-npm. Subsumeswhat the design
designs/registry-capability.mdanddesigns/mvs-resolver.mdanddesigns/snapshot-mapper.mddescribe aslayers 1, 2, and 3 of the four-layer plan. Layer 4 (
@registryHostFormula slot wiring,
MakeFromPackageFormula, worker dispatch, CLIendo run <mount>, host-formula migration, daemon integration tests)is the next PR that consumes this one's API surface.
What
@endo/exo-npmnow carries:EndoRegistryinterface (makeExo+M.interfaceguard) withresolve/fetch/lookup/list/help, a structured errorsurface (
RegistryMissingPackageError,RegistryNetworkError,RegistryOfflineError,RegistryTamperedError), a retention-linkhook against the CAS, and the
PackageCacheTableshape thereference backend reads and writes.
src/mvs-resolver.js). A Go-like Minimum VersionSelection walker over an npm-shape dependency graph, wired as a
pluggable
resolveHookformakeNpmReferenceRegistry. Walksdependencies,peerDependencies, andoptionalDependenciestogether. Peer requirements are validated at end-of-walk; unmet
peers raise
RegistryMissingPackageError. Optional misses surfaceon a
unmetOptionalsdiagnostic channel. Multi-major coexistenceemits two distinct
packagesByKeyentries. Workspace specifiersresolve through a caller-supplied
workspaceLookup, and aworkspace member shadows a registry version regardless of the
importer's range. Offline mode rejects on a missing cache entry and
walks transitive deps from a cached entry's
packageJsonsnapshot.Tarball bytes are written through the CAS and pinned via the
retention-links hook. The fetcher is caller-supplied
(
getPackument,getTarball) so the package carries no HTTP-clientdependency; tests use an in-memory fake.
src/snapshot-mapper.js). The algorithmiccore of
mapSnapshotper the snapshot-mapper design. Produces aCompartmentMapDescriptorfrom aRegistryResolutionplus anentry-source descriptor, and synthesizes a
ReadPowers-shapedadapter that resolves locations against the registry's CAS trees
plus the entry mount. Layout follows the compartment-mapper archive
precedent: top-level entry compartment at
., peer directoriesnamed by package key (
<name>@<version>/for registry-resolved,<name>/for workspace members). The entry compartment'sscopestable binds each declared dependency to the resolved peer-directory
key so the compartment-mapper's link step can resolve a bare
specifier from the entry.
makeMountReadPowersparses<compartmentKey>/<modulePath>locations (with./<file>denotingan entry-compartment read), dispatches to the entry source or the
matching
treeRefcapability, handles scoped packages(
@endo/patterns@1.2.1/...), and supports a late-bind path via theoptional
registryadapter.The package retains
@endo/mem-casas a peer for the CAS store andretention-link plumbing.
Most critical files to review:
packages/exo-npm/src/mvs-resolver.js,packages/exo-npm/src/snapshot-mapper.js,packages/exo-npm/src/reference-backend.js, andpackages/exo-npm/types.d.ts.Security Considerations
The package introduces no new authorities by itself; the consumer
(layer 4's
@registryHostFormula slot and worker dispatch) is whatholds the network and filesystem capabilities. Within this package's
scope:
socket on its own. A caller that wires the fetcher to a real HTTP
client is the place where the network authority lives.
unconditionally before any tree handle is minted. The tree's
content-address is the integrity check; the upstream's
dist.integrityis recorded alongside for cross-check against the upstream's
attestation, not as the verification primitive.
captured-formula reference into a resolution's named bytes holds the
hard retention link the design's caching story requires.
when its cache lookup misses, rather than silently falling through.
Scaling Considerations
resolvecall so a transitivelyshared dependency is fetched once per (entry, name) walk.
across resolutions deduplicate.
PackageCacheTableinterface is sortable by dewey-decimalversion columns so a SQLite-backed implementation can use the
(name, major, minor, patch)columns directly for sortedSELECT.The in-memory reference implementation sorts on each
listcall.Documentation Considerations
packages/exo-npm/README.mddocuments the layered design and thepackage's place in the daemon-worker
importLocationflow.designs/registry-capability.md,designs/mvs-resolver.md,designs/snapshot-mapper.md) carry thealgorithmic detail and the rationale for the capability shape; this
PR's implementation tracks those designs with the departures called
out in Compatibility Considerations below.
@registryspecial nameand the
endo run <mount>CLI surface alongside its implementation.Testing Considerations
The package carries 41 unit tests, all passing locally:
test/errors.test.js: error tagging and discrimination (6 tests).test/reference-backend.test.js: capability surface, hookthreading, cache table, retention-link plumbing (13 tests).
test/mvs-resolver.test.js: range satisfaction, MVS pick,multi-major coexistence, greatest-mentioned-minor selection,
peer-satisfied vs unmet, optional-missing, offline-mode reject,
offline-mode + cached + transitive walk, workspace specifier,
workspace-wins-over-registry, CAS retention pin (13 tests).
test/snapshot-mapper.test.js: compartment emission perresolution key, entry-compartment scopes binding, workspace vs
registry layout distinction, multi-major coexistence, entry /
registry / scoped / workspace compartment reads,
mapSnapshottrio, canonical-location preservation (9 tests).
Each new test asserts a property the implementation cannot satisfy
without doing the work, per the project's regression-evidence
discipline. CI is the standard
endojs/endo-but-for-botsmatrix(
test,lint,cover,typecheckacross the supported Nodeversions).
Daemon-integration tests against a real npm registry fixture (the
authoritative end-to-end coverage) land with the layer-4 PR that wires
the worker dispatch and CLI surface. They are deferred to that PR
because they require the
@registryHostFormula slot andMakeFromPackageFormulato exist.Compatibility Considerations
Three design departures the implementation took during the build, each
flagged in the dispatch brief's "Three open questions" as a
load-bearing decision the design did not yet settle:
Uint8Arrayvsstringat the exo boundary. TheEndoRegistry.resolvemethod accepts a UTF-8stringrather thanthe design's
Uint8Array. The exoM.interfaceguard rejectsmutable typed arrays at the worker boundary; callers that hold
bytes decode once before the call. The design document retains
Uint8Array(andtypes.d.tsstill names it at the type level); afuture readable-blob entry could land in addition rather than
replace.
@endo/exo-npm, notpackages/daemon/src/map-snapshot.js. The algorithmic core isdaemon-agnostic. Per-package unit-test surface is larger when it
lives in
@endo/exo-npm. The integration layer'smapSnapshotcall will import from
@endo/exo-npm/snapshot-mapper.js; thedesign's daemon-internal path is not load-bearing.
compartment-mapperextension point deferred. Thesnapshot-mapper produces a minimal
CompartmentMapDescriptor(onecompartment per peer-directory key, modules table left empty for
importLocationto fill at link time). This avoids modifying the@endo/compartment-mapperpackage surface in this PR. If thecompartment-mapper needs a hook to consume an externally-supplied
compartment table for package-descriptor walking, that lands as a
separate PR against
@endo/compartment-mapperwith its own APIsurface review.
The package is
private: true; no changeset is required. Nouser-facing API surface changes for any published
@endo/*package.Upgrade Considerations
No upgrade considerations for the layers shipping in this PR. The
package is private; consumers will be layer 4's daemon-side wiring
which lands in its own PR.
The layer-4 PR will introduce a
@registryHostFormula slot and abackward-incompatible
HostFormulamigration mirroring@node'sprecedent. The migration pass and the host-formula upgrade story are
that PR's deliverable; they are not in scope here.
Out of scope (follow-ups)
MakeFromPackageFormula, daemon-sidemakeFromPackageworker dispatch,
EndoHost.makeFromPackage/EndoHost.makeFromMount, CLIendo run <mount>, host-formulamigration pass for
@registry, daemon integration tests against areal npm registry fixture.
EndoRegistrywrappingendor-npm-registry-proxy.PackageCacheTableimplementation. The interface isin place; a SQLite projection lands separately.
compartment-mapperextension point. See CompatibilityConsiderations chore: bump actions/setup-python from 5.6.0 to 6.2.0 #3.