Health indicators — classification audit
Every ctx.health.register(...) call in packages/*/src, classified. This is the M70c audit
deliverable (plan §3.9): it stops the “hardcoded up next to live data” pattern recurring by
making every indicator’s status source explicit, and it is enforced by
test/health-indicator-audit.test.ts, which fails if a
ctx.health.register site is added (or moved) without a matching row here.
The CLI schematic at packages/cli/src/schematics/health-indicator.ts mentions
ctx.health.register inside a template string and is not a registration site.
There are 27 registration sites. static-plugin registers the same static-files indicator
from two branches (the no-fs arm and the fs arm), so it contributes two rows; only one is active
at a time.
What each status means
The health plugin projects every indicator to { status, data } (M70c, fixing X3-7) and aggregates:
/ready is 200 only when every indicator is up; /health is 503 only when any is down
(degraded is 200). The gRPC bridge maps the aggregate to grpc.health.v1.Health/Check — since
M70c, degraded maps to NOT_SERVING, agreeing with /ready (X7-8).
| Classification | Meaning |
|---|---|
live-state | The status reflects a live fact about the backend or a live probe: a reachability probe (isHealthy()), a readiness flag that tracks a real connection, or an actual I/O check. The status can change at runtime as the backend changes. |
justified-literal | The status is a fixed literal, but the justification is documented and the literal is the correct signal for that plugin (e.g. a platform binding whose presence is known at registration). |
configuration-literal | The status is a fixed literal (or a lifecycle flag that only ever reports “started / stopped”) while the data payload is live. The status says nothing about the backend’s health. These are the X8-5 class of defect — the indicator’s status is decorative. |
The six M70c in-scope packages (messaging, realtime-backplane, storage, mail, queue,
service-discovery) are now live-state: each reports both signals — isReady() (lifecycle) and
isHealthy() (reachability) — and the indicator maps them (see the per-package READMEs for the
status tables). M90b added cache and database; the audit, notification and worker-pool
rows followed, closing the last three H-70c defects this audit recorded, so no ctx.health.register
site outside the defensible in-process set is configuration-literal any more.
The register
| # | Indicator | Package | Site | Classification | Status source and note |
|---|---|---|---|---|---|
| 1 | messaging (token) | messaging-plugin | packages/messaging-plugin/src/plugin/messaging-plugin.ts:436 | live-state | M70c: isReady() lifecycle + isHealthy() reachability; down when not ready or unreachable, degraded when ready but unreachable and a reconnect is in flight. M90b: every broker probe is cached 5 s and bounded 2 s; the Service Bus transport probes the namespace (getNamespaceProperties(), 401/403 counts as reachable). |
| 2 | realtime-backplane | realtime-backplane-plugin | packages/realtime-backplane-plugin/src/plugin/realtime-backplane-plugin.ts:88 | live-state | M70c: transport isHealthy(); a fan-out failure is degraded (local delivery still works), never down. |
| 3 | storage | storage-plugin | packages/storage-plugin/src/plugin/storage-plugin.ts:175 | live-state | M70c: provider isHealthy() probe (HEAD/list) + lifecycle; down when unreachable. |
| 4 | mail | mail-plugin | packages/mail-plugin/src/plugin/mail-plugin.ts:140 | live-state | M70c: provider isHealthy() (SMTP verify?, SES isHealthy?) + lifecycle. H-70c-4: read through MailService.isHealthy() — now public on IMailer — so the indicator and any holder of the capability answer through one implementation. |
| 5 | queue (token) | queue-plugin | packages/queue-plugin/src/plugin/queue-plugin.ts:272 | live-state | M70c: adapter isHealthy() (PING / connection-fault / GetQueueAttributes) + lifecycle; data.reachable distinguishes up/down/unknown. M70k (X8-4): data.queues adds per-name ready/processing/dead depths where the adapter can count them cheaply, omitted where it cannot. |
| 6 | service-discovery | service-discovery-plugin | packages/service-discovery-plugin/src/plugin/service-discovery-plugin.ts:122 | live-state | M70c: #everResolved + provider probe (Consul /v1/status/leader, Kubernetes limit=1 EndpointSlice LIST); never-resolved-and-unreachable is down (X10-3 fix), stale-cache is degraded. |
| 7 | grpc | grpc-plugin | packages/grpc-plugin/src/plugin/grpc-plugin.ts:79 | configuration-literal | status: 'up' beside live data.available/serviceCount. The transport’s real health is the Health/Check RPC (mapped from the health plugin’s aggregate), so the plugin’s own indicator only reports that the plugin registered. |
| 8 | scheduler | scheduler-plugin | packages/scheduler-plugin/src/plugin/scheduler-plugin.ts:192 | configuration-literal | createHealthIndicator() reports #connected (lifecycle) only — no reachability probe. |
| 9 | audit | audit-plugin | packages/audit-plugin/src/plugin/audit-plugin.ts:149 | live-state | CLOSES H-70c-1: isReady() lifecycle + a cached, bounded sink probe — database select on a sentinel key (never an insert, which would write a fabricated record into the trail), file stat plus the last append’s outcome; memory/log report lifecycle truth. |
| 10 | cache (token) | cache-plugin | packages/cache-plugin/src/plugin/cache-plugin.ts:120 | live-state | M90b (closes H-70c-2): isReady() lifecycle + a cached, bounded reachability probe — Redis ping(); memory/noop report lifecycle truth; no probe is reachable: 'unknown', never a false true. |
| 11 | database (token) | database-plugin | packages/database-plugin/src/plugin/database-plugin.ts:161 | live-state | M90b (closes H-70c-3): uncached lifecycle-only gate (DatabaseService.isClosed, reaching no adapter), then adapter readiness through a cached, bounded probe; a Drizzle poolStats callback publishes data.capacity — data, not a threshold. |
| 12 | notification | notification-plugin | packages/notification-plugin/src/plugin/notification-plugin.ts:105 | live-state | CLOSES H-70c-4: per-channel reachability in data.reachable; one contacted-and-unreachable channel takes the indicator down. Email delegates to IMailer.isHealthy; the send-only transports report 'unknown', because probing them would deliver a real notification. |
| 13 | worker-pool | worker-pool-plugin | packages/worker-pool-plugin/src/plugin/worker-pool-plugin.ts:99 | live-state | CLOSES H-70c-5: the status derives from data.available — a runtime with no worker host rejects every run(), so it reports degraded with a reason rather than up. degraded not down because M45 registers this plugin on Workers deliberately, and /ready must stay 200 there. M70k’s data.exitDetection still reports whether a worker’s thread ending is observable. The pool counters stay data, never a threshold. |
| 14 | cloudflare | cloudflare-plugin | packages/cloudflare-plugin/src/plugin/cloudflare-plugin.ts:229 | justified-literal | Reports the billed binding read — which arms are present is a platform fact known at registration, not a backend to probe. The status is up/down on the presence of the bindings the arms need. |
| 15 | feature-flags | feature-flags-plugin | packages/feature-flags-plugin/src/plugin/feature-flags-plugin.ts:136 | live-state | Queries the provider’s status(); a provider that reports degraded (stale cache, fallback mode) is surfaced as degraded with the detail. |
| 16 | session | session-plugin | packages/session-plugin/src/plugin/session-plugin.ts:131 | live-state | service.storeHealth() — a store that reports unhealthy is down (a session store failure is the one invisible from outside). |
| 17 | static-files | static-plugin | packages/static-plugin/src/plugin/static-plugin.ts:56 | live-state | The no-fs runtime arm: degraded, justified — the plugin cannot serve files without a filesystem. |
| 18 | static-files | static-plugin | packages/static-plugin/src/plugin/static-plugin.ts:85 | live-state | The fs arm: a real fs.stat(root); down when the root is missing or not a directory. |
| 19 | sse | sse-plugin | packages/sse-plugin/src/plugin/sse-plugin.ts:78 | configuration-literal | status: 'up' beside live data.connections. The SSE service has no backend to probe; the status is decorative. |
| 20 | websocket | websocket-plugin | packages/websocket-plugin/src/plugin/websocket-plugin.ts:154 | configuration-literal | status: 'up' beside live data.available/connections/rooms/routes. No backend to probe. |
| 21 | cqrs | cqrs-plugin | packages/cqrs-plugin/src/plugin/cqrs-plugin.ts:149 | configuration-literal | status: 'up' beside live data.commands/queries handler counts. In-process bus, no backend. |
| 22 | events | events-plugin | packages/events-plugin/src/plugin/events-plugin.ts:159 | configuration-literal | status: 'up' beside live data.handlers. In-process bus, no backend. |
| 23 | graphql | graphql-plugin | packages/graphql-plugin/src/plugin/graphql-plugin.ts:210 | configuration-literal | status: 'up' beside live data.endpoint/cachedDocuments/subscriptions. The endpoint is in-process. |
| 24 | multi-tenancy | multi-tenancy-plugin | packages/multi-tenancy-plugin/src/plugin/multi-tenancy-plugin.ts:292 | configuration-literal | status: 'up' beside live data.resolver/strategy/store. In-process middleware, no backend. |
| 25 | react-router | react-router-plugin | packages/react-router-plugin/src/plugin/react-router-plugin.ts:245 | configuration-literal | status: 'up' beside live data.mode/serverBuildPath. Stateless handler (the source notes there is no socket, pool, timer, or subscription to close). |
| 26 | secrets | secrets-plugin | packages/secrets-plugin/src/plugin/secrets-plugin.ts:143 | live-state | M90b: isReady() lifecycle + a cached, bounded reachability probe — Vault /v1/sys/health (no secret read, no token); env reports lifecycle truth; a cloud facade without the optional isHealthy() member is reachable: 'unknown'. |
| 27 | view | view-plugin | packages/view-plugin/src/plugin/view-plugin.ts:77 | justified-literal | M92: rendering is stateless and touches no backend — there is nothing to probe — so up is the correct signal, and the live data carries the real fact: the selected engine (hono-jsx, hono-html, or custom). |
Out-of-scope defects recorded (not changed on this branch)
Per plan §3.9, the configuration-literal sites outside the six in-scope packages that
nonetheless hide a real backend behind a decorative status were recorded in the smoke register
(smoke/DEFECTS.md, which is deliberately not tracked in git — hence a plain reference rather than
a link) rather than fixed on that branch. All five are now closed, and the table is kept as the
record of what the audit found:
| Defect ID | Package | Issue |
|---|---|---|
| H-70c-1 | audit-plugin | CLOSED — the audit indicator now probes the sink (database sentinel read, file stat + last append outcome). |
| H-70c-2 | cache-plugin | CLOSED in M90b — the cache indicator now probes reachability (Redis ping(); unknown, never a false true). |
| H-70c-3 | database-plugin | CLOSED in M90b — the database indicator gates on uncached lifecycle behind a bounded probe and publishes capacity. |
| H-70c-4 | notification-plugin | CLOSED — per-channel reachability; email delegates to IMailer.isHealthy, send-only transports report 'unknown'. |
| H-70c-5 | worker-pool-plugin | CLOSED — the status derives from data.available; a runtime with no worker host reports degraded with a reason. |
The remaining configuration-literal rows (7, 8, 19–25) are defensible: the plugin has no
external backend — the status says “this in-process capability is registered”, and the live data
carries the useful part. They are recorded here so a future milestone can decide whether to
reclassify them, but they are not defects in the X8-5 sense (no backend is being hidden).