File size: 9,842 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
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
using Repoverse.Core.Model;

namespace Repoverse.Core.Generation;

public enum RoomPurpose { Lobby, Corridor, Stairwell, Workshop, InspectionLab, Archive, ProofVault, Assembly, Storage, Toolroom, Commons }

/// <summary>Software concept a world object stands for (§53).</summary>
public enum ArtifactKind { SourceTerminal, TestBench, ProofVault, ArchiveShelf, BuildStation, DataRack, ConfigPanel, AssetCrate, NotebookDesk, HardwareBench }

public sealed record Artifact(string Path, ArtifactKind Kind, long SizeBytes, string Language);

public sealed record Room
{
    public required int Id { get; init; }
    public required string Name { get; init; }
    public required RoomPurpose Purpose { get; init; }
    public required int Floor { get; init; }
    /// <summary>Repository directories this room represents; several when small dirs were merged.</summary>
    public IReadOnlyList<string> Directories { get; init; } = [];
    public int FileCount { get; init; }
    public IReadOnlyList<Artifact> Artifacts { get; init; } = [];
}

public sealed record Door(int A, int B);

/// <summary>Logical interior (§88): rooms and their connections, before any geometry exists.</summary>
public sealed record RoomGraph(IReadOnlyList<Room> Rooms, IReadOnlyList<Door> Doors)
{
    public int EntranceId => Rooms.First(r => r.Purpose == RoomPurpose.Lobby).Id;

    /// <summary>Rooms not reachable from the entrance. Empty for a valid graph (§89).</summary>
    public IReadOnlyList<Room> Unreachable()
    {
        var adj = Rooms.ToDictionary(r => r.Id, _ => new List<int>());
        foreach (var d in Doors) { adj[d.A].Add(d.B); adj[d.B].Add(d.A); }
        var seen = new HashSet<int> { EntranceId };
        var queue = new Queue<int>(seen);
        while (queue.Count > 0)
            foreach (var n in adj[queue.Dequeue()])
                if (seen.Add(n)) queue.Enqueue(n);
        return Rooms.Where(r => !seen.Contains(r.Id)).ToList();
    }
}

public static class RoomGraphBuilder
{
    public static bool IsIgnored(string path)
    {
        foreach (var segment in path.Split('/'))
            if (WorldConstants.IgnoredDirectories.Contains(segment, StringComparer.OrdinalIgnoreCase))
                return true;
        return false;
    }

    public static RoomPurpose PurposeFor(string directory)
    {
        var d = directory.ToLowerInvariant();
        var leaf = d[(d.LastIndexOf('/') + 1)..];
        return leaf switch
        {
            "test" or "tests" or "spec" or "specs" or "__tests__" or "testing" => RoomPurpose.InspectionLab,
            "doc" or "docs" or "documentation" or "papers" or "notes" => RoomPurpose.Archive,
            "proof" or "proofs" or "formal" or "theories" or "verification" => RoomPurpose.ProofVault,
            ".github" or "ci" or ".circleci" or "workflows" or "deploy" => RoomPurpose.Assembly,
            "assets" or "data" or "fixtures" or "demo" or "screenshots" or "resources" => RoomPurpose.Storage,
            "scripts" or "tools" or "bin" or "cmd" => RoomPurpose.Toolroom,
            _ => RoomPurpose.Workshop,
        };
    }

    public static ArtifactKind KindFor(string path)
    {
        var p = path.ToLowerInvariant();
        var name = p[(p.LastIndexOf('/') + 1)..];
        if (name.Contains("test") || name.Contains("spec") || p.Split('/')[..^1].Any(d => d is "test" or "tests" or "spec" or "specs" or "__tests__"))
            return ArtifactKind.TestBench;
        if (name is "makefile" or "cargo.toml" or "package.json" or "cmakelists.txt" or "dockerfile" or "project.toml"
            || name.EndsWith(".csproj") || name.EndsWith(".cabal") || name.EndsWith(".sln") || p.Contains(".github/workflows/"))
            return ArtifactKind.BuildStation;
        var ext = Path.GetExtension(name);
        return ext switch
        {
            ".lean" or ".v" or ".agda" or ".idr" or ".thy" or ".tla" => ArtifactKind.ProofVault,
            ".md" or ".rst" or ".txt" or ".pdf" or ".tex" => ArtifactKind.ArchiveShelf,
            ".json" or ".csv" or ".parquet" or ".sqlite" or ".db" or ".vox" => ArtifactKind.DataRack,
            ".toml" or ".yaml" or ".yml" or ".ini" or ".cfg" or ".lock" => ArtifactKind.ConfigPanel,
            ".png" or ".jpg" or ".svg" or ".zip" or ".wav" or ".glb" => ArtifactKind.AssetCrate,
            ".ipynb" => ArtifactKind.NotebookDesk,
            ".sv" or ".vhd" or ".vhdl" or ".metal" or ".asm" or ".s" => ArtifactKind.HardwareBench,
            _ => ArtifactKind.SourceTerminal,
        };
    }

