Skip to content
Skip to main content
Novel Systems home
Decision log
D-051August 5, 2026

A published promise is gated on a round trip, not on a credential, whenever a date can invalidate it

Decided

Where a page states what *will happen* when a visitor acts, the statement is gated on a live check that the thing can happen, not on the presence of the credential that would make it possible. /support now prints its ticket promise behind helpdeskCanFileTickets(), which is one identity request to Zammad per revalidation window, instead of behind helpdeskHealth(), which reads four environment variables.

Affects: lib/helpdesk.ts, app/support/page.tsx, config/site.ts, scripts/check-claim-consistency.mjs, lib/engineering-practice.ts

What the alternative was, and why it was rejected. The obvious options were the two the owner was offered: buy a Zammad plan before the trial expires on 3 September 2026, or edit the copy so it stops promising tickets. Both leave the page's honesty dependent on somebody acting on a date. The trial does not change any environment variable when it lapses — HELPDESK_API_KEY still holds a plausible token, helpdeskHealth() still returns configured, and every submission comes back 401 while the page keeps promising "the receipt carries the ticket number the helpdesk issued". That is the failure documented in the header of lib/health/probe.ts, where /api/health reported configured for three dependencies that were failing every real request, reproduced deliberately with the expiry date already on the calendar. Buying the plan does not fix it; it postpones it to the next renewal that fails.

A credential is a claim about configuration. A promise is a claim about behaviour. No environment variable can answer a question about what a vendor will do, because the vendor's decision is not stored locally. This is the same distinction the four-state Reachability union was introduced for, applied to copy instead of to a status endpoint.

The failure direction is chosen deliberately. A transient network error downgrades the promise for the rest of the 60-second window, so a blip costs a sentence of accuracy in the pessimistic direction: the downgraded copy describes the mail fallback, which works whether or not the helpdesk does, and the form submits either way. Understating what the page will do costs nothing anybody notices. Overstating it costs somebody their P0.

The same pass established that deleting a claim does not retire it. The homepage tile "4 min — Average quote turnaround" was removed in July as an unowned figure. In August the identical number was still in config/site.ts inside PROOF_CARDS, relabelled "Quote-to-work-order handoff", one property below a comment explaining why the card above it had been rewritten to stop hardcoding figures. The retirement pass had searched for the label. Nothing imported the export, which is why nobody caught it and also why it was dangerous: a dead export shaped like live content is one map() away from being live again. It is deleted, and quote-turnaround-figure — the eleventh claim rule — now fails the build on any quote or bid duration written as a literal outside the four modules entitled to one. The general form: a claim is retired when a rule rejects its value, not when an editor removes its text.

What would make this wrong. If the helpdesk probe became expensive — a vendor that rate-limits identity reads, or one whose cheapest authenticated endpoint is a paginated list — the per-window round trip would stop being free and the gate would need a longer cache than the page's own revalidate. And if a promise were ever needed on a route that cannot be server-rendered, this pattern does not apply at all: the check must stay on the server, because lib/helpdesk.ts is import "server-only" and holds the API key.