feat: adapt roster sources into package resolution

This commit is contained in:
codex
2026-08-05 13:09:04 +03:00
parent cea9d70dc3
commit b83742b3a5
4 changed files with 195 additions and 0 deletions
@@ -0,0 +1,59 @@
using System.Xml;
using System.Xml.Linq;
using System.Text.Json;
using Launcher.Next.Core;
namespace Launcher.Next.Application;
public enum RosterSource { ProjectRosterJson, LegacyDownloadListXml, CentralDefaultRoster, None }
public sealed record RosterPackage(string Id, string Version, string Channel, bool Enabled, int LoadOrder, bool AllowedForMachine = true);
public sealed record RosterContext(string Product, int ProductYear, string Architecture, bool AllowOfflineFallback, string LegacyRosterId);
public sealed record NormalizedRoster(string RosterId, string Product, int Year, string Architecture, bool AllowOfflineFallback, IReadOnlyList<RosterPackage> Packages);
public sealed record RosterSelection(RosterSource Source, NormalizedRoster? Roster, string Code);
public sealed class RosterAdapter
{
public NormalizedRoster ParseJson(string json)
{
using var doc=JsonDocument.Parse(json); var r=doc.RootElement;
if (!r.TryGetProperty("schemaVersion",out var s)||s.GetInt32()!=1) throw new InvalidOperationException("ROSTER_SCHEMA_INVALID");
string V(string n)=>r.TryGetProperty(n,out var x)&&!string.IsNullOrWhiteSpace(x.GetString())?x.GetString()!:throw new InvalidOperationException("ROSTER_FIELD_REQUIRED:"+n);
var packages=r.GetProperty("packages").EnumerateArray().Select(x=>new RosterPackage(x.GetProperty("id").GetString()??throw new InvalidOperationException("ROSTER_PACKAGE_ID_REQUIRED"),x.GetProperty("version").GetString()??throw new InvalidOperationException("ROSTER_PACKAGE_VERSION_REQUIRED"),x.GetProperty("channel").GetString()??throw new InvalidOperationException("ROSTER_PACKAGE_CHANNEL_REQUIRED"),!x.TryGetProperty("enabled",out var e)||e.GetBoolean(),x.TryGetProperty("loadOrder",out var o)?o.GetInt32():0)).ToArray();
if(packages.GroupBy(x=>x.Id,StringComparer.OrdinalIgnoreCase).Any(x=>x.Count()>1)) throw new InvalidOperationException("ROSTER_DUPLICATE_PACKAGE");
return new(V("rosterId"),V("product"),r.GetProperty("year").GetInt32(),V("architecture"),r.TryGetProperty("offlinePolicy",out var p)&&p.GetString()=="AllowVerifiedLastKnownGood",packages.OrderBy(x=>x.LoadOrder).ThenBy(x=>x.Id,StringComparer.Ordinal).ToArray());
}
public NormalizedRoster ParseLegacyDownloadList(string xml, RosterContext context)
{
var settings=new XmlReaderSettings{DtdProcessing=DtdProcessing.Prohibit,XmlResolver=null,MaxCharactersInDocument=1_000_000}; using var reader=XmlReader.Create(new StringReader(xml),settings); var doc=XDocument.Load(reader);
var packages=doc.Descendants("Plugin").Select((x,i)=>new RosterPackage((string?)x.Attribute("id")??throw new InvalidOperationException("LEGACY_PACKAGE_ID_REQUIRED"),(string?)x.Attribute("version")??throw new InvalidOperationException("AMBIGUOUS_LEGACY_SEMANTICS"),(string?)x.Attribute("channel")??"pilot",!string.Equals((string?)x.Attribute("install"),"false",StringComparison.OrdinalIgnoreCase),i)).ToArray();
if(packages.GroupBy(x=>x.Id,StringComparer.OrdinalIgnoreCase).Any(x=>x.Count()>1)) throw new InvalidOperationException("ROSTER_DUPLICATE_PACKAGE");
return new(context.LegacyRosterId,context.Product,context.ProductYear,context.Architecture,context.AllowOfflineFallback,packages);
}
public RosterSelection Select(string? projectJson,string? legacyXml,string? centralJson,RosterContext context)
{ if(projectJson is not null)return new(RosterSource.ProjectRosterJson,ParseJson(projectJson),"PROJECT_ROSTER_JSON"); if(legacyXml is not null)return new(RosterSource.LegacyDownloadListXml,ParseLegacyDownloadList(legacyXml,context),"LEGACY_DOWNLOAD_LIST_XML"); if(centralJson is not null)return new(RosterSource.CentralDefaultRoster,ParseJson(centralJson),"CENTRAL_DEFAULT_ROSTER"); return new(RosterSource.None,null,"NO_ROSTER_AVAILABLE"); }
public IReadOnlyList<PackageRequirement> Requirements(NormalizedRoster roster)=>roster.Packages.Where(x=>x.Enabled&&x.AllowedForMachine).Select(x=>new PackageRequirement(x.Id,roster.Product,roster.Year,roster.Architecture,x.Channel,x.Version,roster.RosterId,roster.AllowOfflineFallback)).ToArray();
}
public sealed record PackagePlanResult(bool IsComplete,IReadOnlyList<PackageResolutionResult> Packages,IReadOnlyList<PackageResolutionEvidence> Evidence);
public sealed class RosterPackagePlanUseCase(RosterAdapter rosterAdapter,PackageResolutionUseCase packageResolution)
{
public async Task<PackagePlanResult> ResolveAsync(RosterSelection selection,CancellationToken cancellationToken=default)
{
var evidence=new List<PackageResolutionEvidence>();
if(selection.Roster is null){evidence.Add(new("PACKAGE_PLAN_BLOCKED","NO_ROSTER_AVAILABLE"));return new(false,Array.Empty<PackageResolutionResult>(),evidence);}
evidence.Add(new("ROSTER_SOURCE_SELECTED",$"{selection.Code}:{selection.Roster.RosterId}"));
evidence.Add(new("ROSTER_PARSED",selection.Roster.RosterId));
evidence.Add(new("ROSTER_VALIDATED",selection.Roster.RosterId));
var requirements=rosterAdapter.Requirements(selection.Roster);
if(requirements.Count==0){evidence.Add(new("PACKAGE_PLAN_BLOCKED","NO_ENABLED_PACKAGE_REQUIREMENTS"));return new(false,Array.Empty<PackageResolutionResult>(),evidence);}
var results=new List<PackageResolutionResult>();
foreach(var requirement in requirements)
{
evidence.Add(new("PACKAGE_REQUIREMENT_CREATED",$"{requirement.PackageId}:{requirement.RosterIdentity}"));
var result=await packageResolution.ResolveAsync(requirement,cancellationToken);
results.Add(result);evidence.AddRange(result.Evidence);
if(!result.IsResolved){evidence.Add(new("PACKAGE_PLAN_BLOCKED",$"PACKAGE_REQUIREMENT_REJECTED:{requirement.PackageId}"));return new(false,results,evidence);}
}
evidence.Add(new("PACKAGE_PLAN_COMPLETED",selection.Roster.RosterId));
return new(true,results,evidence);
}
}