Deps redesign: package universe repositories, Resolver-owned selection - #142
Open
JPDuchesne wants to merge 38 commits into
Open
Deps redesign: package universe repositories, Resolver-owned selection#142JPDuchesne wants to merge 38 commits into
JPDuchesne wants to merge 38 commits into
Conversation
The deps domain conflated four concepts inside Repository#fetch's untyped id hash: identity, available versions, requirement, and pin. This adds the two missing halves as first-class types so the repository layer can state facts without also carrying constraints or choices: - PackageId: constraint-free, version-free identity, keyed by integration so two ecosystems publishing the same name no longer collide. - Package: the aggregate a Repository returns — an identity plus its available versions, with no satisfies?/sort/best_match (those belong to VersionScheme and Resolver respectively). - PackageVersion: one version's facts, with every optional fact in its empty form rather than nil. - Artifact: bytes dev fetches itself, digest as an enforcement input. - DependencyEdge: a version's outgoing requirement, constraint untouched. Co-authored-by: Cursor <cursoragent@cursor.com>
Constraint semantics are a property of an ecosystem, not of any package or repository, so they get their own strategy seam: Package states facts, VersionScheme evaluates predicates (satisfies?/sort), Resolver chooses. No repository evaluated constraints before this — ficsit silently ignored its declared ^ ranges and took the newest version — so these schemes are the missing predicate layer, not an extraction: - GemScheme: Gem::Requirement/Gem::Version (bundler) - SemverScheme: node-style ranges (^ ~ comparators, conjunction) for ficsit - Pep440Scheme: the PEP 440 subset pip declarations use, pip-style cmpkey - RockScheme: luarocks dotted+revision grammar, where 3.4-1 releases above 3.4 rather than semver's prerelease-below reading - PinnedScheme: brew/cmake/gh/steam/xcode universes arrive pre-narrowed by the backing service; everything satisfies, reported order stands Co-authored-by: Cursor <cursoragent@cursor.com>
The facts-only repository contract: no lifecycle, no constraint, no choice. #fetch and #prepare stay temporarily (marked deprecated) so each repository can gain find in its own green commit; both die in the Resolver cutover commit. Co-authored-by: Cursor <cursoragent@cursor.com>
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
Two contract refinements the repository reshapes need: - PackageVersion#metadata: ecosystem-specific facts the integration reads at install (mod_id, release assets, tap). The repository composes it, so minted pins keep today's lockfile metadata shapes exactly. - Repository#find(id, filter:): the declaration constraint as a server-side locator. Pinned ecosystems (git tag, release tag, steam buildid, brew suffix) need it to locate their singleton universe; filtering returns all matching versions and never picks — range evaluation stays with VersionScheme, choice with the Resolver. Co-authored-by: Cursor <cursoragent@cursor.com>
Every published version becomes a PackageVersion: targets as platforms, each target's download as an Artifact carrying the API's SHA256 (dev-enforced integrity), required mod deps as edges, and the install facts FicsitIntegration reads. This is the fix for ficsit ignoring semver constraints: the whole universe is now visible, so the Resolver can pick with SemverScheme instead of blindly taking versions.first. A missing requested platform no longer raises here — the block just lacks it, and disqualifying the version is the Resolver's call. ModNotFoundError is now a Repository::PackageNotFoundError. #fetch is untouched until the cutover. Co-authored-by: Cursor <cursoragent@cursor.com>
luarocks search yields versions (deduplicated across arches) and nothing more, so that is the universe: no digests — luarocks verifies rockspec integrity itself at install, and the old resolve-time download-and-hash produced an audit hash and a downloaded_path that nothing read — and no edges. Constraint evaluation moves to RockScheme, fixing the old take- first-ignore-constraint behavior. #fetch stays until the cutover. Co-authored-by: Cursor <cursoragent@cursor.com>
brew info answers with a single current stable version, so the universe is a singleton located by the filter: the declared version is a formula suffix (18 selects llvm@18), tap scopes the name, cask switches to an unversioned entry. Casks use an empty-string version stand-in that the Resolver mints back to nil. Bottle SHA256 rides as the version digest. Co-authored-by: Cursor <cursoragent@cursor.com>
GitHub refs are not an enumerable version index, so the filter's tag locates the one release (prebuilt shape) or ref (source shape) the declaration pins; the owner/repo slug rides as PackageId#source. Install facts mirror today's pin metadata exactly — asset digests for GhIntegration's download verification, commit SHA for provenance. ReleaseNotFoundError is now a Repository::PackageNotFoundError. Co-authored-by: Cursor <cursoragent@cursor.com>
One GET https://pypi.org/pypi/<name>/json yields every published version with file digests — no more pip download at resolve time. Each version's digest is its sdist SHA256 (platform-independent), wheel fallback, nil for yanked/file-less releases. Edges stay empty: pip still owns the transitive tree at install. Constraint evaluation moves to Pep440Scheme. Co-authored-by: Cursor <cursoragent@cursor.com>
None of these ecosystems has an enumerable version index, so each find reports a singleton universe located by the filter: git resolves the declared tag/commit to its SHA via ls-remote (RefResolutionError is now a PackageNotFoundError), steam pins an explicit buildid or the branch's current one via SteamCMD, xcode's declared version IS the universe, and url downloads once to mint a dev-enforced trust-on-first-use SHA256. Install facts mirror today's pin metadata key for key. Co-authored-by: Cursor <cursoragent@cursor.com>
The whole-set solve that hid in BundlerRepository#prepare gets its own home: Locker is the batch seam (lock(declarations) -> tool lockfile), and BundlerLocker owns Gemfile generation plus bundle lock. BundlerRepository becomes what it always really was — a reader over Gemfile.lock: find reports each gem's singleton universe (the joint solve's choice, with the CHECKSUMS digest). MissingGemError is now a PackageNotFoundError. prepare/fetch remain as thin deprecated delegations until the cutover. Co-authored-by: Cursor <cursoragent@cursor.com>
The Resolver needs one rescue point: a universe can legitimately contain versions that ignore the ecosystem's conventions (old tags, oddball uploads), and those candidates should be skipped as non-satisfying, not fail the resolve — while a malformed constraint is the user's declaration being wrong and must propagate. Each scheme's InvalidVersionError and InvalidConstraintError now subclass the VersionScheme base pair. Co-authored-by: Cursor <cursoragent@cursor.com>
The Resolver is now the choice layer over pure facts: for each declaration it asks the repository for the package universe (find, with the constraint riding along as a server-side locator), filters candidates through the integration's VersionScheme — treating scheme-unparseable universe versions as non-satisfying rather than fatal — takes the highest satisfying version that publishes every explicitly requested platform, mints the pin from that version's facts, and walks its edges for transitives (which inherit the declaring dep's group, host, and env). Disagreeing constraints on one name are rejected up front. With no callers left, the per-item fetch contract and the prepare lifecycle hook are deleted from Repository and every implementation, along with their now-dead private helpers and error classes; bundler's lock step lives only in BundlerLocker, and BundlerRepository is a pure Gemfile.lock reader. Repository tests covering behavior unique to fetch (gh auth/API errors, brew tap retry, ficsit link fallback) are ported to find; the rest are deleted as duplicates of existing find coverage. Also fixes typed-strict debt srb tc surfaced in the new domain types (untyped-receiver equality returning nilable booleans, redundant T.must on Array#<=> results). Co-authored-by: Cursor <cursoragent@cursor.com>
Every Registry entry now declares its VersionScheme (the ecosystem's constraint semantics) alongside its repository, and entries whose tool owns the whole-set solve declare a Locker (bundler -> BundlerLocker). update-deps becomes a lock-then-resolve pipeline: each integration's locker runs over its declarations first, so repositories read an already-solved universe, then the Resolver is built from Registry.repositories + Registry.schemes. BundlerRepository no longer takes ruby_version_requirement — that's the locker's concern. The registry consistency test grows two anti-drift guards: every *_scheme.rb (bar the abstract base) and every *_locker.rb class must be referenced by a registry entry. Co-authored-by: Cursor <cursoragent@cursor.com>
The reference the lib/dev/deps comments point at: the four-concept ontology (identity / universe / requirement / pin), the layer table with each class's one question, the lock-then-resolve pipeline, per-integration constraint semantics, the three integrity regimes, the new-ecosystem recipe, and the solve-ownership decision gate — per-ecosystem hybrid leaning dev-owned, with the criteria for revisiting bundler/pip/luarocks tool ownership recorded. Co-authored-by: Cursor <cursoragent@cursor.com>
Require blocks keep the redesign's package/package_id/package_version requires and adopt main's convention of loading sorbet-runtime once in src/dev.rb (applied to every new deps file, matching the sweep in 5a3024a). Ficsit's find keeps the redesign body with main's single target binding ported. Dead main-side requires (tempfile, digest/open3/ tmpdir, repository.rb's dependency pair) dropped with the code that used them. Co-authored-by: Cursor <cursoragent@cursor.com>
The >/<= operator branches in Pep440Scheme, RockScheme, and SemverScheme gain Where-table rows; the abstract Locker#lock raise gets its own test mirroring VersionScheme's; PipRepository#get_project's HTTP seam is asserted against the PyPI project URL; and update-deps' locker dispatch is exercised with a manifest gem declaration and a mocked locker. Co-authored-by: Cursor <cursoragent@cursor.com>
The same name under two integrations is two packages: each resolves against its own integration's universe, transitive edges stay inside the declaring dep's integration, constraint-conflict detection and platform unioning are scoped per (integration, name). Co-authored-by: Cursor <cursoragent@cursor.com>
The lockfile key becomes (integration, name), matching package identity, so the same name under two integrations occupies two keys instead of colliding. The reader keeps a legacy flat-format shim until every consumer repo's lockfiles are rewritten by update-deps. BuildContainer's install_dir/build-context scans now parse lockfiles through Lockfile instead of raw YAML, so format knowledge (including the shim) lives in one place; project_needs_llvm? tolerates the dep key's indentation. Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
JPDuchesne
marked this pull request as ready for review
September 5, 2026 19:17
Co-authored-by: Cursor <cursoragent@cursor.com>
Declaration is the shared atom (name, integration, constraint) — the part of a dependency ask that whoever authored it can state. Scope (group, host, env) is the resolution context that rides the resolve walk parent -> child as a unit; ScopedDeclaration composes the two plus the per-row axes (platform, post_install) that deliberately do not inherit. Composition, not subclassing: a scoped declaration must never pass where a context-free Declaration is expected. The resolver now inherits transitive context by copying one Scope object instead of three fields, and attach_install_scoping collapses into Scope#to_metadata. The three new types ride the pre-bundle bootstrap chain (dsl.rb), so they stay sorbet-runtime-free and join the StrictSigil exclusion list. Co-authored-by: Cursor <cursoragent@cursor.com>
…he seam A PackageVersion's declared dependencies are now Declarations — the same shared atom the project side wraps in ScopedDeclarations. The reporting repository stamps the integration (ficsit mods require ficsit mods) and normalizes the upstream constraint syntax into dev's shape at construction, so constraints cross the system boundary exactly once. That finishes Resolver#normalize_constraint's job at the right layer: the resolver now receives finished Declarations and only stamps the walk context (Scope) onto them, deleting the raw-constraint case analysis. Co-authored-by: Cursor <cursoragent@cursor.com>
A bare array can't say which regime the claim was made under — [] collapses 'affirmatively requires nothing' into 'the tool owns a closure dev never sees'. Declarations::Resolved([Declaration...]) and Declarations::ToolOwned make both states representable; sealed! so consumers can case-and-T.absurd. The claim will travel with the data: each Repository constructs the variant its regime warrants, making construction the dispatch (no resolver guard, no registry attribute). Wired into PackageVersion in the next commit. Co-authored-by: Cursor <cursoragent@cursor.com>
The field now returns the sum type instead of a bare array, defaulting to the affirmative Resolved([]) — ToolOwned must be stated explicitly. The resolver walk cases on the variant: Resolved walks its declarations under the parent's Scope, ToolOwned walks nothing (the tool owns the closure), T.absurd seals the case. No guard, no rescue: construction is the dispatch. The empty-forms doc paragraph is finally honest — 'declares nothing requires nothing' is now true because tool-owned closures can no longer hide inside the empty form. Co-authored-by: Cursor <cursoragent@cursor.com>
Each of the ten repositories now constructs the Declarations variant its regime warrants — construction is the dispatch, so no guard or enum exists: - ficsit: Resolved(normalized, integration-stamped declarations) - bundler, pip, luarocks, brew: ToolOwned — the ecosystem's tool resolves the closure. bundler's comment records that Gemfile.lock is read for pinned versions only, never mined for dependency declarations. - steam, git, xcode, url: Resolved([]) — self-contained by construction. - gh: Resolved([]) — prebuilt assets by guarantee; source builds as a usage contract (the consumer declares transitive needs) until subproject resolution lands. Co-authored-by: Cursor <cursoragent@cursor.com>
Inside Dev::Deps the word 'Dependency' is the module's subject; as a prefix it carries no information. Mechanical rename: file, class, requires, the install-deps command factory, and the rules-file mention. Docs get their full ontology rewrite in the next commit. Co-authored-by: Cursor <cursoragent@cursor.com>
The ontology table grows to five ideas (Declaration joins as the shared atom; ScopedDeclaration replaces DependencyDeclaration as the requirement) plus a compact aggregate diagram of intent/universe/pin. New sections: the constraint standard (dev-shaped hash minted at the repository seam, interpreted by the integration's VersionScheme — schemes widen, vocabularies never translate) and the transitive-dependency regimes table with the two standing decisions (lock files are never availability facts; Resolved([]) and ToolOwned are different claims). Sequence diagrams renamed to the new types, with the transitive queueing wrapped in an opt fragment; the ASCII pipeline block is gone (it repeated the diagrams with stale shapes). The new-ecosystem recipe now tells a repository to state its regime by construction and normalize constraints at find. Co-authored-by: Cursor <cursoragent@cursor.com>
This was referenced Sep 6, 2026
The 'stdlib-only pre-bundle chain' constraint was self-imposed: only bin/test.rb and bin/tc.rb loaded dependencies.rb before bundler/setup, and that early load served no purpose (EnsureBundler self-loads it, post- setup). Every real pathway — bin scripts, the dev CLI, docker's vendored keg gems — has sorbet-runtime available. - Drop the pre-bundle load of dependencies.rb from bin/test.rb, bin/tc.rb, and bin/rbi.rb; the chain now always loads with gems active. - typed: strict with full sigs: deps.rb, cli_ui.rb, config.rb, dsl.rb, declaration.rb, scope.rb, scoped_declaration.rb, tap.rb, lockfile.rb, installer.rb, ensure_bundler.rb. - ensure_bundler.rb becomes module EnsureBundler (top-level defs can't carry sigs); error nested as EnsureBundler::BundlerInstallError. - Tap's Data.define-synthesized readers get sigs via an RBI shim; the now-visible nilability of Tap#url fixed properly in brew_integration. - Sorbet/StrictSigil exclusions shrink from 13 files to 2 genuine holdouts (dependency.rb: Data.define kwargs-initialize, error 4010; fetcher.rb: consumer-repo Lockfile API). Co-authored-by: Cursor <cursoragent@cursor.com>
Both lines were restructured by the strict-sigil pass and had no test: register_tap's remote-URL branch and CliUI.available?'s memoized return. Co-authored-by: Cursor <cursoragent@cursor.com>
…on field source is identity-shaping and legitimately statable by both authors of the atom (project rows and repository-reported manifest edges — cargo-style git deps), so it lives on Declaration and will feed PackageId#source. Install instructions (install_dir, asset globs, build recipes) are consumer-side and non-inherited, so they live on ScopedDeclaration next to platform and post_install — never on the shared atom, where they would be a structurally vacuous field for every upstream edge. Both participate in value equality so disagreeing sources or install dirs stay loud conflicts. Co-authored-by: Cursor <cursoragent@cursor.com>
satisfies? now takes the whole PackageVersion: some ecosystems' constraints match version facts rather than the version string (a Steam branch, the git ref a SHA resolved from, a brew formula suffix). Range schemes read only version.version. The new #pin extracts the exact coordinate a constraint pins, for the Resolver to pass to Repository#find as the probe — the access path for universes that cannot enumerate. Extraction lives on the scheme because constraint keys are the scheme's vocabulary; the raw constraint hash itself will stop reaching repositories in the next commit. Co-authored-by: Cursor <cursoragent@cursor.com>
…s, resolver projection The filter hash smuggled three unrelated things through the repository seam: version coordinates (tag/commit/buildid), source coordinates (repo/url/tap/app), and install instructions (install_dir/assets/ platforms/target). Each now travels its own channel: - Repository#find(id, probe:) — the probe is a single typed version coordinate, extracted by the integration's scheme (VersionScheme#pin), and only non-enumerable universes get one (gh tags, git refs, brew suffixes, xcode versions, url labels). Enumerable universes (ficsit, steam branches, pip, luarocks, bundler) ignore it. - Source coordinates ride PackageId#source (from Declaration#source). - Install instructions ride ScopedDeclaration#materialization and are stamped onto the pin at Resolver#mint, which also projects the declared platform union / ficsit target against the chosen version's artifacts (projection moved out of FicsitRepository). Scheme cutover: PinnedScheme (satisfies-everything) is dead. Each pinned-style ecosystem now states its real constraint semantics: ExactScheme(key:) for gh/xcode/url, GitScheme for cmake refs, SteamScheme for branch+buildid selection over enumerated branch tips, BrewScheme for formula version suffixes. VersionScheme#satisfies? is fact-aware (takes the PackageVersion, not the bare string) so schemes can match against universe facts like branch or ref. Repository fallout: SteamRepository enumerates every branch tip via SteamCmd.resolve_branches; GhRepository always resolves the commit and records all release assets as facts (glob selection moved to GhIntegration at install, loud NoMatchingAssetsError); casks split into BrewCaskRepository under the :cask integration — a genuinely separate universe with no versions or bottle digests. DSL verbs sort kwargs into constraint/source/materialization per integration; SteamIntegration owns the dev-platform -> steamcmd platform mapping. Co-authored-by: Cursor <cursoragent@cursor.com>
ExactScheme/GitScheme/SteamScheme/BrewScheme each get their own constraint-semantics tests (match, miss, unconstrained, pin, sort), and SteamIntegration#steam_platform_for gets a mapping table test — the provisioning fixture bypasses it, so nothing else executes it. Co-authored-by: Cursor <cursoragent@cursor.com>
… identity stance The find contract section, resolution pipeline, sequence diagram, constraint-semantics table, and new-ecosystem recipe now describe the probe/source/materialization channels instead of the retired filter hash. Also records the standing stance that version is never identity: same-package-twice is a per-context-resolution problem, not a PackageId problem. Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
The full deps-domain redesign (plan: "Extract Bundler Locker / package universe"): kill
Repository#prepareand the untypedfetch(id)hash by separating the four concepts they conflate — identity, universe, requirement, pin — and moving constraint semantics into per-ecosystem domain services.Layering rule, now enforced by the interfaces: Repository states facts, VersionScheme evaluates predicates, Resolver chooses.
Package universe domain types (
typed: strict):PackageId(integration-keyed identity — fixes the cross-ecosystem name-collision latent in the bare-name resolved-set key),Package(the aggregate a Repository returns; no satisfies?/sort/best_match by design),PackageVersion(facts only, absence as empty forms),Artifact(dev-fetched bytes; digest is an enforcement input),Declarations(a version's declared-deps claim — see the regimes bullet).Naming glossary / object model (final shape of the intent side):
Declaration— the shared atom (name + integration + dev-shaped constraint hash;{}= unconstrained), stated by project rows and upstream manifests alike;Scope— the walk-inherited context (group/host/env) that rides parent → child as one unit and projects onto pins as host/env metadata;ScopedDeclaration— Declaration + Scope + per-rowplatform/post_install, replacingDependencyDeclaration(composition, deliberately not a subclass);Declarations— a sealed sum type,Resolved([Declaration…])|ToolOwned;Dependency— the concretized pin, unchanged;Installer— wasDependencyInstaller.DependencyEdgeis dead: version facts reuseDeclaration.Transitive regimes stated by construction: every repository builds the
Declarationsvariant its regime warrants — ficsitResolved(...)with normalized, integration-stamped declarations; bundler/pip/luarocks/brewToolOwned(the tool owns the closure); steam/git/xcode/url/ghResolved([])(self-contained by construction; gh's is a usage contract until subproject resolution). Constraints are normalized into dev's shape at the repository seam — upstream syntax crosses the boundary exactly once, andResolver#normalize_constraintis deleted. Standing decisions recorded in the docs: lock files are never availability facts (Gemfile.lock is read for pinned versions only),Resolved([])≠ToolOwned, and version is never identity (same-package-twice is a per-context-resolution problem, not aPackageIdproblem). Follow-ups filed under thetransitivitylabel: Resolver silently skips constraint checks on already-resolved transitive edges #147, Feature: prefer sub-lock pins as solver preferences (conservative resolution) #148, Cross-ecosystem constraint algebra: widen VersionScheme with intersect/compatible? #149, Define Scope propagation semantics for real transitive walks #151 (Scope propagation semantics for real transitive walks), Dev-manifest dependencies at a ref: read dependencies.rb from the pinned checkout #152 (dev-manifest dependencies at a ref). Promote source out of the constraint hash onto Declaration #150 is closed by this PR.VersionScheme domain services:
GemScheme,SemverScheme(ficsit),Pep440Scheme(pip subset),RockScheme(luarocks dotted+revision grammar — deliberately not semver), and real constraint semantics for every formerly "pinned" ecosystem:ExactScheme(key:)(gh/xcode/url — the constraint names one exact coordinate),GitScheme(cmake — commit matches the SHA, tag matches the version'sreffact),SteamScheme(branch selection over enumerated branch tips + optional exact buildid assertion that fails loudly when stale),BrewScheme(formula version suffixes matched against theversion_suffixfact).PinnedScheme(satisfies-everything) is dead. No repository evaluated constraints before this — ficsit silently ignored declared^ranges.satisfies?is fact-aware (takes thePackageVersion), andpin(constraint)answers "does this constraint name one exact coordinate?". Parse errors split by fault:InvalidConstraintError(user's declaration — propagates) vsInvalidVersionError(nonconforming universe version — skipped as non-candidate), rooted in sharedVersionSchemebases.Repository#find(PackageId, probe:) -> Packageacross all eleven repositories — thefilterhash is retired. It smuggled three unrelated things through the repository seam, and each now travels its own channel: version coordinates ride theprobe(a single typed coordinate, extracted byVersionScheme#pin, present only for non-enumerable universes — gh tags, git refs, brew suffixes, xcode versions, url labels; enumerable universes get none and report everything); source coordinates rideDeclaration#source→PackageId#source(closes Promote source out of the constraint hash onto Declaration #150 — repo/url/tap/app leave the constraint hash); install instructions rideScopedDeclaration#materialization(install_dir, asset globs, build recipes, ficsit targets, steam depot platform) and never reach a repository — the Resolver stamps them onto the pin at mint and projects declared platforms/targets against the chosen version's artifacts. Fallout:SteamRepositoryenumerates every branch tip in oneapp_info_print;GhRepositoryalways resolves the tag's commit and reports all release assets as facts (glob selection moved to install-timeGhIntegration, loudNoMatchingAssetsError); casks split intoBrewCaskRepositoryunder a:caskintegration (a genuinely separate universe — Homebrew publishes no versions or bottle digests for casks);FicsitRepositoryreports unconditional facts (per-target artifacts) with projection moved toResolver#mint. pip moved to the PyPI JSON API — no more artifact downloads at resolve.fetch/preparedeleted with all their dead helpers.BundlerLockerextraction: the whole-setbundle locksolve leavesBundlerRepository, which is now a pureGemfile.lockreader.Resolver rewrite: resolved set keyed by
PackageId, so the same name under two integrations resolves independently against each one's universe and transitive edges stay inside the declaring dep's integration; conflict rejection per (integration, name) now compares constraint + source + materialization (replaces silent first-wins; disagreeing install dirs are different asks), scheme-filtered selection with platform-union handling, pin minting fromPackageVersionfacts merged with the declaration's materialization plus artifact projection, and a transitive walk that cases on the version'sDeclarationsclaim (Resolvedwalks,ToolOwnedhas nothing to walk,T.absurdseals it) inheriting the parent'sScopeas one unit.Lockfile schema: entries nest by integration (
brew:→zlib:→ attrs), carrying the same (integration, name) identity to disk — no more name-keyed collisions at the serialization seam. The reader keeps a legacy flat-format shim; once every consumer repo's lockfiles are rewritten byupdate-deps, the shim and its test are deleted.BuildContainer's install_dir/build-context scans parse viaLockfileinstead of raw YAML, so format knowledge lives in one place.Registry
scheme/lockerslots + lock-then-resolve pipeline inupdate-deps; consistency tests gain anti-drift guards for*_scheme.rband*_locker.rbfiles.docs/deps-architecture.md: ontology (five ideas + aggregate diagram), layer table, integrity regimes (dev-enforced / tool-enforced / identity-as-integrity), the new transitive-regimes table, the constraint standard (shape + per-integration scheme as interpreter; schemes widen, vocabularies never translate), new-ecosystem recipe, and the solve-ownership decision gate (per-ecosystem hybrid leaning dev-owned; bundler stays tool-owned behind the Locker; revisit criteria recorded).Strict sigils across the whole chain: the "deps require chain must be stdlib-only pre-bundle" constraint was self-imposed —
bin/test.rb/bin/tc.rb/bin/rbi.rbloadeddependencies.rbbeforebundler/setupfor no live reason (EnsureBundlerself-loads it post-setup), and every other pathway (dev CLI, docker's vendored keg gems) already has sorbet-runtime. With the early loads dropped, the chain —deps.rb,config.rb,dsl.rb,lockfile.rb,installer.rb,cli_ui.rb,tap.rb,ensure_bundler.rb(nowmodule EnsureBundler), andDeclaration/Scope/ScopedDeclaration— istyped: strictwith full sigs; theSorbet/StrictSigilexclusion list shrinks from 13 files to 2 genuine Sorbet holdouts (dependency.rb: Data.define kwargs-initialize, error 4010;fetcher.rb: consumer-repo Lockfile API).Stacking / merge order
Sorbet/StrictSigilrules; merge Enforce Sorbet typed sigil #140 first and this retargets to main automatically.update_deps_command.rb(+ test): Deployment scheme: layered settings + the Brewfile host contract #133 renamescontext.project_root→context.project!.rooton lines the wiring step here touches. One trivial conflict; suggested order Enforce Sorbet typed sigil #140 → Deployment scheme: layered settings + the Brewfile host contract #133 → this.Verification
srb tcclean. RuboCop clean under Enforce Sorbet typed sigil #140's strict-sigil enforcement. Patch coverage: no PR-added line uncovered.find; behavior unique to the oldfetchpaths (gh auth/API errors, brew tap retry, ficsit link fallback) ported rather than dropped. Each new scheme gets dedicated constraint-semantics tests.Migration
Each consumer repo's
deps.lock/build-deps.lockrewrites wholesale (same data, nested shape) on its nextupdate-deps; build images re-tag once since lockfile content feeds the content-addressed tag. Old lockfiles keep installing via the read shim in the meantime. Sweep and shim removal tracked in #146.