proximo

Checks are a first-class concept, with a report and a remedy

proximo diagnoses unevenly. preflight() verifies three things and returns bare errors; dns.CheckPortFree guards the DNS port while nothing guards :80 or :443; proximo status prints four different lines that nobody calls a diagnosis; and docs/troubleshooting.md documents seventeen failure modes in prose that no code detects. Underneath all of it sits a sharper fact: the codebase contains no verification at all. tls installs and removes trust, dns configures and removes the resolver, and neither knows how to lookplatform.Has and platform.IsActiveService are the only predicates in the project. Every attempt to make one failure actionable ran into the same missing piece: proximo had no way to state what it had observed, only a way to fail.

Decision. A Check is one independently verifiable statement, and a Report is one complete pass of them. A check passes, fails, or is skipped, and every failure carries a Remedy — the cure where one exists, and otherwise the command whose own output names the cause, generalising the docker pull remedy that stack.go already prints for an absent stack image. Checks live in one registry, proximo doctor runs all of them, and install and up gate on the subset that is meaningful before the host has been changed. A check never elevates: read the host, never write it, and never ask for a password. The two commands divide cleanly, and the division is the contract: proximo status never prints a Remedy, proximo doctor always does.

Considered options

Fold the report into proximo status. No new command, and status already prints warnings. Rejected: status answers what is running, a row per route, and the common case is a clean table. Growing it into what is broken makes the healthy case noisier for every developer in order to serve the broken one, and leaves no way to ask for a diagnosis without asking for an inventory.

Check only at install time. Rejected outright: the moment a developer wants a diagnosis is never the moment they are installing. It is three weeks later, when a name stopped resolving.

Keep preflight.go as its own thing, separate from the registry. Rejected because the divergence has already happened and is visible in the tree: the DNS port is guarded and :443 is not, and neither is ever reported to a developer who asks. Two lists of the same kind of fact drift apart by default.

A fourth outcome, warn, for things that are wrong but not broken — a stack running an older version than the CLI, say. Rejected: that skew has an exact remedy, proximo update, and anything with a remedy is a failure. warn would mean "a failure proximo decided not to insist on", and that decision belongs to the developer reading the report, not to proximo writing it.

Let a failed check report without a remedy, or let the remedy be prose. Rejected: both turn Remedy into a synonym for "advice", and the term stops carrying a promise. Holding the line forced the useful reframing below — a diagnostic command is a remedy when the cause is unknown.

net.LookupHost for the end-to-end DNS check. Rejected, and worth recording because it is the obvious choice and it is wrong: Go's pure resolver reads /etc/resolv.conf and honours neither /etc/resolver/<tld> on macOS nor a Domains=~<tld> drop-in on Linux. It would report a failure on a perfectly healthy machine — the one outcome a diagnostic tool must never produce.

Let doctor use sudo where a check would need it. Rejected as a matter of definition rather than convenience. Everything proximo must read is readable unprivileged, and a check that genuinely needed elevation would be describing a repair. The constraint is load-bearing and easy to erode one check at a time.

Ship --json with the first version. Rejected: the exit code covers the only named consumer, the internal shape is typed anyway, and nothing in proximo emits JSON outward today. Adding it later breaks nothing.

Consequences