독립 검증 도구 추가와 앱 상태 책임 분리

This commit is contained in:
2026-09-06 18:09:11 +09:00
parent 0f0431de5a
commit 002a922e1b
62 changed files with 3454 additions and 2008 deletions
@@ -0,0 +1,88 @@
using System.Text.Json;
using ProjectQuint.Probes;
var json = new JsonSerializerOptions { WriteIndented = true, PropertyNamingPolicy = JsonNamingPolicy.CamelCase };
try
{
var action = args.Length == 0 ? "list" : args[0];
var options = new Dictionary<string, int>(StringComparer.Ordinal);
string? output = null;
for (var index = 1; index < args.Length; index += 2)
{
if (index + 1 >= args.Length)
throw new ArgumentException("Every option requires a value.");
if (args[index] == "--output" && output == null)
output = args[index + 1];
else if (args[index] is "--notes" or "--seed" or "--fps" or "--iterations" &&
int.TryParse(args[index + 1], out var value) && options.TryAdd(args[index], value))
continue;
else
throw new ArgumentException("Unknown, duplicated or invalid option.");
}
var notes = options.GetValueOrDefault("--notes", 10_000);
var seed = options.GetValueOrDefault("--seed", 42);
var fps = options.GetValueOrDefault("--fps", 240);
var iterations = options.GetValueOrDefault("--iterations", 20);
if (seed < 0)
throw new ArgumentException("Seed must be nonnegative.");
object report;
switch (action)
{
case "list":
if (options.Count != 0 || output != null)
throw new ArgumentException("The list command accepts no options.");
report = new
{
schemaVersion = 1,
component = "headless-core",
commands = new[] { "list", "replay", "benchmark", "test (wrapper)" },
input = "seeded synthetic four-lane notes; no music or external library",
options = new[] { "--notes", "--seed", "--fps", "--iterations (benchmark)", "--output" },
};
break;
case "replay":
if (options.ContainsKey("--iterations"))
throw new ArgumentException("Iterations applies only to benchmarks.");
var workload = SyntheticWorkload.Create(notes, (uint)seed);
report = new
{
schemaVersion = 1, complete = true, notes, seed, frameRate = fps,
eventCount = workload.Events.Length,
result = ReplayRunner.Run(workload, fps),
};
break;
case "benchmark":
report = CoreBenchmark.Run(notes, (uint)seed, fps, iterations, checkpoint =>
{
Console.Error.WriteLine($"benchmark {checkpoint.Samples.Length}/{iterations}");
if (output != null)
Save(output, checkpoint);
});
break;
default:
throw new ArgumentException("Unknown command. Use list, replay, benchmark or test.");
}
if (output != null)
Save(output, report);
Console.WriteLine(JsonSerializer.Serialize(report, json));
return 0;
}
catch (Exception error)
{
// Paths in I/O exceptions can identify the local machine; keep structured failures generic.
Console.Error.WriteLine(JsonSerializer.Serialize(new
{
error = error.GetType().Name,
detail = error is ArgumentException ? error.Message : "The probe failed; no complete result was produced.",
}));
return 1;
}
void Save(string path, object report)
{
var destination = Path.GetFullPath(path);
Directory.CreateDirectory(Path.GetDirectoryName(destination)!);
var temporary = destination + ".tmp";
File.WriteAllText(temporary, JsonSerializer.Serialize(report, json) + "\n");
File.Move(temporary, destination, overwrite: true);
}