Hacktoberfest 2026: le issue che i maintainer hanno segnato per ottobre, aperte e adatte ai principianti. Sfoglia le issue Hacktoberfest

[claude] Version Harmony's own change types (HarmonyVersion design)

Aperta
#141 0 commenti 0 reazioni 0 assegnatari Vedi su GitHub

I maintainer di solito rispondono entro 4 giorni

Nessuno ha ancora preso questa issue.

Valutazione

Difficoltà
5/5
Tempo stimato
Più di una settimana
Idoneità per principianti
20/100
Tipo di issue
Funzionalità
Chiarezza
Specificata chiaramente
Stato di attività
Attiva
Stack tecnologico
csharp
Ambito
backend

Direzione di ricerca

The proposal names HarmonyVersion.cs, ModelVersionBuilder, ModelVersionChanges, and ModelVersionsValidator, with synthetic history tests described in the issue. Start by reading the existing model version and remote resource change implementations, then compare them with the proposed design. Done means Harmony-owned change history is integrated, skipped versions and invalidation are handled, and validation guides apps to the current HarmonyVersion.

Scritto dal modello di indicizzazione a partire dal testo della issue.

Descrizione

[Claude, autonomous]

Harmony registers its own change types (the five remote resource changes from AddCrdtRemoteResources). Model versions (#135) make apps list them in version 0 with RemoteResourceChanges<TMetadata>(), but nothing makes an app bump them when a Harmony release changes how one of them applies. Until that happens there is nothing to version, so this is parked. The design below was built and tested on the #135 branch and then dropped as machinery for a one-entry history; it is here so it doesn't have to be reinvented.

Design

Harmony keeps its own append-only version history for its types, the same shape as an app's ModelVersionBuilder. Apps name the version they are on; the change version numbers come from the app's own model versions, so a consumer never types a number for a type Harmony owns.

// consumer
.Add(0, "Before model versions", v => v
    .Change<NewWordChange>()
    .RemoteResourceChanges<MediaMetadata>(HarmonyVersion.V1))
.Add(3, "Harmony 2.4", v => v.RemoteResourceChanges<MediaMetadata>(HarmonyVersion.V2))
  • HarmonyVersion is a sealed class with one static instance per version, each pointing at its predecessor. A version lists the open generic change types it changed, and optionally an earlier HarmonyVersion to invalidate from, exactly like a consumer's Change<T>(invalidateFrom:).
  • RemoteResourceChanges<TMetadata>(version) lists what changed since the Harmony version the app last listed (everything, for a new app), so skipped versions fold in. Where a skipped version names an invalidateFrom, the earliest one wins.
  • A Harmony invalidateFrom maps to the app's model version that listed the first Harmony version at or after it. That is exact: options validation means an app can't have authored changes under a Harmony version it never listed. An app that never listed one (a new app) invalidates nothing.
  • Options validation fails when remote resources are enabled and the latest listed Harmony version isn't HarmonyVersion.Current, naming the call to add: Add a model version that calls RemoteResourceChanges<MediaMetadata>(HarmonyVersion.V2). Listing an older version after a newer one throws in the builder.
  • Discovery is the startup error, not autocomplete: IntelliSense sorts members alphabetically, so a V10 would sort before V2 either way.

Code as last built

HarmonyVersion.cs:

public sealed class HarmonyVersion
{
    public static readonly HarmonyVersion V1 = new(nameof(V1), null, v => v
        .Change(typeof(RemoteResourceUploadedChange<>))
        .Change(typeof(CreateRemoteResourceChange<>))
        .Change(typeof(CreateRemoteResourcePendingUploadChange<>))
        .Change(typeof(SetRemoteResourceMetadataChange<>))
        .Change(typeof(DeleteRemoteResourceChange<>)));

    internal static HarmonyVersion Current => V1;

    private HarmonyVersion(string name, HarmonyVersion? previous, Action<HarmonyVersionChanges> configure)
    {
        Name = name;
        Previous = previous;
        Number = (previous?.Number ?? 0) + 1;
        var changes = new HarmonyVersionChanges(this);
        configure(changes);
        Changes = changes.List;
    }

    internal HarmonyVersion Next(string name, Action<HarmonyVersionChanges> configure)
    {
        return new HarmonyVersion(name, this, configure);
    }

    internal int Number { get; }
    internal string Name { get; }
    private HarmonyVersion? Previous { get; }
    private IReadOnlyList<(Type OpenType, HarmonyVersion? InvalidateFrom)> Changes { get; }

    /// What an app that last listed previous (null for none) must list to be on this version:
    /// every type changed by a version after it, invalidated from the earliest version any of them names.
    private IEnumerable<(Type OpenType, HarmonyVersion? InvalidateFrom)> ChangesSince(HarmonyVersion? previous)
    {
        return VersionsSince(previous)
            .SelectMany(v => v.Changes)
            .GroupBy(c => c.OpenType)
            .Select(g => (g.Key, g.Select(c => c.InvalidateFrom).Where(v => v is not null).MinBy(v => v!.Number)));
    }

    private IEnumerable<HarmonyVersion> VersionsSince(HarmonyVersion? previous)
    {
        for (var version = this; version is not null && version != previous; version = version.Previous)
        {
            yield return version;
        }
    }

    /// ChangesSince with the change types closed over TMetadata
    internal IEnumerable<(Type Type, string Discriminator, HarmonyVersion? InvalidateFrom)> ChangesSince<TMetadata>(HarmonyVersion? previous)
    {
        foreach (var (openType, invalidateFrom) in ChangesSince(previous))
        {
            var type = openType.MakeGenericType(typeof(TMetadata));
            var discriminator = (string)type.GetProperty(nameof(IPolyType.TypeName))!.GetValue(null)!;
            yield return (type, discriminator, invalidateFrom);
        }
    }

    public override string ToString() => $"{nameof(HarmonyVersion)}.{Name}";

    internal sealed class HarmonyVersionChanges(HarmonyVersion version)
    {
        internal List<(Type OpenType, HarmonyVersion? InvalidateFrom)> List { get; } = [];

        public HarmonyVersionChanges Change(Type openType, HarmonyVersion? invalidateFrom = null)
        {
            if (invalidateFrom is not null && invalidateFrom.Number >= version.Number)
                throw new ArgumentOutOfRangeException(nameof(invalidateFrom), invalidateFrom, $"must be before {version}");
            List.Add((openType, invalidateFrom));
            return this;
        }
    }
}

ModelVersionBuilder additions:

private readonly List<(HarmonyVersion Version, int ModelVersion)> _harmonyVersions = [];

internal HarmonyVersion? HarmonyVersion => _harmonyVersions.Count == 0 ? null : _harmonyVersions[^1].Version;

/// The model version from which this app authored changes with harmonyVersion or a later one:
/// the one that listed the first such version. Null when none did.
internal int? ModelVersionListing(HarmonyVersion harmonyVersion)
{
    return _harmonyVersions.Where(h => h.Version.Number >= harmonyVersion.Number)
        .Select(h => (int?)h.ModelVersion)
        .FirstOrDefault();
}

// in Add, after configure:
if (changes.HarmonyVersion is not null) _harmonyVersions.Add((changes.HarmonyVersion, number));

ModelVersionChanges helper:

public ModelVersionChanges RemoteResourceChanges<TMetadata>(HarmonyVersion version) where TMetadata : class
{
    ArgumentNullException.ThrowIfNull(version);
    var listed = _builder.HarmonyVersion;
    if (listed is not null && listed.Number >= version.Number)
        throw new ArgumentException($"{version} must come after {listed}, which an earlier model version lists");
    foreach (var change in version.ChangesSince<TMetadata>(listed))
    {
        //a new app, or one that never listed that version, has no changes to invalidate
        var invalidateFrom = change.InvalidateFrom is null ? null : _builder.ModelVersionListing(change.InvalidateFrom);
        Change(change.Discriminator, invalidateFrom);
        Types.Add(change.Type);
    }
    HarmonyVersion = version;
    return this;
}

ModelVersionsValidator addition:

if (options.RemoteResourcesEnabled && options.ModelVersionBuilder.HarmonyVersion != HarmonyVersion.Current)
    return ValidateOptionsResult.Fail(
        $"Add a model version that calls {nameof(ModelVersionChanges.RemoteResourceChanges)}<{options.RemoteResourceMetadataType!.Name}>({HarmonyVersion.Current}), " +
        "Harmony's remote resource change types changed");

Tests, against a synthetic history (V2 changes deletes and invalidates from V1, V3 changes uploads and deletes):

var v2 = HarmonyVersion.V1.Next("V2", v => v.Change(typeof(DeleteRemoteResourceChange<>), invalidateFrom: HarmonyVersion.V1));
var v3 = v2.Next("V3", v => v.Change(typeof(RemoteResourceUploadedChange<>)).Change(typeof(DeleteRemoteResourceChange<>)));

// ChangesSince: v3 since v1 lists uploads (no invalidation) and deletes invalidated from v1, so skipping V2 still redoes old deletes
// RemoteResourceChanges: an app with V0 listing v1, V1 notes, V2 listing v3 gets
//   version 2: delete:RemoteResource invalidateFrom 0, uploaded:RemoteResource invalidateFrom 2
// a new app listing v3 in version 0 lists all five with invalidateFrom 0
// listing v2 after v3 throws "HarmonyVersion.V2 must come after HarmonyVersion.V3"
// the sample with the five types listed by hand fails validation naming RemoteResourceChanges<MediaMetadata>(HarmonyVersion.V1)

Compatibility of adding it later

HarmonyVersion.V1 lists the same five types the plain helper lists today, at the same numbers, so switching version 0 from RemoteResourceChanges<T>() to RemoteResourceChanges<T>(HarmonyVersion.V1) stores identical applied versions and the "released version was modified" check passes. The one constraint: Harmony must not change how a resource change type applies before this lands, or apps will have bumped those types by hand and the history here would have to match their numbers.

Lingua principale
C#
Stelle
14
Fork
4
Merge medio
5g 4h
PR unite (30g)
13

Preparare l'ambiente

Questo progetto non fornisce container di sviluppo, Dockerfile né guida per i contributori, quindi l'ambiente è a tuo carico: parti dal suo README e consulta la nostra guida al primo contributo per i passaggi generali.

Come iniziare

  1. Leggi tutta la issue e poi la guida ai contributi del progetto.
  2. Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
  3. Fai un fork del repository e lavora su un branch.
  4. Apri una pull request che faccia riferimento al numero della issue.

Altre issue di sillsdev/harmony

Tutte le issue di sillsdev/harmony

Issue simili

Altre issue su C#

Ricevi le nuove issue nella tua casella

Un breve riepilogo di issue GitHub adatte ai principianti.