using Repoverse.Core.Model;
using static Repoverse.Core.WorldConstants;
namespace Repoverse.Core.Generation;
///
/// Canonical repositories → world manifest. Pure and deterministic: same snapshot,
/// seed, overrides and previous manifest give an equal manifest (§59).
///
public static class ManifestBuilder
{
private const int RoomWidth = 6;
/// Plot slots of a district ordered outward from its centre, so districts grow as compact blobs.
private static readonly (int Col, int Row)[] SpiralSlots = Enumerable.Range(0, DistrictSide * DistrictSide)
.Select(i => (Col: i % DistrictSide, Row: i / DistrictSide))
.OrderBy(s => Math.Max(Math.Abs(s.Col * 2 + 1 - DistrictSide), Math.Abs(s.Row * 2 + 1 - DistrictSide)))
.ThenBy(s => Math.Atan2(s.Row * 2 + 1 - DistrictSide, s.Col * 2 + 1 - DistrictSide))
.ToArray();
public static (int X, int Z) DistrictOrigin(District d)
{
var i = (int)d;
const int stride = DistrictSpan + 2 * PlotSize; // avenue between districts
return (i % DistrictGridColumns * stride, i / DistrictGridColumns * stride);
}
public static (int X, int Z) PlotOrigin(PlotAddress plot)
{
var (ox, oz) = DistrictOrigin(plot.District);
var (col, row) = SpiralSlots[plot.Slot];
return (ox + col * PlotSize, oz + row * PlotSize);
}
public static WorldManifest Build(
IReadOnlyList repos,
Seed worldSeed,
Overrides? overrides = null,
WorldManifest? previous = null)
{
overrides ??= Overrides.Empty;
var ordered = repos.OrderBy(r => r.Id.Key, StringComparer.Ordinal).ToList();
var dup = ordered.GroupBy(r => r.Id.Key).FirstOrDefault(g => g.Count() > 1);
if (dup != null) throw new ArgumentException($"Repository '{dup.Key}' appears twice in the snapshot.");
var classified = ordered.Select(r =>
{
var category = Classifier.Classify(r, overrides);
var district = overrides.District(r.Id) is { } d
? new Derived(d, Provenance.Override(overrides.Source))
: new Derived(Classifier.DistrictFor(category.Value, r.Archived),
new("district-from-category", r.Archived ? "archived" : category.Value.ToString()));
return (Repo: r, Category: category, District: district);
}).ToList();
// Sticky placement (§7, §85): a repo keeps its plot while it stays in the same district;
// newcomers take the innermost free slot in id order.
var taken = new Dictionary>();
var slotOf = new Dictionary();
foreach (var c in classified)
{
var old = previous?.Find(c.Repo.Id);
if (old == null || old.Plot.District != c.District.Value) continue;
if (!taken.TryGetValue(old.Plot.District, out var set)) taken[old.Plot.District] = set = [];
if (set.Add(old.Plot.Slot)) slotOf[c.Repo.Id.Key] = old.Plot;
}
foreach (var c in classified)
{
if (slotOf.ContainsKey(c.Repo.Id.Key)) continue;
if (!taken.TryGetValue(c.District.Value, out var set)) taken[c.District.Value] = set = [];
var slot = Enumerable.Range(0, SpiralSlots.Length).FirstOrDefault(s => !set.Contains(s), -1);
if (slot < 0) throw new InvalidOperationException($"District {c.District.Value} is full ({SpiralSlots.Length} plots).");
set.Add(slot);
slotOf[c.Repo.Id.Key] = new PlotAddress(c.District.Value, slot);
}
var inWorld = ordered.Select(r => r.Id.Key).ToHashSet();
var buildings = classified.Select(c => Transform(c.Repo, c.Category, c.District, slotOf[c.Repo.Id.Key], worldSeed, overrides)).ToList();
var edges = ordered
.SelectMany(r => r.DependsOn.Where(d => inWorld.Contains(d.Key) && d.Key != r.Id.Key).Select(d => new DependencyEdge(r.Id, d)))
.DistinctBy(e => (e.From.Key, e.To.Key))
.OrderBy(e => e.From.Key, StringComparer.Ordinal).ThenBy(e => e.To.Key, StringComparer.Ordinal)
.ToList();
return new WorldManifest
{
WorldSeed = worldSeed.ToString(),
Snapshots = ordered.ToDictionary(r => r.Id.Key, r => r.SnapshotId),
Buildings = buildings,
Dependencies = edges,
};
}
///
/// Repository → building (§9). Floors follow module structure (room count), depth follows
/// code volume, activity follows recent commits, construction follows releases — not size → height.
///
public static BuildingDefinition Transform(
CanonicalRepository repo,
Derived category,
Derived district,
PlotAddress plot,
Seed worldSeed,
Overrides overrides)
{
var seed = worldSeed.Derive("repo:" + repo.Id.Key);
var archetype = overrides.Archetype(repo.Id) is { } a
? new Derived(a, Provenance.Override(overrides.Source))
: new Derived(Classifier.ArchetypeFor(category.Value), new("archetype-from-category", category.Value.ToString()));
bool landmark = overrides.Landmark(repo.Id);
// Count prospective rooms first to size the floor plate, then build the real graph with that width.
var probe = RoomGraphBuilder.Build(repo, int.MaxValue);
int workRooms = probe.Rooms.Count(r => r.Purpose is not (RoomPurpose.Lobby or RoomPurpose.Corridor or RoomPurpose.Stairwell));
int maxPerSide = (MaxFootprint - 2) / RoomWidth;
int perSide = Math.Clamp((int)Math.Ceiling(Math.Sqrt(Math.Max(1, workRooms))), 1, maxPerSide);
if (landmark) perSide = maxPerSide;
var interior = RoomGraphBuilder.Build(repo, perSide * 2);
int floors = Math.Min(MaxFloors, interior.Rooms.Count(r => r.Purpose == RoomPurpose.Corridor));
long codeBytes = repo.Languages.Values.Sum();
if (codeBytes == 0) codeBytes = repo.Tree.Where(e => e.Kind == TreeEntryKind.File).Sum(e => e.SizeBytes);
int roomDepth = Math.Clamp(3 + (int)Math.Log2(Math.Max(1, codeBytes / 4096.0) + 1), 4, 10);
int width = Math.Clamp(2 + perSide * RoomWidth, MinFootprint, MaxFootprint);
int depth = Math.Clamp(2 * roomDepth + 7, MinFootprint, MaxFootprint);
var (px, pz) = PlotOrigin(plot);
var footprint = new Footprint(px + (PlotSize - width) / 2, pz + (PlotSize - depth) / 2, width, depth);
var activity = repo.CommitsLast90Days switch
{
0 => ActivityLevel.Dormant,
< 10 => ActivityLevel.Low,
< 50 => ActivityLevel.Moderate,
_ => ActivityLevel.High,
};
var construction = repo.Archived ? ConstructionState.Archived
: repo.Releases.Any(r => !r.Prerelease) ? ConstructionState.Released
: repo.Releases.Count > 0 ? ConstructionState.Prerelease
: ConstructionState.Unreleased;
return new BuildingDefinition
{
Repo = repo.Id,
Description = repo.Description ?? "",
Category = category,
District = district,
Archetype = archetype,
Material = MaterialFor(archetype.Value),
Plot = plot,
Footprint = footprint,
Floors = floors,
RoomsPerSide = perSide,
RoomDepth = roomDepth,
Activity = new(activity, new("commits-in-window", $"{repo.CommitsLast90Days} commits / {ActivityWindowDays}d")),
Construction = new(construction, new("release-state", repo.Archived ? "archived" : $"{repo.Releases.Count} releases")),
Landmark = landmark,
Population = Math.Clamp(repo.Contributors, 1, perSide * 2 * floors),
Seed = seed.ToString(),
Interior = interior,
};
}
public static MaterialFamily MaterialFor(BuildingArchetype a) => a switch
{
BuildingArchetype.Foundry => MaterialFamily.Metal,
BuildingArchetype.ControlTower => MaterialFamily.Concrete,
BuildingArchetype.Vault => MaterialFamily.Security,
BuildingArchetype.ProofChamber => MaterialFamily.Stone,
BuildingArchetype.Exchange => MaterialFamily.Glass,
BuildingArchetype.Laboratory => MaterialFamily.Laboratory,
BuildingArchetype.Library => MaterialFamily.Archive,
BuildingArchetype.Studio => MaterialFamily.Glass,
BuildingArchetype.Warehouse => MaterialFamily.Concrete,
BuildingArchetype.Relay => MaterialFamily.Electronics,
BuildingArchetype.Commons => MaterialFamily.Wood,
_ => MaterialFamily.Wood,
};
}