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

@ -1,4 +1,6 @@
using AcDream.App;
using AcDream.App.Configuration;
using AcDream.App.Credentials;
using AcDream.App.Plugins;
using AcDream.App.Platform;
using AcDream.App.Rendering;
@ -32,17 +34,96 @@ Log.Information(
dependency =>
$"{dependency.Feature}={dependency.PublishedFileName}")));
var datDir = args.FirstOrDefault() ?? Environment.GetEnvironmentVariable("ACDREAM_DAT_DIR");
if (string.IsNullOrWhiteSpace(datDir))
{
Log.Error("usage: AcDream.App <dat-directory> (or set ACDREAM_DAT_DIR)");
return 2;
}
// Campaign LA slice LA1: --session-config <path> is purely additive — the
// existing one positional dat-dir argument and every ACDREAM_* env var keep
// working exactly as before when the flag is absent. See
// docs/plans/2026-08-14-launcher-campaign.md LA1.
string? sessionConfigFlagPath = ExtractFlagValue(args, "--session-config");
string[] positionalArgs = WithoutFlagAndValue(args, "--session-config");
var datDirArg = positionalArgs.FirstOrDefault();
var envDatDir = Environment.GetEnvironmentVariable("ACDREAM_DAT_DIR");
// Single read of the startup-time process environment. Every downstream
// consumer (GameWindow + collaborators) reads the typed bundle, not the
// raw env vars. See docs/architecture/code-structure.md §2 Rule 4.
var runtimeOptions = RuntimeOptions.FromEnvironment(datDir);
RuntimeOptions runtimeOptions;
if (sessionConfigFlagPath is not null)
{
SessionConfiguration sessionConfig;
SessionDescriptor session;
try
{
(sessionConfig, session) = SessionConfigurationLoader.Load(sessionConfigFlagPath);
}
catch (Exception error)
when (error is IOException
or UnauthorizedAccessException
or ArgumentException
or NotSupportedException
or System.Text.Json.JsonException
or SessionConfigurationException)
{
Log.Error("--session-config invalid: {Error}", error.Message);
return 2;
}
string? resolvedDatDir =
NullIfEmpty(sessionConfig.Process?.Content?.DatDirectory)
?? NullIfEmpty(datDirArg)
?? NullIfEmpty(envDatDir);
if (resolvedDatDir is null)
{
Log.Error(
"usage: AcDream.App <dat-directory> (or set ACDREAM_DAT_DIR, "
+ "or supply process.content.datDirectory in --session-config)");
return 2;
}
AppCredentialSecret? secret = null;
try
{
var resolver = new AppCredentialResolver(
Console.In,
applicationPaths.ConfigDirectory,
graphicalPlatform.OperatingSystem
== GraphicalHostOperatingSystem.Linux);
secret = resolver.Resolve(session.Id, session.Credential);
runtimeOptions = RuntimeOptions.FromSessionConfig(
resolvedDatDir,
Environment.GetEnvironmentVariable,
sessionConfigFlagPath,
sessionConfig,
session,
secret.Reveal());
}
catch (AppCredentialException error)
{
Log.Error("--session-config credential unavailable: {Error}", error.Message);
return 2;
}
finally
{
secret?.Dispose();
}
// Env-var flow untouched when the flag is absent; when both are present
// the flag wins — this line makes that explicit rather than silent.
Log.Information(
"--session-config {Path} present; overriding ACDREAM_LIVE*/ACDREAM_TEST_* "
+ "env-var live-session settings",
sessionConfigFlagPath);
}
else
{
var datDir = datDirArg ?? envDatDir;
if (string.IsNullOrWhiteSpace(datDir))
{
Log.Error("usage: AcDream.App <dat-directory> (or set ACDREAM_DAT_DIR)");
return 2;
}
runtimeOptions = RuntimeOptions.FromEnvironment(datDir);
}
if (runtimeOptions.DevTools)
{
@ -158,3 +239,35 @@ finally
}
return 0;
// Campaign LA slice LA1: --session-config <path> parsing helpers. Kept
// local/minimal rather than a general-purpose CLI parser — App has exactly
// one optional flag-with-value today; the positional dat-dir argument must
// stay untouched by its presence (see the comment above the flag parse).
static string? ExtractFlagValue(string[] arguments, string flag)
{
for (int i = 0; i < arguments.Length - 1; i++)
{
if (string.Equals(arguments[i], flag, StringComparison.Ordinal))
return arguments[i + 1];
}
return null;
}
static string[] WithoutFlagAndValue(string[] arguments, string flag)
{
var result = new List<string>(arguments.Length);
for (int i = 0; i < arguments.Length; i++)
{
if (string.Equals(arguments[i], flag, StringComparison.Ordinal))
{
i++; // also skip the flag's value
continue;
}
result.Add(arguments[i]);
}
return [.. result];
}
static string? NullIfEmpty(string? value) =>
string.IsNullOrWhiteSpace(value) ? null : value;