File size: 2,715 Bytes
3ce1a40
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
using System.Text.Json;
using Repoverse.Core.Generation;
using Repoverse.Core.Model;

namespace Repoverse.Core.Ingestion;

public sealed record Snapshot
{
    public const int CurrentSchema = 1;
    public int SchemaVersion { get; init; } = CurrentSchema;
    public required DateTimeOffset TakenAt { get; init; }
    public required string Source { get; init; }
    public required IReadOnlyList<CanonicalRepository> Repositories { get; init; }
    public IReadOnlyList<IngestFailure> Failures { get; init; } = [];
}

/// <summary>
/// Last-known-good canonical state (§27). A refresh is merged per repository: repos that
/// failed this time keep their previous record and are reported stale, and an empty or
/// wholly failed refresh never replaces a populated snapshot.
/// </summary>
public static class SnapshotStore
{
    public static Snapshot? Load(string path) =>
        File.Exists(path) ? JsonSerializer.Deserialize<Snapshot>(File.ReadAllText(path), Json.Options) : null;

    public static void Save(string path, Snapshot snapshot)
    {
        Directory.CreateDirectory(Path.GetDirectoryName(Path.GetFullPath(path))!);
        var tmp = path + ".tmp";
        File.WriteAllText(tmp, JsonSerializer.Serialize(snapshot, Json.Options));
        File.Move(tmp, path, overwrite: true); // atomic replace: a crash never leaves half a snapshot
    }

    public sealed record MergeResult(Snapshot Snapshot, IReadOnlyList<string> Stale, bool KeptPrevious);

    public static MergeResult Merge(Snapshot? previous, IngestResult fresh, string source, DateTimeOffset now)
    {
        if (previous != null && fresh.Repositories.Count == 0)
            return new MergeResult(previous with { Failures = fresh.Failures }, previous.Repositories.Select(r => r.Id.Key).ToList(), true);

        var failed = fresh.Failures.Select(f => f.Repo.ToLowerInvariant()).ToHashSet();
        var byKey = fresh.Repositories.ToDictionary(r => r.Id.Key);
        var stale = new List<string>();
        if (previous != null)
            foreach (var old in previous.Repositories)
                // Keep old records for repos that failed or were not reached (e.g. rate limit mid-run).
                if (!byKey.ContainsKey(old.Id.Key) && (failed.Contains(old.Id.Key) || fresh.RateLimited))
                {
                    byKey[old.Id.Key] = old;
                    stale.Add(old.Id.Key);
                }

        var merged = new Snapshot
        {
            TakenAt = now,
            Source = source,
            Repositories = byKey.Values.OrderBy(r => r.Id.Key, StringComparer.Ordinal).ToList(),
            Failures = fresh.Failures,
        };
        return new MergeResult(merged, stale, false);
    }
}