Refactor/version selectors#65
Conversation
There was a problem hiding this comment.
Pull request overview
This PR refactors Lucy’s “fuzzy” version selector vocabulary away from the previous latest/compatible model toward a new selector set (any/stable/beta) and updates multiple upstream providers and manifest defaults accordingly.
Changes:
- Replaces
latest/compatibleselectors withbeta/stable(and shifts defaults towardany) across version parsing, providers, and CLI completion hints. - Updates provider-specific selector resolution/fallback behavior (including recursive install resolution fallbacks).
- Adjusts tests and manifest normalization to reflect the new selector defaults.
Reviewed changes
Copilot reviewed 23 out of 23 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| version/version_parse.go | Updates ambiguous-selector handling during version parsing. |
| upstream/providers/spiget/spiget.go | Aligns Spiget selector resolution with new selector names. |
| upstream/providers/spiget/spiget_version.go | Updates Spiget version resolution selector set. |
| upstream/providers/spiget/spiget_conversion_test.go | Updates Spiget tests for selector identity expectations. |
| upstream/providers/neoforge/neoforge.go | Updates NeoForge selector handling to new constants. |
| upstream/providers/modrinth/modrinth.go | Updates Modrinth selector resolution behavior (new stable/beta/any mapping). |
| upstream/providers/modrinth/modrinth_version.go | Switches Modrinth “inferable” check to CanInfer(). |
| upstream/providers/modrinth/modrinth_project.go | Changes Modrinth dependency selector default from compatible→any. |
| upstream/providers/mcdr/mcdr.go | Updates MCDR provider selector mapping to new selector names. |
| upstream/providers/mcdr/mcdr_requests.go | Updates MCDR request logic to treat new selectors as “latest” where applicable. |
| upstream/providers/hangar/hangar_version.go | Updates Hangar selector mapping to new stable/beta/any names. |
| upstream/providers/forge/forge.go | Updates Forge selector handling to new constants. |
| upstream/providers/fabric/fabric.go | Updates Fabric loader selector recognition to new constants. |
| upstream/providers/curseforge/curseforge.go | Updates CurseForge selector mapping to new stable/beta/any names. |
| types/type_dependency.go | Replaces selector constants and documents the new selector set. |
| state/manifest.go | Changes manifest default selector from compatible→any in multiple normalization/default paths. |
| state/manifest_test.go | Updates manifest tests to expect the new defaults/selectors. |
| install/install.go | Removes special-casing that previously remapped VersionAny→VersionCompatible for identity installs. |
| install/install_recursive_provider_resolver.go | Updates fallback attempt chain for stable/beta/any selectors. |
| install/install_batch.go | Removes batch-time remapping of VersionAny→VersionCompatible. |
| cmd/cmd_completion_helpers.go | Updates CLI version completion candidates to any/stable/beta. |
| cmd/cmd_add.go | Removes add-time remapping of VersionAny→VersionCompatible. |
| cmd/cmd_add_test.go | Updates add tests for the new “any” default. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| switch p.Version { | ||
| case types.VersionCompatible: | ||
| case types.VersionStable: | ||
| v, err = latestCompatibleVersion(p.Name, p.Platform) | ||
| case types.VersionAny, types.VersionNone, types.VersionLatest: | ||
| case types.VersionBeta, types.VersionAny, types.VersionNone: | ||
| v, err = latestVersion(p.Name) |
| func NormalizeManifestVersionIntent(version types.BareVersion) string { | ||
| trimmed := strings.TrimSpace(version.String()) | ||
| switch trimmed { | ||
| case "", "any", "none", "unknown": | ||
| return types.VersionCompatible.String() | ||
| return types.VersionAny.String() | ||
| default: | ||
| return trimmed | ||
| } |
| // Version selectors — all three are compatible with the detected server by | ||
| // default. They differ only in stability preference: | ||
| // | ||
| // - VersionAny (default): latest version regardless of release type. | ||
| // - VersionStable: only stable/release versions, no beta fallback. | ||
| // - VersionBeta: include beta and pre-release versions. |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 23 out of 23 changed files in this pull request and generated 6 comments.
Comments suppressed due to low confidence (1)
upstream/providers/modrinth/modrinth_version.go:72
- getVersion() treats VersionStable as an inferable selector (via CanInfer) but resolves it using latestVersion(), which ignores the stable/compatibility semantics implemented in ResolveVersionSelector (stable -> latestCompatibleVersion). This can return a pre-release or loader-incompatible version when callers pass VersionStable to getVersion/Dependencies.
versions, err := listVersions(id.Name)
if err != nil {
return nil, err
}
if id.Version.CanInfer() {
v, err = latestVersion(id.Name)
if err != nil {
return nil, err
}
return v, nil
}
| version, err = latestCompatibleVersion( | ||
| input.ToProjectName(project.Slug), | ||
| dependent.Platform, | ||
| ) | ||
| if err != nil { | ||
| return p, fmt.Errorf("resolve dependency latest version: %w", err) | ||
| } | ||
| p.Name = input.ToProjectName(project.Slug) | ||
| p.Version = types.VersionCompatible | ||
| p.Version = types.VersionAny | ||
| return p, nil |
| // There are several special constants for ambiguous(adaptive) versions. | ||
| // You MUST call upstream.InferVersion() before parsing them to ResolvableVersion. | ||
| // | ||
| // Version selectors — all three are compatible with the detected server by | ||
| // default. They differ only in stability preference: |
| // - VersionAny (default): latest version regardless of release type. | ||
| // - VersionStable: only stable/release versions, no beta fallback. | ||
| // - VersionBeta: include beta and pre-release versions. |
| updated := UpdateManifestRolesForAdd( | ||
| manifest, []types.PackageRequest{ | ||
| { | ||
| FullPackageRef: types.FullPackageRef{ | ||
| PackageRef: types.PackageRef{ | ||
| Platform: types.PlatformFabric, Name: "new-root", | ||
| }, | ||
| Version: types.VersionLatest, | ||
| Scope: types.SourceModrinth, | ||
| Version: types.VersionAny, | ||
| Scope: types.SourceModrinth, | ||
| }, | ||
| }, |
| updated := UpdateManifestRolesForRemove( | ||
| manifest, | ||
| []types.FullPackageRef{ | ||
| { | ||
| PackageRef: types.PackageRef{ | ||
| Platform: types.PlatformFabric, | ||
| Name: "root-a", | ||
| }, | ||
| Version: types.VersionCompatible, | ||
| Scope: types.SourceAuto, | ||
| }, { | ||
| PackageRef: types.PackageRef{ | ||
| Platform: types.PlatformFabric, | ||
| Name: "manual-jar", | ||
| }, | ||
| Version: types.VersionCompatible, | ||
| Scope: types.SourceAuto, | ||
| Version: types.VersionAny, | ||
| Scope: types.SourceAuto, | ||
| }, { | ||
| PackageRef: types.PackageRef{ | ||
| Platform: types.PlatformFabric, | ||
| Name: "manual-jar", | ||
| }, | ||
| Version: types.VersionAny, | ||
| Scope: types.SourceAuto, | ||
| }, | ||
| }, |
| func NormalizeManifestVersionIntent(version types.BareVersion) string { | ||
| trimmed := strings.TrimSpace(version.String()) | ||
| switch trimmed { | ||
| case "", "any", "none", "unknown": | ||
| return types.VersionCompatible.String() | ||
| return types.VersionAny.String() |
533ed1e to
fe8db9b
Compare
fe8db9b to
da54d55
Compare
da54d55 to
af0ea1b
Compare
| case types.VersionCompatible: | ||
| ws := workspace.New() | ||
| case types.VersionStable: | ||
| serverInfo := workspace.ServerInfo() |
Replace @latest/@compatible with @any/@stable/@beta selector constants. Keep compatibility as the baseline for all fuzzy selectors and encode release stability as the only selector difference.
Preserve VersionAny through install entry points and replace the old @compatible -> @latest -> @any retry chain with @stable -> @beta -> @any.
af0ea1b to
80bfce0
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 28 out of 28 changed files in this pull request and generated 3 comments.
Comments suppressed due to low confidence (1)
state/manifest.go:287
validateManifestEcosystemrejects empty values up-front, so thetypes.EcoUnspecifiedcase can never match. At the same time, the docs/tests now refer to manifest platformnone, which is currently rejected here. Consider explicitly acceptingnone(and drop the unreachableEcoUnspecifiedcase).
// validateManifestEcosystem remains as a legacy helper for the pre-Task-2 lock
// schema, which still validates a single platform field.
func validateManifestEcosystem(value string) error {
platform := types.Ecosystem(strings.TrimSpace(value))
if platform == "" {
return fmt.Errorf("environment.platform is required")
}
switch platform {
case types.EcoFabric, types.EcoNeoforge, types.EcoForge, types.EcoMcdr, types.EcoUnspecified:
return nil
default:
return fmt.Errorf("invalid environment.platform %q", value)
}
| ### Runtime Topology | ||
|
|
||
| Lucy builds a graph of your server's runtime. Each node (Fabric, Forge, Paper, MCDR, Geyser, Velocity) carries a role, a set of capabilities (`fabric_mods`, `bukkit_plugins`, `mcdr_plugins`), and a risk level. Edges describe how nodes relate: one adapts another, one bridges to another. This graph powers `lucy status`, init discovery, and compatibility resolution. |
| ### 运行时拓扑 | ||
|
|
||
| Lucy 为你的服务器构建一张运行时图。图中的每个节点(Fabric、Forge、Paper、MCDR、Geyser、Velocity)都有角色、能力(`fabric_mods`、`bukkit_plugins`、`mcdr_plugins`)和风险等级。边描述节点之间的关系:谁适配谁、谁桥接谁、谁代理谁。这张图是 `lucy status`、init 探测和兼容性解析的基础。 |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 28 out of 28 changed files in this pull request and generated 5 comments.
Comments suppressed due to low confidence (1)
upstream/providers/modrinth/modrinth_version.go:70
getVersiontreatsVersionStablethe same as other inferable selectors viaCanInfer()and callslatestVersion, which ignores loader compatibility and stability semantics. This can select a pre-release or an incompatible loader version when the caller requested@stable.
if id.Version.CanInfer() {
v, err = latestVersion(id.Name)
if err != nil {
return nil, err
}
| case types.VersionStable: | ||
| serverInfo := workspace.ServerInfo() | ||
| rel, err = getLatestCompatibleRelease( | ||
| id.Name.Pep8String(), | ||
| ws.Environments.Mcdr.Version, | ||
| ) |
| // Version selectors — all three are compatible with the detected server by | ||
| // default. They differ only in stability preference: | ||
| // | ||
| // - VersionAny (default): latest version regardless of release type. | ||
| // - VersionStable: only stable/release versions, no beta fallback. | ||
| // - VersionBeta: include beta and pre-release versions. |
| // When a stability-preferring selector fails, fall back to VersionAny | ||
| // as a last resort (no stability filtering). | ||
| if id.Version == types.VersionStable { |
| platform name version | ||
| ``` | ||
|
|
||
| `@any` is the default and selects the latest compatible version regardless of release type. `@stable` selects the latest compatible release only. `@beta` allows compatible pre-releases. |
| 平台 名称 版本 | ||
| ``` | ||
|
|
||
| `@any` 是默认值,选择任意稳定性的最新兼容版本。`@stable` 只选择最新兼容正式版。`@beta` 允许兼容预发布版本。 |
80bfce0 to
a71931a
Compare
Closes #60