Deps redesign: package universe repositories, Resolver-owned selection - #142
Merged
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>
An addressable revision (git SHA, exact Xcode version) is the other way to ask: it forgoes resolution instead of selecting over a published universe. The atom rejects revision+constraint at construction (RevisionWithConstraintError), ScopedDeclaration delegates it, and the Resolver's conflict rejection now treats disagreeing revisions as disagreeing asks. Co-authored-by: Cursor <cursoragent@cursor.com>
at(id, revision) lifts an address in an integration's continuous space into a PackageVersion — pure, no I/O; the address is trusted at resolve and verified at install. Overriding at IS the declaration that a continuous space exists; the base raises NoAddressableSpaceError and the Resolver lets the refusal propagate. A revision-pinned declaration dispatches to at, skipping find and the scheme entirely, and mints the pin from the lifted version (materialization merged as usual). Co-authored-by: Cursor <cursoragent@cursor.com>
GitRepository#find now lists every tag and branch head in one ls-remote call (peeled SHAs win for annotated tags) — the ref rides each SHA version as the fact GitScheme's tag:/branch: constraints select on. A commit pin is no longer a constraint: the cmake DSL verb sorts commit: into the declaration's revision (full 40-hex only, killing the silent resolve-short-commit-as-tag fallthrough) and GitRepository#at lifts it purely. Ref-less git deps are rejected at the DSL boundary: an unconstrained enumeration would pin an arbitrary ref. Co-authored-by: Cursor <cursoragent@cursor.com>
GhRepository#find now lists releases (assets and digests ride the list response) and tags (commit SHAs ditto) via two paginated endpoints — no per-version calls, no probe. ExactScheme's tag selection is unchanged; universe order puts releases last, newest at the end, so an unconstrained gh dep pins the latest release. MissingTagError and ReleaseNotFoundError die: a missing tag is now the Resolver's NoSatisfyingVersionError over an honest universe, and a 404 on a list endpoint can only mean the repo is invisible (RepoAccessError). Co-authored-by: Cursor <cursoragent@cursor.com>
BrewRepository#find now reports the whole family — the bare spec plus every versioned_formulae sibling, fetched in one batched brew info call. Each sibling's @suffix rides its version as the version_suffix fact BrewScheme's version: constraint matches; the bare spec sits last as the unconstrained pick. Head-only siblings without a stable version are skipped; tap formulae with empty families degrade to the old singleton. Co-authored-by: Cursor <cursoragent@cursor.com>
…nstraint Apple publishes no queryable registry, so there is no discrete universe: XcodeRepository#find now refuses (NoEnumerableUniverseError) and at lifts the declared version as the identity. The DSL's xcode verb mints the version as the declaration's revision (blank rejected loudly), and the Registry's scheme slot goes nilable — a purely addressable type answers 'how do constraints work' with 'they never occur here'. Co-authored-by: Cursor <cursoragent@cursor.com>
cmake url: declarations now route to a first-class :url registry entry instead of masquerading as git-backed cmake deps. UrlRepository#find downloads and hashes the artifact as an unversioned singleton (TOFU integrity); the author's tag: becomes a display label riding materialization, promoted into the pin's version slot at mint — never a constraint, because no scheme grammar exists to evaluate one. Scheme-less integrations get an explicit resolver posture: an empty ask resolves over the universe as reported; a version constraint raises UnknownIntegrationError instead of silently pretending to evaluate it. The :url entry aliases its installer to :cmake via install_alias, and Installer#dispatch now groups by integration instance rather than symbol, so cmake and url deps land in one install_all call and batch artifacts (deps.cmake) can't clobber each other. Co-authored-by: Cursor <cursoragent@cursor.com>
Every universe now enumerates (or is a singleton whose query is the observation), so the probe — the access path we threaded through find for universes that couldn't — has no remaining caller. Repository#find is find(id): identity in, universe out. VersionScheme#pin and its overrides die with it; constraints are now exclusively predicates the scheme evaluates over reported facts, never coordinates smuggled to the repository. Casks follow brew's own universe: versioned casks are distinct cask names (temurin@21), so the name is the whole coordinate and a version: constraint on a cask is unsatisfiable by construction — loudly, via BrewScheme finding no suffix fact to match. Co-authored-by: Cursor <cursoragent@cursor.com>
deps-architecture.md now teaches the split: find(id) enumerates the discrete published universe (identity in, universe out — the I/O operation, where a degenerate universe's query is the observation), at(id, revision) purely lifts an address into the continuous space between published versions, forgoing resolution by the author's own hand. The resolve sequence diagram branches on the two paths, the scheme table drops probe/pin vocabulary (constraints are predicates over enumerated facts, revisions are forwarded addresses), and the new-ecosystem recipe states that overriding at() is itself the declaration of a continuous space. Co-authored-by: Cursor <cursoragent@cursor.com>
Sorbet rejects splatting a runtime-sized array into capture3's typed signature (srb 7019); same escape hatch every other dynamic-argv capture3 call site in the codebase uses. Co-authored-by: Cursor <cursoragent@cursor.com>
JPDuchesne
commented
Sep 8, 2026
| # @raise [MissingUriError] if uri is missing or blank | ||
| sig { params(uri: T.nilable(String), digest: T.nilable(String)).void } | ||
| def initialize(uri:, digest: nil) | ||
| raise MissingUriError, "an artifact without a uri cannot be fetched" if uri.nil? || uri.empty? |
Contributor
Author
There was a problem hiding this comment.
if uri is required, let's make it non-nilable in the constructor
Contributor
Author
There was a problem hiding this comment.
Done in bdcc24b — uri: is String in the sig, so Sorbet rejects nil statically. Both producers already coerced at their real boundary (ficsit's download_url falls back to the canonical URL when the GraphQL link is absent; url's uri is the declaration's source), so the nilable param was only weakening the contract. The blank-string guard and MissingUriError stay: non-emptiness is the one invariant the type can't express.
…hecked Both producers already coerce at their real boundary (ficsit's download_url falls back to the canonical URL when the GraphQL link is absent; url's uri is the declaration's source), so the nilable parameter only weakened the contract for internal callers. Sorbet now rejects a nil uri statically; the blank-string guard stays, since non-emptiness is the one invariant the type can't express. Addresses review feedback on #142. 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 — the constraint names one release tag out of the enumerated universe),GitScheme(cmake — tag/branch match the version'sreffact; a commit is a revision, not a constraint — see the two-operation bullet),SteamScheme(branch selection over enumerated branch tips + optional exact buildid assertion that fails loudly when stale),BrewScheme(formula version suffixes selecting a sibling out of the enumerated spec family via theversion_suffixfact).PinnedScheme(satisfies-everything) is dead, and so isVersionScheme#pin. url and xcode register no scheme at all: no constraint grammar exists for them, only the empty constraint is legal, and anything else raises — never a silent pass. No repository evaluated constraints before this — ficsit silently ignored declared^ranges.satisfies?is fact-aware (takes thePackageVersion). Parse errors split by fault:InvalidConstraintError(user's declaration — propagates) vsInvalidVersionError(nonconforming universe version — skipped as non-candidate), rooted in sharedVersionSchemebases.Two-operation repository contract —
find(id)andat(id, revision)across all twelve repositories. Thefilterhash (and its interimprobe:successor) is fully retired; every ecosystem's version space splits into discrete (the published universe — enumerable, sofind(id)takes identity and nothing else, reports every version with facts, and constraints select over it) and continuous (the space between published versions — git commit SHAs, exact Xcode versions — never enumerated; a declaration addresses it withDeclaration#revisionandat(id, revision)purely lifts the address, no I/O ever, trusted at resolve and verified at install, the same pin-as-assertion semantics a steam buildid has). Overridingatis the declaration that a continuous space exists (NoAddressableSpaceErrorbase); declaring both a revision and a constraint is a loud DSL error;cmake commit:(strict 40-hex) andxcodeversions sort into revisions. What the old filter smuggled now travels its own channel: source coordinates rideDeclaration#source→PackageId#source(closes Promote source out of the constraint hash onto Declaration #150); install instructions rideScopedDeclaration#materializationand never reach a repository. Universes now genuinely enumerate:GhRepositoryjoins the releases list (assets+digests) with the tags list (commit SHAs), unconstrained = latest release,MissingTagErrordead;GitRepositoryenumeratesls-remotetags+heads (annotated tags peeled) withatas the pure SHA lift;BrewRepositoryenumerates the formula-spec family viaversioned_formulae+ one batchedbrew info;SteamRepositoryenumerates every branch tip in oneapp_info_print; casks stay a singleton whose name is the whole coordinate (versioned casks are distinct cask names, soversion:on a cask is unsatisfiable by construction);FicsitRepositoryreports unconditional facts with projection moved toResolver#mint; pip moved to the PyPI JSON API. gh SHA revisions deferred to deps: gh commit-SHA revisions (GhRepository#at) #153 (no consumer, no dead code).fetch/preparedeleted with all their dead helpers.url is a real integration:
cmake url:declarations route to a first-class:urlregistry entry instead of masquerading as git-backed cmake deps (previously they'd have hitGitRepositoryand failed).UrlRepository#finddownloads and hashes the artifact as an unversioned singleton — the query is the observation, which is exactly why url is afinduniverse and not anatone (keeps the at-is-pure invariant). The author'stag:is a display label riding materialization, promoted into the pin's version slot at mint — naming, never selection. The:urlentry installs through cmake's integration instance (newRegistry::Entry#install_alias,Installer#dispatchgroups by instance), sodeps.cmakeis written once, whole, when a project mixes both. deps: shared download utility — four hand-rolled curl call sites #154 filed for the four hand-rolled curl call sites (url/cmake/gh/steamcmd) — materialization-layer cleanup, separate PR.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; a revision ask dispatches straight toat()— no universe query, no scheme, no selection (the author foreran resolution, and with it diamond reconciliation, by definition); conflict rejection per (integration, name) now compares constraint + source + revision + 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, the discrete/continuous two-operation section (resolve sequence diagram branches on the constraint-vs-revision paths; revisions are opaque addresses validated at the DSL boundary, deliberately unstandardized because dev only forwards them), 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/at; 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.