Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 12 additions & 11 deletions Assets/_Project/Editor/BootstrapSceneGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -286,22 +286,23 @@ private static void EnsureGlutrinneMapAsset()
AssetDatabase.CreateAsset(map, mapPath);
}

// Both lists are READ from MatchBootstrap, never restated here.
// They used to be literals, and they went stale: this baked the
// five-field layout with a comment claiming it was what
// MatchBootstrap registers, straight through the map changes of
// sprint 21 that took it to fifteen. Nobody caught it because
// MapDefinitionSO has no runtime consumer — which makes a wrong
// value cheap today and expensive on the day someone reads it.
Vector2Int[] hqCentres = MatchBootstrap.CanonicalHqCentreCells;
Vector2Int[] fieldCells = MatchBootstrap.CanonicalFieldCells;

map.Initialize(
"Glutrinne",
MapBiomeType.Desert,
128,
128,
// D-107 HQ footprint centres: point mirror p -> 124-p.
new[] { new Vector2(5f, 5f), new Vector2(119f, 119f) },
// The five fields MatchBootstrap registers, in canonical id order.
new[]
{
new Vector2(7f, 7f),
new Vector2(117f, 117f),
new Vector2(24f, 40f),
new Vector2(100f, 84f),
new Vector2(62f, 62f),
});
System.Array.ConvertAll(hqCentres, c => new Vector2(c.x, c.y)),
System.Array.ConvertAll(fieldCells, c => new Vector2(c.x, c.y)));
EditorUtility.SetDirty(map);
}

Expand Down
30 changes: 29 additions & 1 deletion Assets/_Project/Scripts/Gameplay/Match/MatchBootstrap.cs
Original file line number Diff line number Diff line change
Expand Up @@ -308,7 +308,21 @@ public bool TryGetFieldInitialReserve(ushort fieldId, out long reserveAE)
/// flank 0/1, contested far flank 0/1. Presentation iterates this
/// list so marker and scatter geometry cannot silently omit a field.
/// </summary>
public Vector2Int[] AllFieldCells
public Vector2Int[] AllFieldCells => CanonicalFieldCells;

/// <summary>
/// The same list as <see cref="AllFieldCells"/>, readable WITHOUT a
/// live component. Editor tooling that bakes the canonical map reads
/// the layout from here instead of restating it: the field table
/// already stands in five places by necessity (Unity host, headless
/// scenario, three test mirrors — risk R-1 of sprint 21), and a sixth
/// copy in BootstrapSceneGenerator drifted silently through two map
/// changes before anyone noticed. Its comment still claimed "the five
/// fields MatchBootstrap registers" when there were fifteen. A copy
/// nobody reads is not harmless; it is a wrong answer waiting for its
/// first reader.
/// </summary>
public static Vector2Int[] CanonicalFieldCells
{
get
{
Expand All @@ -321,6 +335,20 @@ public Vector2Int[] AllFieldCells
}
}

/// <summary>
/// The two HQ footprint CENTRES in slot order (local, enemy), derived
/// from the same slot layouts the match spawns from — so the D-107
/// point mirror p -&gt; 124 - p holds by construction rather than by
/// a second hand-kept pair of literals. Same purpose as
/// <see cref="CanonicalFieldCells"/>: editor tooling reads, it does
/// not restate.
/// </summary>
public static Vector2Int[] CanonicalHqCentreCells => new[]
{
new Vector2Int(LocalLayout.HqOriginX + 1, LocalLayout.HqOriginY + 1),
new Vector2Int(EnemyLayout.HqOriginX + 1, EnemyLayout.HqOriginY + 1),
};

/// <summary>Lower-left footprint origin of the human HQ (4, 4).</summary>
public Vector2Int LocalHqOrigin => new Vector2Int(LocalPlayerLayout.HqOriginX, LocalPlayerLayout.HqOriginY);

Expand Down
28 changes: 28 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,34 @@ die Versionierung folgt (in der aktuellen Doku-Phase) dem Dokumentationsstand de
bei 2 AE/Tick, bis eine gespielte Balance-Kalibrierung belastbare Werte gibt

