Hacktoberfest 2026: những issue maintainer đã đánh dấu cho tháng Mười, đang mở và phù hợp người mới. Xem issue Hacktoberfest

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

Đang mở
#141 0 bình luận 0 reaction 0 người được giao Xem trên GitHub

Maintainer thường phản hồi trong vòng 4 ngày

Chưa có ai nhận issue này.

Đánh giá

Độ khó
5/5
Thời gian dự kiến
Hơn một tuần
Mức phù hợp với người mới
20/100
Loại issue
Tính năng
Độ rõ ràng
Đặc tả rõ ràng
Mức độ hoạt động
Sôi nổi
Công nghệ
csharp
Lĩnh vực
backend

Hướng nghiên cứu

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.

Do mô hình lập chỉ mục viết ra từ nội dung của issue.

Mô tả

[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 list them again when a Harmony release adds a change type or changes how one 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 added or changed, and optionally an earlier HarmonyVersion to invalidate from, exactly like a consumer's Change<T>(invalidateFrom:).
  • RemoteResourceChanges<TMetadata>(version) lists what was added or 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: a change is stored with its authoring app's latest model version (AuthoredModelVersion), and options validation makes an app list the Harmony version it runs, so changes authored at that model version or later ran that Harmony version or a later one, and earlier ones didn't. 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 on the #135 branch, updated to main after #135 merged.

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, closed over TMetadata:
    /// every type added or changed by a version after it, invalidated from the earliest version any of them names.
    internal IEnumerable<(Type Type, string Discriminator, HarmonyVersion? InvalidateFrom)> ChangesSince<TMetadata>(HarmonyVersion? previous)
    {
        return VersionsSince(previous)
            .SelectMany(v => v.Changes)
            .GroupBy(c => c.OpenType)
            .Select(g =>
            {
                var type = g.Key.MakeGenericType(typeof(TMetadata));
                var discriminator = (string)type.GetProperty(nameof(IPolyType.TypeName))!.GetValue(null)!;
                return (type, discriminator, 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;
        }
    }

    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: construct with new ModelVersionChanges(this, number), and after configure:
if (changes.HarmonyVersion is not null) _harmonyVersions.Add((changes.HarmonyVersion, number));

// in Clear:
_harmonyVersions.Clear();

This replaces _remoteResourcesListed and RemoteResourcesListed.

ModelVersionChanges takes the builder in its constructor (_builder), and its RemoteResourcesListed becomes internal HarmonyVersion? HarmonyVersion { get; private set; }. The helper replaces the parameterless one:

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, replacing the RemoteResourcesListed check:

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 add a resource change type or change how one applies before this lands. Apps would have to list those types by hand, which _harmonyVersions doesn't record, so a later Harmony invalidateFrom would map to the model version that first called RemoteResourceChanges with the new version, missing changes authored between the hand listing and that call. Adding the new type to the plain RemoteResourceChanges<T>() instead would change every app's released version 0, which reconcile rejects.

Ngôn ngữ chính
C#
Star
14
Fork
4
Merge trung bình
5 ngày 4 giờ
Pull request đã merge (30 ngày)
13

Chuẩn bị môi trường

Dự án này không cung cấp dev container, Dockerfile hay hướng dẫn đóng góp, nên bạn cần tự thiết lập môi trường: hãy bắt đầu từ README và xem hướng dẫn đóng góp lần đầu của chúng tôi để biết các bước chung.

Bắt đầu từ đâu

  1. Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
  2. Bình luận trên issue rằng bạn sẽ nhận — tránh hai người làm cùng một việc.
  3. Fork repository và làm thay đổi trên một nhánh.
  4. Mở pull request có tham chiếu số hiệu của issue.

Issue khác của sillsdev/harmony

Tất cả issue của sillsdev/harmony

Issue tương tự

Thêm issue về C#

Nhận issue mới trong hộp thư của bạn

Bản tóm tắt ngắn những issue GitHub phù hợp với người mới.