A licence reports LICENSE_EXPIRED. The support desk issues a replacement. The
replacement reads as expired too.
This is the ticket that costs the most, and until v1.4 our own tooling could not answer it. The licence had not expired. The machine’s clock was running three days fast, and every licence that machine will ever be issued reads as expired the moment the clock skips forward again. Re-issuing does not fix a clock.
The information needed to say so was already in the report. It was just in two different places.
Why a per-section reader can’t get there
parse_health() walks a diagnostic report and emits one item per signal. It has
been shipping since the Licence Inspector landed, and it is good at what it does:
every item is a fact, drawn from one section, with no inference layered on top.
That is also its ceiling. The expiry verdict lives in the licence section. The measured clock drift that explains it lives in the clock section. A reader that derives every row from one section in isolation reports both — correctly, and as two independent findings — and leaves the join to whoever is reading. On a good day that is an engineer who spots it. On a normal day it is a support reply saying “we’ll send you a new licence.”
So the charter for diagnose() was narrow from the start: correlate across
sections, never re-litigate inside a LicenseResult. A rule earns its place
only by saying something a single error code cannot.
The rule that stops it becoming a second error reference
The obvious failure mode for a diagnostic engine is that it grows into a duplicate of the error catalog — same advice, different file, drifting apart at its own pace. Two years later nobody knows which one is authoritative.
One admission rule prevents it. A rule ships only if it does one of two things:
- correlates two or more sections of the report, or
- splits one code into distinct causes with genuinely distinct remedies.
The second case is narrower than it sounds, and the example that justifies it is
HashMismatch. A binary that does not match its .sig is either a modified
binary or a half-applied upgrade where the library was replaced and the signature
was not. Those need opposite responses, and nothing in the code itself
distinguishes them — only the file timestamps recorded in the report do. That
split is worth a rule.
Where correlation changes nothing, no rule fires and the catalog’s own
suggested_action is surfaced instead. The engine says less than it could, on
purpose.
Ranking, and why there are no weights
Once you have several candidate causes you have to order them, and the tempting answer is a weighted score.
We rejected it. A weighted score is unauditable. When an engineer asks why the engine told their customer to renew a licence that was not expired, the answer has to be a sentence — and a weight vector is not a sentence. You cannot review one in a diff, and you cannot argue with one.
A free-form priority integer is worse in a different way. It rots. The first insertion is at 50, the next at 47, and within a year the numbers encode a history nobody remembers.
What shipped is a discrete key: (band, -confidence, declaration index). Band
first, because a contributing factor must never outrank the thing it contributes
to. Confidence second. Declaration order last — which means the tie-break is the
order the rules are written in the source file, reviewable in a diff like
anything else.
Suppression is a static per-rule list, applied only when the suppressor actually
fired. It is one level deep and never transitive, and an Informational cause
can never silence a Certain one.
Suppressed causes are kept, not deleted
When one cause explains another, the explained one stays in the result and is marked, rather than being dropped.
That costs a field and some rendering complexity, and it is not negotiable. An
engine that silently discards what it rejected cannot be argued with by an
engineer who thinks it is wrong — and in the field, that engineer will sometimes
be right. Every cause carries the report fields it was drawn from, a confidence,
one action, and a stable cause_id a program can branch on. The list is never
empty, even for a healthy report.
It never reads the clock
diagnose() is a pure function of the report it is handed. It never reads the
system clock. The only “now” anywhere in the implementation is
clock.system_time_epoch — the value the report recorded on the machine where
the problem happened.
That is what makes a diagnosis quotable. A report diagnosed a year after it was
collected yields the byte-identical answer it would have given on the day. One
std::time(nullptr) in a “days remaining” calculation would destroy the property
silently, so the file that implements the rules includes no <ctime> at all and
says so at the top.
The sign convention is the part that nearly went wrong. drift_seconds is
defined as online_time - system_time, so a positive value means the clock is
behind. A licence only appears falsely expired when the drift is negative. An
early draft of the rule table had that inverted, which would have told customers
with healthy licences to fix their clocks, and customers with genuinely dead
licences that their clock was at fault. The test that covers it asserts both
directions in one body, for the obvious reason.
There is a second guard worth naming. Before the engine will blame the clock, it checks that the licence is not also expired against the corrected time — otherwise “your clock is wrong” becomes a way to wave away a licence that really has run out.
What we deliberately did not ship
rg_inspect does not diagnose. The tool your end users run collects and seals;
it does not interpret.
Two reasons, and neither is technical. The anti-rollback remedies double as a
tamper recipe for whoever caused the problem in the first place. And changing
what a customer’s end users see on screen is the customer’s decision, not ours.
Causes carrying that kind of reasoning are flagged support_only, so a vendor
building their own interface can filter them out and we are not the ones deciding
for them.
The engine’s disclosure is also never broader than the report’s own, and narrower wherever the destination is a screen: no rule quotes a masked hardware value or a masked metadata value back, whatever the report was collected with.
On the reader side, --diagnose is a fourth mode. --summary is unchanged byte
for byte, down to its trailing line, because its entire value is being identical
to what the end user is reading aloud on the phone. Exit codes are unchanged too
— 0 whenever the file was read, whatever it says. Diagnosing a broken licence is
the tool succeeding, and a reader that exited non-zero on a successful diagnosis
would break every script that tests for 0.
The full cause reference is Customer Documentation §10.1.2. If you want to watch it run against a real machine, the demo bundle is on the download page.