### Behoben
- **Das Architektur-Gate war auf unberührtem `main` rot, und niemand sah es (Paket 22.4).**
`run_gate_check.py` führte `Nova.AI.Data` auf Rang 2 neben `Nova.AI` — nach
dem Namen einsortiert, nicht nach der Abhängigkeitsrichtung. Die reale Kante
`Nova.AI → Nova.AI.Data` ist damit eine Kante innerhalb derselben Schicht, und
die verbietet D-061: das Kriterium **G0-B.3 meldete auf jedem Baum eine
Verletzung**. Aufgefallen ist es nur, weil das Gate ohnehin nicht läuft.
`Nova.AI.Data` referenziert nichts außer `Nova.Core` und wird sonst nur aus
Rang 4 gelesen — Rang 1 ist der einzige Rang, der den bestehenden Graphen
gültig macht, ohne eine Zeile Code zu bewegen. G0-B.3 ist damit erstmals grün
- **Die Schichtenkarte des Gates führte zwei Assemblies, die es nicht gibt (Paket 22.4).**
`Nova.Presentation.Maps` und `Nova.Presentation.Shaders` haben seit der
Zusammenlegung der Präsentationsschicht keine `.asmdef` mehr. Tote Ränge sind
nicht harmlos: sie genehmigen einem künftigen Träger dieses Namens still eine
Schicht, statt die Entscheidung zu erzwingen. Beide Einträge sind raus, und
ein neuer Wächter macht die Klasse dicht — **ein Eintrag in der Karte ohne
zugehörige `.asmdef` lässt das Gate jetzt rot werden.** Die Gegenrichtung
(eine `.asmdef` ohne Rang) war bereits abgedeckt; niemand hatte je andersherum
geschaut
- **Die sechste Kopie der kanonischen Feldlage ist beseitigt (Paket 22.3).**
`BootstrapSceneGenerator` backte eine eigene Fünferliste in `MapDefinitionSO`,
mit dem Kommentar „the five fields MatchBootstrap registers" — der seit Paket
21.7 falsch war, weil dort fünfzehn stehen. Statt die Literale nachzuziehen
(und damit die Kopie zu behalten) liest der Generator die Lage jetzt über die
neuen statischen Leseflächen `MatchBootstrap.CanonicalFieldCells` und
`CanonicalHqCentreCells`. Dasselbe gilt für die HQ-Mitten, die dort ebenfalls
ein zweites Mal literal standen. Risiko R-1 aus Sprint 21 zählt damit wieder
fünf statt sechs Stellen — und die verbleibenden fünf sind alle durch Tests
gepinnt
- **Der Gate-Vertrag zeigte auf ein Repository, das es nicht mehr gibt (#14, Stufe 1).**
Das GitHub-Repo heißt seit dem 09.08.2026 `VibecodingGermany/HashKrieg`;
`GateEvidence.schema.json`, `GateAuthorization.schema.json` und
Expand Down
42 changes: 39 additions & 3 deletions quality/scripts/run_gate_check.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,17 +74,34 @@
COMMIT_SHA_RE = re.compile(r"^[0-9a-f]{40,64}$")
# Architecture layers (D-061): references may only point strictly downwards.
# Nova.Editor is the host layer; test assemblies are handled separately.
#
# This map is the AUTHORITY on which layer an assembly sits in, so it must
# describe the assemblies that actually exist and no others. Both directions
# are guarded: an asmdef missing from the map trips "unknown assembly in
# reference graph" below, and a map entry without an asmdef trips the
# stale-entry check in analyze_asmdef_tree. Until 2026-08-29 the map carried
# Nova.Presentation.Maps and Nova.Presentation.Shaders, which have had no
# asmdef since the presentation layer was consolidated — dead ranks nobody
# noticed, because nothing looked the other way round. A rank that describes
# nothing is not harmless: it silently pre-approves a layer for whoever
# reintroduces that name later, instead of making them decide.
ASSEMBLY_RANKS = {
"Nova.Core": 0,
"Nova.Simulation": 1,
# Nova.AI.Data sat at rank 2 next to Nova.AI — filed by name, not by
# dependency direction. It is the profile data Nova.AI READS, references
# nothing but Nova.Core, and is otherwise consumed only from rank 4
# (Nova.Presentation.UI). At rank 2 the real edge Nova.AI -> Nova.AI.Data
# is a same-layer edge, which D-061 forbids: check_architecture (G0-B.3)
# has therefore been reporting a violation on an untouched tree. Rank 1 is
# the only rank that makes the existing graph valid without moving a line
# of code — every edge into and out of it stays strictly downward.
"Nova.AI.Data": 1,
"Nova.AI": 2,
"Nova.AI.Data": 2,
"Nova.Networking": 2,
"Nova.Data": 2,
"Nova.Gameplay": 3,
"Nova.Presentation": 4,
"Nova.Presentation.Maps": 4,
"Nova.Presentation.Shaders": 4,
"Nova.Presentation.UI": 4,
"Nova.Editor": 5,
}
Expand Down Expand Up @@ -157,6 +174,25 @@ def analyze_asmdef_tree(base: Path) -> list[str]:
continue
assemblies[name] = document

# The map must not describe assemblies that do not exist. The opposite
# direction (an asmdef with no rank) is caught per reference below; this
# is the direction nothing looked at, and it is how two dead ranks
# survived the consolidation of the presentation layer unnoticed. A rank
# without an asmdef pre-approves a layer for whoever reintroduces that
# name, instead of forcing the decision into the open.
#
# It fires during a rename, and that is the point: the map is part of the
# rename, not a thing that catches up afterwards. What it deliberately
# does NOT catch is a rank that is wrong rather than dead — an assembly
# filed one layer too low still passes here and is only caught by the
# edge checks it then fails to trip.
for mapped in sorted(ASSEMBLY_RANKS):
if mapped not in assemblies:
violations.append(
f"{mapped}: ranked in ASSEMBLY_RANKS but no .asmdef defines it "
"— drop the entry or add the assembly"
)

for name, document in sorted(assemblies.items()):
references = document.get("references")
if not isinstance(references, list):
Expand Down
Loading