Hacktoberfest 2026: los issues que los mantenedores marcaron para octubre, abiertos y aptos para principiantes. Explorar issues de Hacktoberfest

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

Abierto
#141 0 comentarios 0 reacciones 0 asignados Ver en GitHub

Los mantenedores suelen responder en 4 días

Nadie ha tomado este issue todavía.

Evaluación

Dificultad
5/5
Tiempo estimado
Más de una semana
Aptitud para principiantes
20/100
Tipo de issue
Nueva funcionalidad
Claridad
Bien especificado
Estado de actividad
Activo
Stack tecnológico
csharp
Área
backend

Línea de trabajo

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.

Escrito por el modelo de indexación a partir del texto del issue.

Descripción

[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.

Lenguaje dominante
C#
Estrellas
14
Forks
4
Merge medio
5 d 4 h
PR fusionados (30 d)
13

Preparar el entorno

Este proyecto no incluye contenedor de desarrollo, Dockerfile ni guía de contribución, así que la configuración corre por tu cuenta: empieza por su README y consulta nuestra guía para la primera contribución para los pasos generales.

Primeros pasos

  1. Lee el issue completo y luego la guía de contribución del proyecto.
  2. Comenta en el issue que vas a ocuparte — evita que dos personas hagan lo mismo.
  3. Haz un fork del repositorio y trabaja en una rama.
  4. Abre un pull request que haga referencia al número del issue.

Más de sillsdev/harmony

Todos los issues de sillsdev/harmony

Issues similares

Más issues de C#

Recibe los nuevos issues en tu correo

Un resumen breve de issues de GitHub para principiantes.