    public static string LanguageFor(string path) => Path.GetExtension(path).ToLowerInvariant() switch
    {
        ".cs" => "C#", ".rs" => "Rust", ".py" => "Python", ".hs" => "Haskell", ".jl" => "Julia",
        ".js" or ".mjs" or ".cjs" => "JavaScript", ".ts" or ".tsx" => "TypeScript", ".swift" => "Swift",
        ".c" or ".h" => "C", ".cpp" or ".cc" or ".hpp" => "C++", ".go" => "Go", ".lean" => "Lean",
        ".asm" or ".s" => "Assembly", ".metal" => "Metal", ".m" => "Objective-C", ".pl" => "Prolog",
        ".html" => "HTML", ".css" => "CSS", ".md" => "Markdown", ".json" => "JSON", ".toml" => "TOML",
        ".yml" or ".yaml" => "YAML", ".idr" => "Idris", ".zig" => "Zig", ".java" => "Java", ".kt" => "Kotlin",
        _ => "Other",
    };

    /// <summary>
    /// Groups the tree into rooms by top-level directory (then second level for large ones),
    /// merges tiny directories into a Commons room, and lays rooms out over floors joined
    /// by a stairwell. Every room hangs off its floor's corridor, so the graph is connected
    /// by construction; <see cref="RoomGraph.Unreachable"/> verifies it.
    /// </summary>
    public static RoomGraph Build(CanonicalRepository repo, int roomsPerFloor)
    {
        var files = repo.Tree
            .Where(e => e.Kind == TreeEntryKind.File && !IsIgnored(e.Path))
            .OrderBy(e => e.Path, StringComparer.Ordinal)
            .ToList();

        // Bucket by top-level directory; split big buckets one level deeper.
        var buckets = new SortedDictionary<string, List<TreeEntry>>(StringComparer.Ordinal);
        void Add(string key, TreeEntry e)
        {
            if (!buckets.TryGetValue(key, out var list)) buckets[key] = list = [];
            list.Add(e);
        }
        var byTop = files.GroupBy(f => f.Path.Contains('/') ? f.Path[..f.Path.IndexOf('/')] : "");
        foreach (var group in byTop)
        {
            var items = group.ToList();
            bool split = group.Key != "" && items.Count > WorldConstants.MaxArtifactsPerRoom;
            foreach (var e in items)
            {
                if (!split) { Add(group.Key, e); continue; }
                var rest = e.Path[(group.Key.Length + 1)..];
                Add(rest.Contains('/') ? $"{group.Key}/{rest[..rest.IndexOf('/')]}" : group.Key, e);
            }
        }

        var roomDirs = new List<(string Name, List<string> Dirs, List<TreeEntry> Files)>();
        var commons = (Name: "Commons", Dirs: new List<string>(), Files: new List<TreeEntry>());
        foreach (var (dir, list) in buckets)
        {
            if (dir == "" || list.Count < WorldConstants.MinFilesForOwnRoom) { commons.Dirs.Add(dir == "" ? "/" : dir); commons.Files.AddRange(list); }
            else roomDirs.Add((dir, [dir], list));
        }
        // Semantic compression: keep the biggest rooms, fold the tail into Commons.
        var budget = WorldConstants.MaxRoomsPerBuilding - 1;
        foreach (var extra in roomDirs.OrderByDescending(r => r.Files.Count).ThenBy(r => r.Name, StringComparer.Ordinal).Skip(budget).ToList())
        {
            roomDirs.Remove(extra);
            commons.Dirs.AddRange(extra.Dirs);
            commons.Files.AddRange(extra.Files);
        }
        if (commons.Files.Count > 0) roomDirs.Add(commons);

        var rooms = new List<Room>();
        var doors = new List<Door>();
        int nextId = 0;
        var lobby = new Room { Id = nextId++, Name = "Lobby", Purpose = RoomPurpose.Lobby, Floor = 0 };
        rooms.Add(lobby);

        int floors = Math.Max(1, (int)Math.Ceiling(roomDirs.Count / (double)roomsPerFloor));
        int prevStair = -1;
        for (int f = 0; f < floors; f++)
        {
            var corridor = new Room { Id = nextId++, Name = $"Corridor {f}", Purpose = RoomPurpose.Corridor, Floor = f };
            var stair = new Room { Id = nextId++, Name = $"Stairwell {f}", Purpose = RoomPurpose.Stairwell, Floor = f };
            rooms.Add(corridor); rooms.Add(stair);
            doors.Add(new Door(corridor.Id, stair.Id));
            if (f == 0) doors.Add(new Door(lobby.Id, corridor.Id));
            if (prevStair >= 0) doors.Add(new Door(prevStair, stair.Id));
            prevStair = stair.Id;

            foreach (var (name, dirs, list) in roomDirs.Skip(f * roomsPerFloor).Take(roomsPerFloor))
            {
                var room = new Room
                {
                    Id = nextId++,
                    Name = name,
                    Purpose = name == "Commons" ? RoomPurpose.Commons : PurposeFor(name),
                    Floor = f,
                    Directories = dirs,
                    FileCount = list.Count,
                    Artifacts = list
                        .OrderByDescending(e => KindFor(e.Path) == ArtifactKind.BuildStation)
                        .ThenByDescending(e => e.SizeBytes)
                        .ThenBy(e => e.Path, StringComparer.Ordinal)
                        .Take(WorldConstants.MaxArtifactsPerRoom)
                        .Select(e => new Artifact(e.Path, KindFor(e.Path), e.SizeBytes, LanguageFor(e.Path)))
                        .ToList(),
                };
                rooms.Add(room);
                doors.Add(new Door(corridor.Id, room.Id));
            }
        }
        return new RoomGraph(rooms, doors);
    }
}