diff --git a/spikes/README.md b/spikes/README.md
index 3b65681..9443df2 100644
--- a/spikes/README.md
+++ b/spikes/README.md
@@ -32,17 +32,32 @@ Two facts it discovered that anything referencing this package needs:
```powershell
cd windows/spikes/WslcApiDump
dotnet run # dump the API + check every facade assumption
-dotnet run -- --probe # + call the two read-only statics (service version, missing components)
-dotnet run -- --session # + create a real session and print its live property VALUES
+dotnet run -- --probe # + GetMissingComponents / GetVersion
+dotnet run -- --session # + create a session, then a SECOND one with the same name
+dotnet run -- --all-types # include the ABI/marshalling plumbing in the dump
```
+**If it reports missing components** (`VirtualMachinePlatform`, `WslPackage`), the machine cannot
+run wslc yet: `wsl --install`, reboot for the Virtual Machine Platform feature, and run again.
+Everything that reaches the service fails with `REGDB_E_CLASSNOTREG` (0x80040154) until then, and
+the tool says so rather than emitting a string of unexplained COM errors. `--session` is skipped
+in that state instead of failing.
+
+Note that the assumption check reads the **`Microsoft.WSL.Containers`** namespace only. That is
+correctness, not tidiness: a C#/WinRT projection also exports `ABI.Microsoft.WSL.Containers.*`
+marshalling types with the *same short names*, and matching on short name alone checks every
+member against the marshalling struct — which reported 42 false MISSINGs, with `CreateMarshaler`
+helpfully offered as the nearest name.
+
It writes the full public object model to `wslc-api-dump.txt` (`--out` to relocate) and prints an
`ok` / `MISSING` line per assumption, each with *why that member matters* and a nearest-name hint.
It exits 0 even when assumptions fail — a mismatch is the product, not an error. Only a genuinely
broken run (the assembly won't load) exits non-zero.
Every path in it has been exercised against a stand-in assembly carrying the observed 2.9.3 type
-and member names, so a failure on your machine is a finding about wslc, not about this tool.
+and member names — including the `ABI.` shadow types and a service that reports missing components
+and throws `0x80040154` — so a failure on your machine is a finding about wslc, not about this
+tool.
**What to send back:** the console output, and `wslc-api-dump.txt` if anything is MISSING —
which now means the package moved under us, not that we guessed wrong.
diff --git a/spikes/WslcApiDump/Program.cs b/spikes/WslcApiDump/Program.cs
index e2ac6aa..c469d85 100644
--- a/spikes/WslcApiDump/Program.cs
+++ b/spikes/WslcApiDump/Program.cs
@@ -27,6 +27,14 @@ internal static class Program
/// loading it by package name fails — which is exactly the first thing this tool found.
private const string AssemblyName = "wslcsdkcs";
+ /// The API namespace. Filtering on it is not tidiness — it is correctness. A C#/WinRT
+ /// projection also exports `ABI.Microsoft.WSL.Containers.*` marshalling plumbing whose types
+ /// have the SAME short names (`Session`, `Container`, `ProcessSettings`), and `ABI.` sorts
+ /// first. Matching assumptions by short name therefore checked every member against the
+ /// marshalling struct and reported 42 false MISSINGs, with `CreateMarshaler` offered as the
+ /// nearest name — which is the tell.
+ private const string ApiNamespace = "Microsoft.WSL.Containers";
+
private static int Main(string[] args)
{
var probe = args.Contains("--probe") || args.Contains("--session");
@@ -46,29 +54,65 @@ internal static class Program
return 2;
}
- var types = assembly.GetExportedTypes().OrderBy(t => t.FullName, StringComparer.Ordinal).ToArray();
- Console.WriteLine($"{AssemblyName} {assembly.GetName().Version} — {types.Length} public types");
+ var exported = assembly.GetExportedTypes().OrderBy(t => t.FullName, StringComparer.Ordinal).ToArray();
+ var types = exported.Where(t => t.Namespace == ApiNamespace).ToArray();
+ Console.WriteLine(
+ $"{AssemblyName} {assembly.GetName().Version} — {types.Length} types in {ApiNamespace} "
+ + $"({exported.Length - types.Length} more are ABI/marshalling plumbing)");
Console.WriteLine();
var report = new StringBuilder();
report.AppendLine($"# {AssemblyName} {assembly.GetName().Version}");
report.AppendLine($"# location: {assembly.Location}");
report.AppendLine();
- foreach (var type in types) DescribeType(type, report);
+ // The dump defaults to the API namespace for the same reason the checks do; `--all-types`
+ // includes the ABI plumbing for when the projection itself is what's being debugged.
+ foreach (var type in args.Contains("--all-types") ? exported : types) DescribeType(type, report);
File.WriteAllText(outPath, report.ToString());
Console.WriteLine($"full API dump → {Path.GetFullPath(outPath)}");
Console.WriteLine();
var missing = CheckAssumptions(types);
- if (probe) ProbeStatics(types);
- if (session) ProbeSession(types, ArgValue(args, "--session-name") ?? "nucleic-spike");
+ // Components are probed BEFORE anything else that touches the service. On a machine
+ // where WSL container support isn't installed, every live call fails with
+ // REGDB_E_CLASSNOTREG (0x80040154) — "Class not registered" — and reporting that as a
+ // string of mysterious COM errors would bury the one fact that explains them all.
+ var componentsMissing = probe ? ProbeStatics(types) : null;
+ if (session)
+ {
+ if (componentsMissing is { Count: > 0 })
+ {
+ Console.WriteLine();
+ Console.WriteLine("skipping --session: the components above are missing, so creating "
+ + "a session would only fail with the same 'class not registered'.");
+ }
+ else
+ {
+ ProbeSession(types, ArgValue(args, "--session-name") ?? "nucleic-spike");
+ }
+ }
Console.WriteLine();
- Console.WriteLine(missing == 0
- ? "RESULT: every WslcFacade assumption is present. Fix nothing; write the typed spike."
- : $"RESULT: {missing} assumption(s) wrong — each one is a line to change in "
- + "windows/NucleicBroker/Wslc/WslcFacade.cs, and nowhere else (that is what IWslc is for).");
+ if (componentsMissing is { Count: > 0 })
+ {
+ Console.WriteLine($"RESULT: this machine cannot run wslc yet — missing "
+ + $"{string.Join(", ", componentsMissing)}. Install with `wsl --install` (the "
+ + "Virtual Machine Platform component needs a reboot), then run this again.");
+ Console.WriteLine(" This is the §8 onboarding condition, not a defect.");
+ }
+ else if (missing == 0)
+ {
+ Console.WriteLine("RESULT: every WslcFacade assumption is present — the package matches "
+ + "the surface recorded in docs/WINDOWS_PORT.md §13.1.");
+ }
+ else
+ {
+ Console.WriteLine($"RESULT: {missing} assumption(s) wrong. The recorded surface is 2.9.3, "
+ + "so this means the package MOVED: reconcile §13.1, then fix "
+ + "windows/NucleicBroker/Wslc/WslcFacade.cs — and nowhere else (that is what IWslc "
+ + "is for).");
+ }
// Exit 0 either way: a mismatch is this tool's PRODUCT, not its failure. Only a genuinely
// broken run (assembly missing) is non-zero, so a wrapper script can tell them apart.
return 0;
@@ -174,7 +218,8 @@ internal static class Program
wrong++;
Console.WriteLine($" MISSING {type.Name}.{assumption.Member} "
+ $"({assumption.MemberKind.ToString().ToLowerInvariant()})");
- Console.WriteLine($" why: {assumption.Why}");
+ foreach (var line in Wrap(assumption.Why, 94))
+ Console.WriteLine($" {line}");
var near = Nearest(assumption.Member, MemberNames(type));
if (near.Length > 0)
Console.WriteLine($" nearest: {string.Join(", ", near)}");
@@ -202,6 +247,28 @@ internal static class Program
/// Cheap "did they just rename it" hint: shared prefix or containment, no edit
/// distance. A three-name shortlist is enough to spot HostGateway vs
/// HostGatewayAddress, which is the realistic failure mode.
+ /// Hard-wrap so a long rationale can't be mangled into an unreadable fragment by the
+ /// console. First line is prefixed "why:", continuations are indented under it.
+ private static IEnumerable Wrap(string text, int width)
+ {
+ var words = text.Split(' ', StringSplitOptions.RemoveEmptyEntries);
+ var line = new StringBuilder("why: ");
+ var any = false;
+ foreach (var word in words)
+ {
+ if (line.Length + word.Length + 1 > width && any)
+ {
+ yield return line.ToString();
+ line = new StringBuilder(" ");
+ any = false;
+ }
+ if (any) line.Append(' ');
+ line.Append(word);
+ any = true;
+ }
+ if (any) yield return line.ToString();
+ }
+
private static string[] Nearest(string wanted, IEnumerable candidates)
{
var needle = wanted.TrimStart('.');
@@ -229,14 +296,17 @@ internal static class Program
/// Names come from the real 2.9.3 surface (`GetVersion`, `GetMissingComponents`) — the
/// documented `GetServiceVersion`/`ComponentFlags` shapes in Microsoft Learn's sample are
/// stale against the shipped package (docs/WINDOWS_PORT.md §13.1).
- private static void ProbeStatics(Type[] types)
+ private static IReadOnlyList? ProbeStatics(Type[] types)
{
Console.WriteLine();
Console.WriteLine("live probe (read-only):");
var service = types.FirstOrDefault(t => t.Name == "WslcService");
- if (service is null) { Console.WriteLine(" no WslcService type — skipping"); return; }
+ if (service is null) { Console.WriteLine(" no WslcService type — skipping"); return null; }
- foreach (var name in new[] { "GetVersion", "GetMissingComponents" })
+ List? componentsMissing = null;
+ // GetMissingComponents first: it answers from OS feature state and works even when the
+ // service class isn't registered, so it is the call that EXPLAINS the others.
+ foreach (var name in new[] { "GetMissingComponents", "GetVersion" })
{
var method = service.GetMethods(Public)
.FirstOrDefault(m => m.IsStatic && m.Name == name && m.GetParameters().Length == 0);
@@ -248,16 +318,26 @@ internal static class Program
}
try
{
- Console.WriteLine($" {name}() = {Render(method.Invoke(null, null))}");
+ var value = method.Invoke(null, null);
+ Console.WriteLine($" {name}() = {Render(value)}");
+ if (name == "GetMissingComponents" && value is System.Collections.IEnumerable list)
+ {
+ componentsMissing = list.Cast