From dc6a5206c170d5a17e3dbafc061d762856bb63bf Mon Sep 17 00:00:00 2001 From: Dennis Westermann Date: Sun, 9 Aug 2026 20:55:12 +0200 Subject: [PATCH] test(ai): Ausgangspin der kanonischen KI-Partie vom Identitaetspin trennen (D-101) SkirmishAiTests pinnte Kennung, Entscheidungstick und Endzustands-Hash in einer Zusicherung. Die Kopplung war beabsichtigt, liegt aber an der falschen Naht: die beiden Zahlen bewegen sich bei JEDER Aenderung an der Simulation, in der die KI spielt. Paket 16.1 verschob den Endzustand, 16.2 zusaetzlich den Tick - beide fassen keine Zeile KI-Code an. Die im Kommentar hinterlegte Prozedur schickte den Netzstrang damit in das Verhaltensjournal des Einheitenstrangs. Entscheidungstick und Endzustand ziehen nach CanonicalAiOutcomeTests um, das der Netzstrang besitzt. Die Kennung bleibt in SkirmishAiTests, samt ihrer Prozedur. Die Diagnose geht nicht verloren: der neue Test liest die Kennung mit und unterscheidet im Fehlertext zwischen KI- und Simulationsaenderung. Neu ist eine Zusicherung, die es vorher nicht gab: eine unentschiedene Partie (Tick 0) ist ein Defekt und kein verschobener Pin. Sie wird zuerst und getrennt geprueft. Genau dieser Fall trat in Paket 16.3 auf. Vier Deklarationen in SkirmishAiTests werden internal, damit der Harness wiederverwendbar ist; er ist damit Vertragsflaeche und im Regelwerk vermerkt. tools/Nova.SimRunner.Tests/ bekommt erstmals eine Eigentuemerzeile. Keine Baseline-Datei beruehrt. Co-Authored-By: Claude Opus 5 --- CHANGELOG.md | 14 ++++ docs/production/DecisionLog.md | 71 ++++++++++++++++- .../hashkrieg/13-15_Parallelbetrieb.md | 7 +- .../CanonicalAiOutcomeTests.cs | 76 +++++++++++++++++++ tools/Nova.SimRunner.Tests/SkirmishAiTests.cs | 61 +++++++-------- 5 files changed, 194 insertions(+), 35 deletions(-) create mode 100644 tools/Nova.SimRunner.Tests/CanonicalAiOutcomeTests.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index d6ac279..ed056ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -100,6 +100,20 @@ die Versionierung folgt (in der aktuellen Doku-Phase) dem Dokumentationsstand de gespulte Partie ohne dieses Etikett nichts wert ist. ### Geändert +- **Der Ausgangspin der kanonischen KI-Partie ist vom Identitätspin getrennt + (D-101).** `SkirmishAiTests` pinnte Kennung, Entscheidungstick und + Endzustands-Hash in einer Zusicherung. Die beiden Zahlen bewegen sich aber bei + jeder Änderung an der Simulation, in der die KI spielt — im ersten + Wirtschaftssprint riss das jedes Paket, ohne dass eine Zeile KI-Code berührt + war, und die im Test hinterlegte Prozedur schickte den falschen Strang ins + Verhaltensjournal. Entscheidungstick und Endzustand liegen jetzt in + `CanonicalAiOutcomeTests` beim Maintainer-Strang, die Kennung bleibt beim + Einheitenstrang. Die Diagnose bleibt erhalten: der neue Test liest die Kennung + mit und unterscheidet im Fehlertext zwischen KI- und Simulationsänderung. Neu + ist eine Zusicherung, die es vorher nicht gab — eine **unentschiedene** Partie + gilt als Defekt und nicht als verschobener Pin. Dazu bekommt + `tools/Nova.SimRunner.Tests/` erstmals eine Eigentümerzeile in der + Schreibhoheitstabelle: geteilt je Datei, fremde Testdateien nur nach Ansage. - **Angeschlagene KI-Einheiten drehen ab (Einheitenstrang, KI-Verhalten `r4`):** Wer die KI angriff, merkte nichts davon — angeschlagene Einheiten kämpften bis zum letzten Lebenspunkt. Eine Einheit unter 60 % Leben, in deren Nähe (8 diff --git a/docs/production/DecisionLog.md b/docs/production/DecisionLog.md index 8d76f21..4c4e8b5 100644 --- a/docs/production/DecisionLog.md +++ b/docs/production/DecisionLog.md @@ -1,6 +1,6 @@ # Decision Log -**Version:** 1.32.0 | **Status:** aktiv (laufend) | **Verantwortungsbereich:** Game Director / Lead Technical Director / Project Owner | **Sprint:** 16 +**Version:** 1.33.0 | **Status:** aktiv (laufend) | **Verantwortungsbereich:** Game Director / Lead Technical Director / Project Owner | **Sprint:** 16 ## Zweck @@ -2924,6 +2924,74 @@ Vertriebsweg und setzt sich später neben `install_hash`, ohne Tabellen, Sperrarten oder Bedienweg zu ändern. Q-041 bleibt bis zur Öffnung der Beta offen. +--- + +### D-101 | verbindlich | Sprint 16 (der Ausgangspin der kanonischen KI-Partie wird vom Identitätspin getrennt) + +**Status:** Inhaberentscheidung vom 2026-08-09, umgesetzt im selben Zug. + +**Kontext:** `SkirmishAiTests.AiBehaviorId_TracksWhatTheAiActuallyDoes` pinnte +drei Werte in einer Zusicherung: die KI-Kennung `AiBehaviorId.Value`, den +Entscheidungstick der kanonischen Partie und deren Endzustands-Hash. Die +Kopplung war beabsichtigt — der Kommentar begründet sie damit, dass die Kennung +geänderte Zahlen fängt und der Endzustand geänderte Regeln. + +Der erste Wirtschaftssprint hat gezeigt, dass die Kopplung an der falschen Naht +liegt: Entscheidungstick und Endzustand bewegen sich bei **jeder** Änderung an +der Simulation, in der die KI spielt. Paket 16.1 verschob den Endzustand, 16.2 +zusätzlich den Tick — beide fassen keine Zeile KI-Code an. Die im Test +hinterlegte Prozedur schickte den Netzstrang damit in Arns Verhaltensjournal, +also in das falsche Buch. Der Test liegt zudem in +`tools/Nova.SimRunner.Tests/`, einem Verzeichnis, das in der +Schreibhoheitstabelle gar keinen Eigentümer hatte. + +**Alternativen:** + +1. **Arn zieht den Pin nach jedem Merge des Netzstrangs nach.** Verworfen: bei + zehn Paketen in Sprint 16 sind das zehn Runden Ping-Pong über einen + Fork-PR-Weg, und jede blockiert einen fertigen Maintainer-PR an einer + Zahl, die niemand aus seinem Strang bewegt hat. +2. **Der Netzstrang aktualisiert die Zahlen selbst, der Test bleibt, wo er + ist.** Verworfen: dann schreibt ein Strang regelmäßig in die Testdatei des + anderen, ohne dass die Tabelle das deckt — genau die Unklarheit, die den + Fall erzeugt hat. Ausserdem bliebe die irreführende Prozedur im Kommentar + stehen. +3. **Den Ausgangspin ersatzlos streichen.** Verworfen: er ist der einzige Test, + der bemerkt, dass die KI die kanonische Partie überhaupt nicht mehr + entscheidet. Genau das ist in Paket 16.3 passiert (Entscheidungstick 0), und + ohne den Pin wäre es niemandem aufgefallen. +4. **ANGENOMMEN: der Pin wird geteilt.** Die Kennung bleibt beim + Einheitenstrang (`SkirmishAiTests.AiBehaviorId_TracksWhichAiThisIs`), + Entscheidungstick und Endzustand ziehen in `CanonicalAiOutcomeTests` um, + das der Netzstrang besitzt. + +**Entscheidung:** + +1. `CanonicalAiOutcomeTests` (neu, Netzstrang) pinnt Entscheidungstick und + Endzustands-Hash der kanonischen KI-Partie. +2. `SkirmishAiTests` behält die Kennung und deren Prozedur (Revision bumpen, + Journaleintrag) unverändert. +3. Der Ausgangstest **liest die Kennung mit** und macht die Unterscheidung im + Fehlertext: Ausgang bewegt **und** Kennung bewegt heisst KI-Änderung; + Ausgang bewegt und Kennung unverändert heisst Simulationsänderung. +4. **Unentschieden ist ein Defekt, kein verschobener Pin.** Der Ausgangstest + sichert das zuerst und getrennt zu, damit der Fehlertext beide Fälle + auseinanderhält. +5. `tools/Nova.SimRunner.Tests/` bekommt eine Eigentümerzeile: geteilt je + Datei, fremde Testdateien nur nach Ansage. Der KI-Harness (`AiHost`, + `BuildMatch`) wird Vertragsfläche und dafür `internal`. + +**Begründung:** Die Kennung beantwortet „welche KI ist das", der Ausgang +beantwortet „hat sich irgendetwas bewegt". Das sind zwei Fragen, und nur die +erste gehört dem Einheitenstrang. Die Diagnose, die die Kopplung leistete, geht +nicht verloren — sie wandert aus der Zusicherung in den Fehlertext, wo sie +ohnehin hingehört: gelesen wird sie erst, wenn der Test rot ist. + +**Konsequenzen:** Vier Deklarationen in `SkirmishAiTests` werden `internal`, +die Zusicherung dort schrumpft auf eine Zeile, der erklärende Kommentar +verweist auf die neue Datei. Der Einheitenstrang ist zu informieren (Issue #75). +Wer den Harness umbenennt, sagt es an. Keine Baseline-Datei ist berührt. + ## Offene Punkte - Alle Sprint-4-Review-Befunde (105, davon 9 kritisch): 7 entscheidungsbedürftige kritische Befunde sind durch D-043–D-052 entschieden. @@ -3004,6 +3072,7 @@ offen. | Version | Datum | Änderung | Autor | |---|---|---|---| +| 1.33.0 | 2026-08-09 | D-101 aufgenommen: der Ausgangspin der kanonischen KI-Partie (Entscheidungstick, Endzustand) wird vom Identitätspin getrennt und zieht in eine Maintainer-Datei; `tools/Nova.SimRunner.Tests/` bekommt erstmals eine Eigentümerzeile | Project Owner / Orchestrator | | 1.0.0 | 2026-07-21 | D-001 bis D-005 aus Sprint 0 protokolliert | Game Director | | 1.1.0 | 2026-07-21 | D-006 (Unity 6.3 LTS + URP bestätigt) aus Sprint-1-Validierung | Lead Technical Director | | 1.2.0 | 2026-07-21 | D-007 bis D-019: verbindliche Game-Design-Grundlagen (Q-001–Q-012, Q-016, Q-017) | Game Director | diff --git a/docs/production/hashkrieg/13-15_Parallelbetrieb.md b/docs/production/hashkrieg/13-15_Parallelbetrieb.md index 0f84430..d522823 100644 --- a/docs/production/hashkrieg/13-15_Parallelbetrieb.md +++ b/docs/production/hashkrieg/13-15_Parallelbetrieb.md @@ -1,6 +1,6 @@ # Parallelbetrieb Sprint 13–18 — zwei Stränge, eine Simulation -**Version:** 1.3.1 | **Status:** verbindlich ab Merge des Sprint-13.0-PR | **Verantwortungsbereich:** Maintainers und Strangverantwortliche | **Sprint:** 13–18 | **Gilt für:** [13](13_Sprint_Netzpartie.md), [13B](13B_Sprint_Einheitenverhalten.md), [14](14_Sprint_Lobby.md), [15](15_Sprint_Netzstabilitaet.md), [16](16_Sprint_Wirtschaft.md), [18](18_Sprint_Befehl_und_Auswahl.md) | **Leitsatz:** getrennte Ordner sind billig, getrennte Determinismus-Zustände nicht +**Version:** 1.4.0 | **Status:** verbindlich ab Merge des Sprint-13.0-PR | **Verantwortungsbereich:** Maintainers und Strangverantwortliche | **Sprint:** 13–18 | **Gilt für:** [13](13_Sprint_Netzpartie.md), [13B](13B_Sprint_Einheitenverhalten.md), [14](14_Sprint_Lobby.md), [15](15_Sprint_Netzstabilitaet.md), [16](16_Sprint_Wirtschaft.md), [18](18_Sprint_Befehl_und_Auswahl.md) | **Leitsatz:** getrennte Ordner sind billig, getrennte Determinismus-Zustände nicht ## Warum es dieses Dokument gibt @@ -85,6 +85,7 @@ Freiraum. | `Scripts/Simulation/Replays/`, `Snapshots/` | **niemand ohne D-ID** | Speicherformat und Fingerprint — Änderung ist eine Inhaberentscheidung | | `Scripts/Simulation/State/` — **Layout und Serialisierung** | **niemand ohne D-ID** | Feldbestand, Feldreihenfolge, `StateVersion`, Blockformat. Das ist der Teil, der Snapshots und Replays unlesbar macht | | `Scripts/Simulation/State/` — **Befehlsanwendung** (`UnitCommandStateView`) | **Netzstrang** | mit D-095 aus dem Frost gelöst: *was* ein bestehender `CommandKind` in den Zustand schreibt, ist Verhalten, nicht Format. **Kein neuer `CommandKind`** — das Register bleibt eingefroren | +| `tools/Nova.SimRunner.Tests/` | **geteilt — je Datei** | jeder Strang besitzt die Testdateien zu seinem Gegenstand. Änderungen an einer fremden Testdatei werden **vorher angesagt**. Zwei Pins sind namentlich zugeordnet: `SkirmishAiTests.AiBehaviorId_TracksWhichAiThisIs` gehört dem Einheitenstrang, `CanonicalAiOutcomeTests` dem Netzstrang (D-101) | | `CHANGELOG.md` | **serialisiert** | ein Eintrag pro PR, Konflikte löst der Mergende | | `docs/production/hashkrieg/` | **Maintainer** | Planungsstand; Befunde kommen per Mail oder Issue, nicht per PR | @@ -107,7 +108,7 @@ D-095 schreibt diese bereits gelebte Linie nur auf. ### Vertragsflächen in fremdem Besitz -Vier Flächen gehören einem Strang, werden aber vom anderen konsumiert. Dort gilt +Fünf Flächen gehören einem Strang, werden aber vom anderen konsumiert. Dort gilt zusätzlich: **Verhalten ändern ja, Vertrag ändern nur nach Absprache.** | Fläche | Eigentümer | Konsument | Was ohne Absprache nicht geht | @@ -115,6 +116,7 @@ zusätzlich: **Verhalten ändern ja, Vertrag ändern nur nach Absprache.** | `Pathfinding.CostField` | Einheitenstrang | `ConstructionSystem` (Platzierungsprüfung, Sprint 16) | Signatur oder Begehbarkeits-Semantik von `IsWalkable` ändern. Flow-Field-Erzeugung und Pathfinding-Interna sind frei | | `FogOfWarSystem.GetTeamView` | Netzstrang | `CombatSystem` (Zielerlaubnis) | Rückgabeform oder Sichtbarkeitsregel ändern, ohne den Einheitenstrang zu informieren | | `WeaponProfiles`-Slot `UnitRole.Unit` | Einheitenstrang | `ConstructionSystem` (Baustellen tragen heute diese Rolle) | Sprint 16 löst die Kopplung auf, indem die Baustelle `def.Role` statt `UnitRole.Unit` bekommt. Bis dahin gilt: der Fallback-Schaden von 15 ist **keine** Baustellenregel, sondern ein Nebeneffekt. Wer den Slot umwidmet, sagt es an | +| `SkirmishAiTests.AiHost` / `BuildMatch` | Einheitenstrang | `CanonicalAiOutcomeTests` (Netzstrang) | Umbenennen oder Signatur ändern, ohne es anzusagen — der Ausgangspin baut darauf auf. Der Inhalt des Harness ist frei | | `UnitState.AttackTarget` | Einheitenstrang (`CombatSystem`) | `UnitCommandStateView` (`Stop` löscht es, Sprint 16) | das Feld **löschen** darf der Netzstrang. Eine Regel, *wann automatisch neu erfasst wird* (D-087, Auto-Zielerfassung), gehört dem Einheitenstrang. Ein „Feuer einstellen" ist deshalb kein Netzstrang-Paket | ## Neue Systeme — wer die Tick-Reihenfolge setzt @@ -291,6 +293,7 @@ zusammen gespielt wurden, sind zwei Behauptungen. | Version | Datum | Änderung | Autor | |---|---|---|---| +| 1.4.0 | 2026-08-09 | **D-101:** `tools/Nova.SimRunner.Tests/` bekommt eine Eigentümerzeile — geteilt je Datei, fremde Testdateien nur nach Ansage. Der Ausgangspin der kanonischen KI-Partie ist vom Identitätspin getrennt: `CanonicalAiOutcomeTests` (Netzstrang) hält Entscheidungstick und Endzustand, `SkirmishAiTests` die Kennung. Der KI-Harness ist damit Vertragsfläche | Orchestrator | | 1.3.1 | 2026-08-09 | Zugangsmodell nachgezogen: der externe Beitragende erhält einen Collaborator-Eintrag auf der Stufe **Triage**, damit ihn Issues erreichen können. Der Code-Weg bleibt Fork-only, die Merge-Sperre unberührt | Orchestrator | | 1.3.0 | 2026-08-09 | **D-095:** Trennung von „Verhaltensraum" auf „Dateihoheit" umgestellt — Sprint 16 läuft parallel zu 13B statt dahinter. `Simulation/State/` in Layout (weiter eingefroren) und Befehlsanwendung (Eigentümer des jeweiligen Befehls) getrennt. Zwei Vertragsflächen ergänzt (`WeaponProfiles`-Slot `UnitRole.Unit`, `UnitState.AttackTarget`). Merge-Fenster auf einen Strang je Fenster verschärft. Abschnitte „Definitions-Hash" und „kanonische Startaufstellung an vier Stellen" ergänzt. Plattform-Abschnitt berichtigt: der Linux-Build existiert seit `e15f5e6`, die offene Bringschuld ist stattdessen ein `NovaBuildCommit`-Leser im Spiel | Orchestrator | | 1.2.2 | 2026-08-09 | Zwei Pfade nachgetragen, die der Einheitenstrang in der Praxis braucht und die die Tabelle nicht kannte: `tools/Nova.AiLab/` samt Tests (Messwerkzeug, kein Spielcode) und `Presentation/UI/DebugHud.cs` als ausdrückliche Ausnahme aus `Presentation/`. Beide Lücken lagen im Dokument, nicht im Verhalten des Beitragenden | Producer / Agent (Umsetzung) | diff --git a/tools/Nova.SimRunner.Tests/CanonicalAiOutcomeTests.cs b/tools/Nova.SimRunner.Tests/CanonicalAiOutcomeTests.cs new file mode 100644 index 0000000..262ead1 --- /dev/null +++ b/tools/Nova.SimRunner.Tests/CanonicalAiOutcomeTests.cs @@ -0,0 +1,76 @@ +using NUnit.Framework; +using Nova.AI.Data; + +namespace Nova.SimRunner.Tests +{ + /// + /// Pins the OUTCOME of the canonical AI match: the tick it is decided on + /// and the end-state hash. Owned by the maintainer strand (D-101). + /// + /// Why this is not in any more: these two + /// numbers move on ANY change to the simulation the AI plays in — economy, + /// construction, production, vision — not only on a change to the AI + /// itself. They sat in the AI suite until 2026-08-09, where every package + /// of Sprint 16 tripped them and the failure message sent the maintainer + /// strand to the AI behaviour journal, which is the wrong book. The + /// identifier pin — "which AI is this" — stayed where it belongs, in + /// . + /// + /// + /// The split does not lose the diagnosis, because this test reads the + /// identifier too. Read the two results together: + /// + /// + /// outcome moved, identifier movedthe AI + /// changed. The AI strand bumps AiBehaviorId.Revision, writes the + /// journal entry in tools/Nova.AiLab/reports/behavior-log.md, and + /// updates the numbers below in the same commit. + /// outcome moved, identifier unchangedthe + /// simulation under the AI changed. The strand that changed it updates the + /// numbers below and says so in its PR. No journal entry, no revision + /// bump — nothing about the AI moved. + /// outcome UNDECIDED (tick 0)not a moved + /// pin. The AI no longer finishes the match inside the budget, which means + /// something broke its loop. Fix the cause; do not update the + /// numbers. + /// + /// + /// This is NOT one of the four determinism baselines: those live in their + /// own files and force a behaviour PR and a baseline PR apart. This pin + /// belongs WITH the change that moved it and is updated in the same commit. + /// + /// + [TestFixture] + public sealed class CanonicalAiOutcomeTests + { + /// Decided tick of the canonical AI match, last moved by: Sprint 15 (r5). + private const uint PinnedDecidedTick = 2548u; + + /// End-state hash of the canonical AI match, last moved by: Sprint 15 (r5). + private const string PinnedEndState = "0x14472B2B943ED2BB"; + + [Test] + public void CanonicalAiMatch_DecidesOnThePinnedTick_WithThePinnedEndState() + { + SkirmishAiTests.AiHost host = SkirmishAiTests.BuildMatch(SkirmishAiTests.Seed); + uint decided = host.RunUntilDecided(SkirmishAiTests.EndToEndBudgetTicks); + ulong endState = host.Kernel.CalculateStateHash(); + + // Undecided is a defect, not a moved pin — it is asserted first and + // on its own so the failure says which of the two happened. + Assert.That(decided, Is.Not.Zero, + $"the AI did not decide the canonical match within {SkirmishAiTests.EndToEndBudgetTicks} ticks — " + + "its loop is broken, not merely moved. Fix the cause instead of updating the pin."); + + Assert.Multiple(() => + { + Assert.That(decided, Is.EqualTo(PinnedDecidedTick), + "the canonical AI match is decided on a different tick than the pinned one — " + + $"AI identifier is {AiBehaviorId.Value}; if that string is unchanged, the simulation moved, not the AI"); + Assert.That($"0x{endState:X16}", Is.EqualTo(PinnedEndState), + "the canonical AI match ends in a different state than the pinned one — " + + $"AI identifier is {AiBehaviorId.Value}; if that string is unchanged, the simulation moved, not the AI"); + }); + } + } +} diff --git a/tools/Nova.SimRunner.Tests/SkirmishAiTests.cs b/tools/Nova.SimRunner.Tests/SkirmishAiTests.cs index c2e375f..3357d57 100644 --- a/tools/Nova.SimRunner.Tests/SkirmishAiTests.cs +++ b/tools/Nova.SimRunner.Tests/SkirmishAiTests.cs @@ -40,7 +40,8 @@ namespace Nova.SimRunner.Tests [TestFixture] public sealed class SkirmishAiTests { - private const ulong Seed = 0xA17E57DE57UL; + /// Seed of the canonical AI match. Internal since D-101: CanonicalAiOutcomeTests pins its outcome. + internal const ulong Seed = 0xA17E57DE57UL; private const byte HumanSlot = 0; private const byte AiSlot = 1; private const ushort MapWidth = 128; @@ -53,13 +54,13 @@ public sealed class SkirmishAiTests /// at tick 2242, so 6.000 ticks is a ~2.7x margin — comfortably sane, /// and exact because the whole loop is deterministic. /// - private const int EndToEndBudgetTicks = 6000; + internal const int EndToEndBudgetTicks = 6000; // ---------------------------------------------------------------- // The AI host (mirror of MatchRunner's skirmish wiring) // ---------------------------------------------------------------- - private sealed class AiHost + internal sealed class AiHost { public SimulationKernel Kernel; public EntityManager Entities; @@ -250,7 +251,7 @@ private static void ApplyOpeningPosition(AiHost host) } } - private static AiHost BuildMatch(ulong seed, AiProfile? profile = null) + internal static AiHost BuildMatch(ulong seed, AiProfile? profile = null) { AiHost host = BuildAiHost(seed, profile); ApplyOpeningPosition(host); @@ -417,22 +418,29 @@ public void SkirmishAi_EndToEnd_IsByteDeterministicAcrossRuns() // ---------------------------------------------------------------- /// - /// Pins TOGETHER with what the AI actually - /// does. Either half alone is useless: the identifier's profile hash - /// catches changed numbers but never a changed rule, and the end state - /// catches a changed rule but does not know the identifier exists. + /// Pins — the identifier that says WHICH AI + /// this is. It carries the profile hash and the revision, so it catches + /// changed numbers, and the revision catches a changed rule that the + /// author declared. /// - /// WHEN THIS GOES RED — and only then read on, because the failure - /// message is the procedure: - /// - /// - /// Was the behaviour change intended? If not, fix the code. The - /// test just told you the AI plays differently than you thought. - /// If it was: bump AiBehaviorId.Revision, add its line to - /// the history in that file, write the journal entry in + /// WHEN THIS GOES RED: the AI's profile numbers or its revision moved. + /// Bump AiBehaviorId.Revision, add its line to the history in + /// that file, write the journal entry in /// tools/Nova.AiLab/reports/behavior-log.md — measured values, - /// better AND worse — and only then update the numbers below. - /// + /// better AND worse — and only then update the string below. + /// + /// + /// THE OTHER HALF LIVES ELSEWHERE (D-101, 2026-08-09). This pin used to + /// also assert the decided tick and the end-state hash of the canonical + /// AI match. Those two numbers move on ANY simulation change, not only + /// on an AI change — every package of Sprint 16 tripped them — and the + /// procedure above then pointed the wrong strand at the wrong journal. + /// They now live in CanonicalAiOutcomeTests, which the maintainer + /// strand owns. The diagnosis stays intact because that test reads this + /// identifier: outcome moved AND identifier moved means the AI changed; + /// outcome moved and identifier unchanged means the simulation under it + /// changed. + /// /// /// This is NOT one of the four determinism baselines and must not be /// treated as one: those live in their own files and separate a @@ -441,21 +449,10 @@ public void SkirmishAi_EndToEnd_IsByteDeterministicAcrossRuns() /// /// [Test] - public void AiBehaviorId_TracksWhatTheAiActuallyDoes() + public void AiBehaviorId_TracksWhichAiThisIs() { - AiHost host = BuildMatch(Seed); - uint decided = host.RunUntilDecided(EndToEndBudgetTicks); - ulong endState = host.Kernel.CalculateStateHash(); - - Assert.Multiple(() => - { - Assert.That(AiBehaviorId.Value, Is.EqualTo("r5.779A1B5B"), - "the AI identifier changed — bump the revision and write the journal entry"); - Assert.That(decided, Is.EqualTo(2548u), - "the AI decides the canonical match on a different tick than the pinned one"); - Assert.That($"0x{endState:X16}", Is.EqualTo("0x14472B2B943ED2BB"), - "same identifier, different end state: behaviour moved without the revision moving"); - }); + Assert.That(AiBehaviorId.Value, Is.EqualTo("r5.779A1B5B"), + "the AI identifier changed — bump the revision and write the journal entry"); } // ----------------------------------------------------------------