From a3a25ca82c1bb662e7f86bfee6ceeaa48db35603 Mon Sep 17 00:00:00 2001 From: Dennis Westermann Date: Sat, 29 Aug 2026 12:50:53 +0200 Subject: [PATCH] fix(quality): das Architektur-Gate sagt wieder die Wahrheit, und die sechste Feldlage-Kopie faellt weg (22.3, 22.4) --- .../Editor/BootstrapSceneGenerator.cs | 23 +++++----- .../Scripts/Gameplay/Match/MatchBootstrap.cs | 30 ++++++++++++- CHANGELOG.md | 28 +++++++++++++ quality/scripts/run_gate_check.py | 42 +++++++++++++++++-- 4 files changed, 108 insertions(+), 15 deletions(-) diff --git a/Assets/_Project/Editor/BootstrapSceneGenerator.cs b/Assets/_Project/Editor/BootstrapSceneGenerator.cs index 2a6a961..dfd9e40 100644 --- a/Assets/_Project/Editor/BootstrapSceneGenerator.cs +++ b/Assets/_Project/Editor/BootstrapSceneGenerator.cs @@ -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); } diff --git a/Assets/_Project/Scripts/Gameplay/Match/MatchBootstrap.cs b/Assets/_Project/Scripts/Gameplay/Match/MatchBootstrap.cs index acd984b..e687ec2 100644 --- a/Assets/_Project/Scripts/Gameplay/Match/MatchBootstrap.cs +++ b/Assets/_Project/Scripts/Gameplay/Match/MatchBootstrap.cs @@ -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. /// - public Vector2Int[] AllFieldCells + public Vector2Int[] AllFieldCells => CanonicalFieldCells; + + /// + /// The same list as , 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. + /// + public static Vector2Int[] CanonicalFieldCells { get { @@ -321,6 +335,20 @@ public Vector2Int[] AllFieldCells } } + /// + /// 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 -> 124 - p holds by construction rather than by + /// a second hand-kept pair of literals. Same purpose as + /// : editor tooling reads, it does + /// not restate. + /// + public static Vector2Int[] CanonicalHqCentreCells => new[] + { + new Vector2Int(LocalLayout.HqOriginX + 1, LocalLayout.HqOriginY + 1), + new Vector2Int(EnemyLayout.HqOriginX + 1, EnemyLayout.HqOriginY + 1), + }; + /// Lower-left footprint origin of the human HQ (4, 4). public Vector2Int LocalHqOrigin => new Vector2Int(LocalPlayerLayout.HqOriginX, LocalPlayerLayout.HqOriginY); diff --git a/CHANGELOG.md b/CHANGELOG.md index 881afd5..2acd6a3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/quality/scripts/run_gate_check.py b/quality/scripts/run_gate_check.py index d8fe3fa..ed0c7b3 100644 --- a/quality/scripts/run_gate_check.py +++ b/quality/scripts/run_gate_check.py @@ -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, } @@ -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):