Skip to content

Deps redesign: package universe repositories, Resolver-owned selection - #142

Open
JPDuchesne wants to merge 38 commits into
mainfrom
deps/package-universe
Open

Deps redesign: package universe repositories, Resolver-owned selection#142
JPDuchesne wants to merge 38 commits into
mainfrom
deps/package-universe

Conversation

@JPDuchesne

@JPDuchesne JPDuchesne commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Summary

The full deps-domain redesign (plan: "Extract Bundler Locker / package universe"): kill Repository#prepare and the untyped fetch(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-row platform/post_install, replacing DependencyDeclaration (composition, deliberately not a subclass); Declarations — a sealed sum type, Resolved([Declaration…]) | ToolOwned; Dependency — the concretized pin, unchanged; Installer — was DependencyInstaller. DependencyEdge is dead: version facts reuse Declaration.

  • Transitive regimes stated by construction: every repository builds the Declarations variant its regime warrants — ficsit Resolved(...) with normalized, integration-stamped declarations; bundler/pip/luarocks/brew ToolOwned (the tool owns the closure); steam/git/xcode/url/gh Resolved([]) (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, and Resolver#normalize_constraint is 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 a PackageId problem). Follow-ups filed under the transitivity label: 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's ref fact), SteamScheme (branch selection over enumerated branch tips + optional exact buildid assertion that fails loudly when stale), BrewScheme (formula version suffixes matched against the version_suffix fact). PinnedScheme (satisfies-everything) is dead. No repository evaluated constraints before this — ficsit silently ignored declared ^ ranges. satisfies? is fact-aware (takes the PackageVersion), and pin(constraint) answers "does this constraint name one exact coordinate?". Parse errors split by fault: InvalidConstraintError (user's declaration — propagates) vs InvalidVersionError (nonconforming universe version — skipped as non-candidate), rooted in shared VersionScheme bases.

  • Repository#find(PackageId, probe:) -> Package across all eleven repositories — the filter hash is retired. It smuggled three unrelated things through the repository seam, and each now travels its own channel: version coordinates ride the probe (a single typed coordinate, extracted by VersionScheme#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 ride Declaration#sourcePackageId#source (closes Promote source out of the constraint hash onto Declaration #150 — repo/url/tap/app leave the constraint hash); install instructions ride ScopedDeclaration#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: SteamRepository enumerates every branch tip in one app_info_print; GhRepository always resolves the tag's commit and reports all release assets as facts (glob selection moved to install-time GhIntegration, loud NoMatchingAssetsError); casks split into BrewCaskRepository under a :cask integration (a genuinely separate universe — Homebrew publishes no versions or bottle digests for casks); FicsitRepository reports unconditional facts (per-target artifacts) with projection moved to Resolver#mint. pip moved to the PyPI JSON API — no more artifact downloads at resolve. fetch/prepare deleted with all their dead helpers.

  • BundlerLocker extraction: the whole-set bundle lock solve leaves BundlerRepository, which is now a pure Gemfile.lock reader.

  • 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 from PackageVersion facts merged with the declaration's materialization plus artifact projection, and a transitive walk that cases on the version's Declarations claim (Resolved walks, ToolOwned has nothing to walk, T.absurd seals it) inheriting the parent's Scope as 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 by update-deps, the shim and its test are deleted. BuildContainer's install_dir/build-context scans parse via Lockfile instead of raw YAML, so format knowledge lives in one place.

  • Registry scheme/locker slots + lock-then-resolve pipeline in update-deps; consistency tests gain anti-drift guards for *_scheme.rb and *_locker.rb files.

  • 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.rb loaded dependencies.rb before bundler/setup for no live reason (EnsureBundler self-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 (now module EnsureBundler), and Declaration/Scope/ScopedDeclaration — is typed: strict with full sigs; the Sorbet/StrictSigil exclusion 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

Verification

  • Full suite: 1187 tests, 0 failures. srb tc clean. RuboCop clean under Enforce Sorbet typed sigil #140's strict-sigil enforcement. Patch coverage: no PR-added line uncovered.
  • Repository tests rewritten against find; behavior unique to the old fetch paths (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.lock rewrites wholesale (same data, nested shape) on its next update-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.

JPDuchesne and others added 3 commits September 2, 2026 14:20
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

codecov Bot commented Sep 2, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

JPDuchesne and others added 12 commits September 2, 2026 17:50
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>
Base automatically changed from ai/139-enforce-sorbet-typed-sigil to main September 5, 2026 16:07
JPDuchesne and others added 6 commits September 5, 2026 13:20
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
JPDuchesne marked this pull request as ready for review September 5, 2026 19:17
JPDuchesne and others added 3 commits September 5, 2026 15:43
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>
JPDuchesne and others added 5 commits September 6, 2026 11:02
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>
JPDuchesne and others added 8 commits September 6, 2026 11:45
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Promote source out of the constraint hash onto Declaration

1 participant