docs: Campaign LA — pinned launch-contract schema COMMITTED into plan LA1

The LA3 Opus review process note was right: the contract both sides
implement lived only in orchestrator prompts, which is exactly the drift
mode the pin exists to prevent (and it produced the paths-key CRITICAL).
The schema, field rules, probe-mode discriminator, and status vocabulary
are now a binding plan section; amendments change this text first,
implementations second. Ledger: LA3 fix round dispatched.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-08-14 16:04:32 +02:00
parent 0bcc7ba3a3
commit db9ad53c1c
38 changed files with 2397 additions and 40 deletions

View file

@ -135,6 +135,7 @@ public sealed class LiveSessionControllerTests
public Action? OnReset { get; set; }
public Action? OnConnecting { get; set; }
public Action? OnConnected { get; set; }
public Action? OnRoster { get; set; }
public Action? OnSelected { get; set; }
public Action? OnActivate { get; set; }
public Action? OnEntered { get; set; }
@ -146,6 +147,7 @@ public sealed class LiveSessionControllerTests
public bool ThrowOnBind { get; set; }
public bool ThrowOnConnecting { get; set; }
public bool ThrowOnConnected { get; set; }
public bool ThrowOnRoster { get; set; }
public bool ThrowOnSelected { get; set; }
public bool ThrowOnActivate { get; set; }
public bool ThrowOnEntered { get; set; }
@ -158,6 +160,7 @@ public sealed class LiveSessionControllerTests
public List<TestCommandBus> CommandBuses { get; } = [];
public List<LiveSessionCharacterSelection> Selections { get; } = [];
public List<RuntimeGenerationToken> ResetGenerations { get; } = [];
public List<LiveSessionRosterReport> Rosters { get; } = [];
public LiveSessionBinding BindSession(WorldSession session)
{
@ -231,6 +234,15 @@ public sealed class LiveSessionControllerTests
throw new InvalidOperationException("connected failure");
}
public void ReportRoster(LiveSessionRosterReport roster)
{
calls.Add("roster");
Rosters.Add(roster);
OnRoster?.Invoke();
if (ThrowOnRoster)
throw new InvalidOperationException("roster failure");
}
public void ApplySelectedCharacter(LiveSessionCharacterSelection selection)
{
calls.Add("selected");
@ -290,7 +302,7 @@ public sealed class LiveSessionControllerTests
Assert.Equal(
[
"reset", "resolve", "create", "bind", "report-connecting",
"connect", "report-connected", "selected", "enter:1",
"connect", "report-connected", "roster", "selected", "enter:1",
"activate", "entered",
],
calls);
@ -302,6 +314,32 @@ public sealed class LiveSessionControllerTests
Assert.True(host.CommandBuses[0].Active);
}
[Fact]
public void Start_ReportsRosterFromCharacterListBeforeSelection()
{
var calls = new List<string>();
var operations = new TestOperations(calls);
var host = new TestHost(calls);
var controller = new LiveSessionController(operations);
LiveSessionStartResult result = controller.Start(LiveOptions(), host);
Assert.Equal(LiveSessionStartStatus.Connected, result.Status);
LiveSessionRosterReport roster = Assert.Single(host.Rosters);
Assert.Equal("Canonical", roster.AccountName);
Assert.Equal(11, roster.SlotCount);
Assert.Equal(
[
new LiveSessionRosterEntry(0x50000001u, "Grey", 10u),
new LiveSessionRosterEntry(0x50000002u, "Ready", 0u),
],
roster.Entries);
// "roster" must land strictly before "selected" — the launcher's
// char-select screen (LA7/LA8) will read the roster before any
// selection has been made.
Assert.True(calls.IndexOf("roster") < calls.IndexOf("selected"));
}
[Fact]
public void Start_DisabledAndMissingCredentialsResetButNeverConstructSession()
{
@ -488,7 +526,7 @@ public sealed class LiveSessionControllerTests
[
"deactivate", "detach-events", "dispose-session", "detach-session",
"reset", "resolve", "create", "bind", "report-connecting",
"connect", "report-connected", "selected", "enter:1",
"connect", "report-connected", "roster", "selected", "enter:1",
"activate", "entered",
],
calls);
@ -742,6 +780,7 @@ public sealed class LiveSessionControllerTests
[InlineData("connecting")]
[InlineData("connected")]
[InlineData("characters")]
[InlineData("roster")]
[InlineData("selected")]
[InlineData("activate")]
[InlineData("entered")]
@ -755,6 +794,7 @@ public sealed class LiveSessionControllerTests
case "connecting": host.ThrowOnConnecting = true; break;
case "connected": host.ThrowOnConnected = true; break;
case "characters": operations.ThrowOnCharacters = true; break;
case "roster": host.ThrowOnRoster = true; break;
case "selected": host.ThrowOnSelected = true; break;
case "activate": host.ThrowOnActivate = true; break;
case "entered": host.ThrowOnEntered = true; break;