8. Turning stdout and stderr into Typed Observations
Process output becomes useful when an application can distinguish a raw line, a decoded protocol record and a semantic observation. BinaryWrapper transports both stdout and stderr; each tool integration owns the interpretation that is justified by its reviewed output sources.
The important question is what an event means, and which evidence produced it. A readable C# type name does not make an arbitrary string a reliable infrastructure fact.
What the application can rely on
Typed output exposes recognized facts alongside their original evidence. The application's contract includes the selected output profile, the selected version and the logical process channel.
A known message can report readiness or progress. It cannot substitute for the process's final exit. Unknown formats remain observable through raw or generic records. This allows a product to show progress without fabricating completion and to retain diagnostics when a tool's output changes.
Three layers of interpretation
The line transport supplies OutputLine(Text, Source). A wire decoder recognizes a protocol such as Vagrant's comma-separated records or Docker's JSON events. A semantic mapper derives observations with a narrower meaning.
A parser's ParseLine can return zero, one or multiple events. A valid Vagrant row can yield its raw wire record, a UI message and a specific boot observation. Applications should not count all of these as separate completed operations.
Provenance on events
OutputEvent exposes OutputLine, OutputVersion and OutputProfile. A line-derived event retains the original logical line object. Decoded fields may be normalized, while that original text remains available.
A terminal event comes from process completion and has no producing line. A parser that buffers a multi-line document may emit its projection later; it must retain the earlier source lines rather than attribute it to the line or exit that happened to trigger emission.
Complete JSON evidence can preserve fields the semantic model does not currently know. This is particularly important for GitLab API responses and daemon extensions that evolve independently of a CLI release.
Vagrant's machine-readable envelope
Vagrant uses records shaped like timestamp,target,type,data.... UI output has a method and message in its data fields. The parser decodes known comma/newline escapes while retaining the original encoded row.
This complete offline example decodes one reviewed boot message and checks its producing object identity.
using FrenchExDev.Net.BinaryWrapper;
using FrenchExDev.Net.Vagrant;
var line = new OutputLine(
"1700000000,web,ui,output,Machine booted and ready!",
OutputSource.StdOut);
var events = new VagrantMachineReadableParser().ParseLine(line).ToList();
var ready = events.OfType<VagrantMachineBooted>().Single();
Console.WriteLine(ready.MachineName);
Console.WriteLine(ReferenceEquals(line, ready.OutputLine));
Console.WriteLine(ready.Evidence!.RawText);using FrenchExDev.Net.BinaryWrapper;
using FrenchExDev.Net.Vagrant;
var line = new OutputLine(
"1700000000,web,ui,output,Machine booted and ready!",
OutputSource.StdOut);
var events = new VagrantMachineReadableParser().ParseLine(line).ToList();
var ready = events.OfType<VagrantMachineBooted>().Single();
Console.WriteLine(ready.MachineName);
Console.WriteLine(ReferenceEquals(line, ready.OutputLine));
Console.WriteLine(ready.Evidence!.RawText);This direct parser call deliberately chooses a format. Automatic version selection belongs to the strategy/factory path explained in chapter 09.
The boot projection requires the expected output method, full message shape and consistent machine target. A state,running record is a provider-state observation, not boot completion. A translated or modified boot message remains generic rather than becoming an invented readiness event.
Message catalogues and named parameters
Vagrant's output catalogue is derived from a pinned upstream checkout. Its maintenance tool extracts translation keys and suitable Ruby literals, and explicitly binds reviewed dynamic selections.
Generated UI records expose named interpolation parameters. A port-forward message can expose guest port, host port and adapter; a provisioning message can expose the provisioner. These UI captures are strings because upstream already rendered them. A structured wire projection can apply stronger numeric validation.
Recognition checks the UI method and complete template. Repeated placeholders must agree. More specific literal templates outrank broad patterns; equally specific matches produce an ambiguity observation. Bare or adjacent placeholders cannot reliably determine template identity.
These rules describe recognized shapes. They do not prove a third-party script could never print the same shape. Literal interpolation delimiters can also be ambiguous when parameter text contains those delimiters. Raw evidence remains essential.
The catalogue generator is an explicit maintenance tool. Build-time generation explains why it is different from Roslyn command generation.
stderr participates in parsing
stderr is another source of observations, not an automatic error verdict. Git Trace2 JSON can arrive there. Compose progress can use it. Human diagnostics and fallback records also retain it.
A Vagrant machine-readable-looking line on stderr is not automatically treated as a valid stdout machine protocol record. Recognized plain stderr UI diagnostics have their own records without invented machine-readable timestamps.
The tool-specific parser must establish the format in its context. Treating every JSON-looking stderr line as the same schema would mix tracing, errors and service output.
Collection and semantic results
IResultCollector<TEvent, TResult> has OnEvent and Complete. A collector uses observations to build a result relevant to one operation.
VagrantUpCollector collects distinct ready machine names in observation order. Its result distinguishes observed readiness from terminal success. A zero-exit command without boot observations can validly return an empty ready-machine list.
PackerBuildCollector tracks artifacts, fields and report completeness. A build milestone or artifact ID alone does not establish a complete artifact report. Missing indices, counts or end markers remain observable incompleteness.
The executor additionally enforces process success for collected execution. A nonzero process exit returns CommandError even after it has delivered terminal observations to the collector.
Unknown, malformed and incomplete output
Unknown record kinds preserve wire evidence. Unsupported versions select raw fallback. Multi-line JSON parsing has explicit retention limits; invalid, incomplete or oversized documents must not become partly invented resources.
A successful process can still produce too little recognized output for the application to establish a required postcondition. Another supported query or a domain-specific probe may be needed.
The implementation studies describe these limits per tool. They are part of the usable contract and the maintenance cost of the integration.
Source snapshot
Source inspection: 2 October 2026. These implementation paths are relative to Net/FrenchExDev in the local ecosystem checkout. The v2 changes are largely uncommitted; these paths identify inspected code, without claiming that public branch links contain it.
BinaryWrapper/src/FrenchExDev.Net.BinaryWrapper.Contracts/OutputEvent.csVagrant/src/FrenchExDev.Net.Vagrant/VagrantMachineReadableParser.csVagrant/src/FrenchExDev.Net.Vagrant/VagrantMachineReadableEventMapper.csVagrant/src/FrenchExDev.Net.Vagrant/VagrantUpCollector.csVagrant/doc/TYPED-OUTPUT.md
Previous · Series index · Next