Uh oh!
There was an error while loading. Please reload this page.
docs(protocol): show a real PluginHealthReport under Custom Health Checks - #11812
Conversation
…ecks
The JSON block under `### Custom Health Checks` matched neither an HTTP body
nor the internal type its lead-in named. `PluginHealthReport`
(`packages/spec/src/kernel/plugin-lifecycle-advanced.zod.ts:90-136`, built at
`packages/core/src/health-monitor.ts:185-193`) is per-plugin, and the sketch
diverged on every structural axis:
- `checks` is an array of `{ name, status: 'passed' | 'failed' | 'warning' }`
(schema:121-126), not a keyed map of subsystems whose entries read `"healthy"`
- `uptime` lives under `metrics` in milliseconds (schema:109-116,
`Date.now() - startTime`), not at top level in seconds
- no `version` field exists (0 occurrences in schema:90-136, against a control
of 5 for `status` in the same region)
The example is now a report the monitor actually builds: `status` from
`PluginHealthStatusSchema`, `metrics.uptime` in ms, and a `checks` array whose
entry is named after the configured `checkMethod` (health-monitor.ts:119-125).
`"2.0.0"` is absent because the schema has no `version` field — not because the
literal was edited to something else.
Two fields that were already correct are unchanged: `status: "healthy"` (a real
`PluginHealthStatusSchema` member) and the `timestamp` literal.
The `### Health Status Response` sample above is untouched. That one is the
`GET /health` wire body and a different thing entirely; conflating the two is
the defect this change closes. The callout's "richer per-subsystem report"
became "per-plugin health report", since the per-subsystem block it pointed at
no longer exists.
Not audited, filed separately: the TypeScript example above the block declares
`healthChecks` as a map of async functions, while the real config is
`PluginHealthCheckSchema.checkMethod` (a method name on the plugin).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015ahemw8RcTgqtxrj15PEZxos-steve
commented
Aug 24, 2026
ACCEPT. Verified by content on Two corrections to what I gave you, both mine, both verified1. My schema citation was short. I wrote Both numbers hold, and they hold over the range I should have given you rather than the one I did. 2. ⭐ My stated reason for route A was wrong about the direction, and you corrected it without letting it move the route. I justified A as "the section documents how to write custom health checks, so a reader needs the real shape." An author does not produce a constcheckResult=awaitthis.raceCheckTimeout((pluginasany)[config.checkMethod](),// ← invoked with NO arguments…if(checkResult===false||(checkResult&&checkResult.status==='unhealthy')){…checks.push({name: config.checkMethod,status: 'failed', message });The author's method returns The lean was falsified properly rather than assumedI told you to check whether a faithful example would be unwieldy, and that B was better if so. Written out it is 10 lines at nesting depth 2 — shorter than the 35-line sketch it replaces. So the condition that would have selected B is measurably absent. That is the check actually being run, not waved at. Controls⭐ The strongest one is the diff shape itself: And Field-by-field, every example field traced to both the schema line and the producer line — including The callout edit was right to make and right to flag
#11811 — graded |
Fixes#11787
Route A of the two the card offered: the JSON block under
### Custom Health Checksis now aPluginHealthReportthe monitor actually builds, instead of a per-subsystem shape nothing produces.Why A and not B
B (drop the sketch, describe it in prose) was the fallback if a faithful example turned out to be unwieldy. Written out, it is 10 lines at nesting depth 2 — shorter than the 35-line sketch it replaces — so the condition that would have selected B does not hold.
The second B-condition ("needs fields that distract from the section's subject") also fails, in the direction of A: the report's
checks[].nameis the plugin's configuredcheckMethod(packages/core/src/health-monitor.ts:119-125), so the example is the one thing on the page that connects a custom check to where it surfaces. That is the section's subject, not a distraction.One correction to the reasoning in the card's lean, which does not change the route: a plugin author does not produce a
PluginHealthReport. Their check returnsfalseor{ status: 'unhealthy' }(health-monitor.ts:116-118) and the monitor builds the report. The report is what an author reads back —getHealthReport(pluginName)(health-monitor.ts:281) — which is still a shape the page owes them.Field-by-field correspondence
Schema:
packages/spec/src/kernel/plugin-lifecycle-advanced.zod.ts(the report is lines 90-136, wider than the 90-126 the card cited —dependenciesruns to 135). Producer:packages/core/src/health-monitor.ts:185-193."status": "healthy":94status: PluginHealthStatusSchema—healthyis member 1 of 6 (:23-30):186this.healthStatus.get(pluginName) || 'unknown', set to'healthy'at:144"timestamp": "2024-01-15T11:00:00.000Z":99z.string().datetime():187new Date().toISOString()"metrics": { "uptime": 3600000 }:109-116metrics.uptime: z.number().describe('Plugin uptime in milliseconds'):189-191metrics: { uptime: Date.now() - startTime }"checks": [{ "name": "healthCheck", "status": "passed" }]:121-126z.array(z.object({ name: z.string(), status: z.enum(['passed','failed','warning']), ... })):121checks.push({ name: config.checkMethod, status: 'passed' })Every field in the example exists in the schema with that shape, and every one is emitted by the producer.
"healthCheck"is thecheckMethodvalue the repo's own tests use (plugin-lifecycle-advanced.test.ts:52,health-monitor.test.ts:108).Omitted deliberately, and the prose now says so:
message(producer leaves it undefined on the success path), the five optionalmetricsfields, anddependencies— the producer sets 0 of them (grep -c "dependencies:\|memoryUsage:" packages/core/src/health-monitor.ts= 0, against a positive control ofuptime:= 1).Controls
Fields already correct, left unchanged.
"status": "healthy"and"timestamp": "2024-01-15T11:00:00.000Z"appear as context lines in the diff, not as-/+pairs. So "the example now matches" is distinguishable from "the example was replaced wholesale" — two of its fields were right before and are byte-identical after.### Health Status Responseis untouched. The diff has exactly two hunks, at:633(callout) and:694+ (this block). TheGET /healthsample lives at:643-664, between them, and no line of it is in the diff. That sample is the wire body and a different thing entirely; conflating it with the internal model is the defect this pair of cards is about.The callout changed by one noun phrase, and had to.
:636said "The richer per-subsystem report ... below", pointing at a per-subsystem block that route A removes; leaving it would have created a fresh dangling reference. It now reads "The per-plugin health report". Everything else in the callout — including #11789'stimestampaddition — is byte-identical.The
versionliteralGone because the schema has no such field — not edited, not relocated, not "fixed". A faithful
PluginHealthReporthas nowhere to put"2.0.0". Measured:versioninplugin-lifecycle-advanced.zod.ts:90-136= 0 occurrences, against a positive control ofstatus= 5 in the same region.The prose states the absence positively ("the report carries no
versionfield — it identifies its plugin by the key it is stored under"), because theGET /healthbody two headings up does carryversionand a reader would otherwise assume symmetry.Gates
Union re-run at the final commit
350012655, all green. The 17 families are whatnode scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstackderived from the actual change set (pluscheck:nul-bytes):Quoting the gates' own verdict lines rather than a shell status:
check:doc-formula-expressionsandcheck:doc-security-posturewere red on the first pass for the unbuilt-closure reason, not a content reason (ERR_MODULE_NOT_FOUNDonpackages/lint/dist/index.js); both are green afterpnpm --filter '@objectstack/lint...' build.Both JSON fences on the page parse with
json.loads(2/2). No changeset: docs-only, releases nothing — same disposition as #11789, and the PR carriesskip-changeset.Repo-wide eslint is not owed here, and that is measured rather than assumed. eslint's own verdict on the changed file:
"File ignored because no matching configuration was supplied."—.mdxis outside the linted population (grep -c mdx eslint.config.mjs= 0, positive control\.ts= 18). Nothing in this diff can move any other file's eslint result.Out of scope, filed separately
#11811 — the TypeScript example immediately above this block declares
healthChecksas a map of async functions, while the real config isPluginHealthCheckSchema.checkMethod(a method name), invoked with no arguments. #11787 recorded that type as unaudited; it has been audited now. Not corrected here: the callout already marks the block aspirational, and choosing between "rewrite againstcheckMethod" and "keep the intended shape, mark it unimplemented" is a design question, not a mechanical fix.Generated by Claude Code