Dependency contracts
This page holds the details behind Instance dependencies: how the readers build a graph, how collection reads files, what the Gateway stores, and what the API returns. The code lives inApp\Domain\Instances\Dependencies and App\Actions\Instances\Dependencies.
Graph values
Each reader returns oneDependencyGraph per ecosystem.
The resolution ID keeps installation paths and peer contexts apart, so one version can appear twice. Reachability comes from actual paths from the root. A package’s own development requirements install nothing. A null target never claims that a package is present.
Reader rules
Every reader parses data in memory. None reads files, runs a package manager or script, or contacts a registry. Errors carry a stable code and a shorterror_detail naming a file or failed check, never input text, URLs, credentials, or raw parser messages. The detail is stored with the attempt and returned by scan and show, including when the previous successful snapshot is retained. Older attempts without a stored detail receive a generic diagnostic for their error code on read. Versions stay opaque, and URLs and credentials never reach the result.
This table sums up each reader.
A root
workspaces layout, several importers, path repositories, and local links return dependencies.unsupported_layout. The npm reader ignores workspaces metadata on transitive package records. An unsupported lockfile version, Bun version 3, and binary bun.lockb return dependencies.unsupported_format.
Composer
Rootrequire and require-dev set the two reachability paths. Platform requirements such as php and ext-* stay unresolved edges. A unique locked provider or replacement satisfies a virtual requirement. Aliases point at the locked resolution. Repository URLs are dropped, and source revisions stay.
npm
The caller picksnpm-shrinkwrap.json before package-lock.json. Lookup walks from the nearest package location to the root, as npm does. An alias edge keeps its declared name. Its range must match the locked version under npm’s loose semver rules. A dist-tag alias needs a registry tarball. Bundled dependency names without a lock entry stay optional edges with no target. An empty packages map is a valid empty graph when the manifest declares nothing.
pnpm
Snapshot keys keep peer contexts apart. A remote tarball key becomespnpm:sha256:<digest>, so no URL is kept. The importer must match the manifest’s specifiers and scopes.
Bun
Comments and trailing commas are allowed. Git and remote tarball references becomebun:sha256:<digest>. optionalPeers marks optional peer edges, and an empty specifier means any version.
Local packages
Afile: or link: dependency returns dependencies.unsupported_layout. Support would need a second read of files at lock-supplied paths, with the same path safety, in the same source observation.
Package manager selection
The JavaScript manager comes frompackageManager or devEngines.packageManager, then lockfiles and configuration files. A single declared manager takes precedence over stray lockfiles and configuration signals; conflicting declarations fail. A declared manager without its own lockfile fails rather than falling back to a different manager. Without a declaration, conflicting manager families still fail. Configuration files are only presence signals. They are never evaluated.
No signal at all means pnpm, which still needs
pnpm-lock.yaml.
Collection
CollectInstanceDependencyFilesAction reads the recorded checkout, or current of a production Instance, over SSH. A fixed Python program runs as the Node’s user in development and as the production user in production. For a Laravel Instance, it reads manifests, lockfiles, and manager signals only in the application directory derived from the effective web root. Root apps/site/public selects apps/site inside the checkout or selected release, not the repository-root manifests. Non-Laravel Instances keep repository-root collection. Collection never recursively discovers or combines dependency trees.
Symlinks, non-regular files, and paths that are not canonical fail. Production collection validates and pins the selected release before opening its application directory. The program reads twice and compares the repository or release identity, application directory, file metadata, and hashes. A difference returns
dependencies.source_changed.
ScanInstanceDependenciesAction holds the Instance operation lock, and for development the Node source lock, from collection through publication. It checks the Instance again inside each publication transaction. Its source snapshot retains the effective root and Laravel classification, so directory selection does not fall back to the repository root when the Instance is copied into a snapshot.
Development updates use the same directory for manager-presence inspection, source-safety checks, package commands, and the final scan. Collection receipt validation accepts only the derived directory within the recorded checkout or selected release, not an arbitrary caller-supplied path. Git identity and CLI directory-to-Instance resolution still use the whole repository. Reader rules, package-manager refusals, and collection limits stay unchanged; a nested Laravel app does not enable workspace or multi-importer support.
Stored inventory
The Gateway stores the inventory in five tables.PublishInstanceDependencyScanAction replaces one ecosystem’s observation, resolutions, edges, and attempt in one transaction. A failure records only the attempt and keeps the stored observation. A database failure records dependencies.persistence_failed. An Instance in removal gets dependencies.instance_unavailable. Removing an Instance deletes its rows but keeps shared package rows.
Responses
A scan or read returnsdata with instance_id, succeeded, composer, and javascript. succeeded is null until both ecosystems have attempts.
Each ecosystem has ecosystem, state, succeeded, attempted_at, error_code, error_detail, and snapshot. error_detail is null on success and is limited to 255 printable ASCII characters. It uses fixed diagnostics and collected file names, not values from those files. The SDK also accepts older responses without this field. The state is unknown before the first success, and stale after a later failure. A snapshot has observed_at, a source with project_root, reference, file_hashes, and format. project_root is the absolute directory whose manifests were read; for Laravel root apps/site/public, it ends in /apps/site. The production reference still identifies the selected release. The snapshot also has a graph. A null graph means the ecosystem is absent. An empty graph means a project with no packages.
An update returns instance_id, succeeded, error_code, may_have_mutated, a composer and a javascript step, and inventory. A step has ecosystem, status (succeeded, absent, failed, or not_run), may_have_mutated, and error_code. A refused update has both steps not_run and no inventory.
The SDK rejects a whole response that breaks the shape. It accepts at most 32 MiB of JSON, 50,000 resolutions and 200,000 requirements per ecosystem, 64 file hashes, and 16 KiB per text field.
Target resolution
GET /api/v1/instances/resolve?domain=DOMAIN finds the one Instance behind an active Route domain. It returns domain, instance_id, project_id, node_id, and environment. A missing or inaccessible domain returns dependencies.target_not_found. A Route with more than one target returns dependencies.target_ambiguous. The SDK accepts at most 4,096 bytes of response.
GET /api/v1/instances/resolve-directory?directory=PATH finds the Instance whose checkout or production home holds a canonical path on the caller’s Node. It returns instance_id, project_id, node_id, and environment. It never looks at other Nodes. Nested matches return dependencies.target_ambiguous; the resolver never picks the longest prefix.
Both return dependencies.instance_unavailable for an Instance that is not active or is in removal. Later operations check access and state again.