Skip to content

docs(protocol): show a real PluginHealthReport under Custom Health Checks - #11812

Merged
os-steve merged 1 commit into
mainfrom
claude/issue-11787-health-report-sketch
Aug 24, 2026
Merged

docs(protocol): show a real PluginHealthReport under Custom Health Checks#11812
os-steve merged 1 commit into
mainfrom
claude/issue-11787-health-report-sketch

Conversation

@claude

@claudeclaudeBot commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

Fixes#11787

Route A of the two the card offered: the JSON block under ### Custom Health Checks is now a PluginHealthReport the 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[].name is the plugin's configured checkMethod (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 returns false or { status: 'unhealthy' } (health-monitor.ts:116-118) and the monitor builds the report. The report is what an author reads backgetHealthReport(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 — dependencies runs to 135). Producer: packages/core/src/health-monitor.ts:185-193.

example fieldschemaproducer
"status": "healthy":94status: PluginHealthStatusSchemahealthy is 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 the checkMethod value 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 optional metrics fields, and dependencies — the producer sets 0 of them (grep -c "dependencies:\|memoryUsage:" packages/core/src/health-monitor.ts = 0, against a positive control of uptime: = 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 Response is untouched. The diff has exactly two hunks, at :633 (callout) and :694+ (this block). The GET /health sample 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.:636 said "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's timestamp addition — is byte-identical.

The version literal

Gone because the schema has no such field — not edited, not relocated, not "fixed". A faithful PluginHealthReport has nowhere to put "2.0.0". Measured: version in plugin-lifecycle-advanced.zod.ts:90-136 = 0 occurrences, against a positive control of status = 5 in the same region.

The prose states the absence positively ("the report carries no version field — it identifies its plugin by the key it is stored under"), because the GET /health body two headings up does carry version and a reader would otherwise assume symmetry.

Gates

Union re-run at the final commit 350012655, all green. The 17 families are what node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack derived from the actual change set (plus check:nul-bytes):

check:cross-package-test-inputs check:doc-anchors check:doc-authoring
check:doc-formula-expressions check:doc-security-posture check:docs-audit-scope
check:docs-redirects check:empty-state check:liveness check:published-readme-links
check:react-page-adapter-contract check:role-word check:strictness-ledger
check:variant-docs check-cross-package-test-inputs.mjs check-doc-frontmatter.mjs
check-section-landing-index.mjs check:nul-bytes

Quoting the gates' own verdict lines rather than a shell status:

✅ check-doc-anchors: 278 internal #fragment link(s) across 408 source file(s) all resolve to a real heading
✓ doc authoring guard: 389 files clean — no bare metadata literals.
✅ 26 ObjectSchema.create example(s) in 227 marked block(s) across 237 prose file(s) in 2 root(s) carry an os validate-clean security posture
✓ check-doc-frontmatter: 403 page(s) under content/docs parse with yaml@2.9.0 ...
check-nul-bytes: OK (scanned 6589 text file(s) ... no raw ASCII control bytes).

check:doc-formula-expressions and check:doc-security-posture were red on the first pass for the unbuilt-closure reason, not a content reason (ERR_MODULE_NOT_FOUND on packages/lint/dist/index.js); both are green after pnpm --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 carries skip-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.".mdx is 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 healthChecks as a map of async functions, while the real config is PluginHealthCheckSchema.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 against checkMethod" and "keep the intended shape, mark it unimplemented" is a design question, not a mechanical fix.


Generated by Claude Code

…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_015ahemw8RcTgqtxrj15PEZx
@claudeclaudeBot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Aug 24, 2026
@github-actionsgithub-actionsBot added size/s documentation Improvements or additions to documentation labels Aug 24, 2026
@os-steve
os-steve marked this pull request as ready for review August 24, 2026 17:43
@os-steveClaude

Copy link
Copy Markdown
Collaborator

ACCEPT. Verified by content on origin/main @ 7e8393262.

Two corrections to what I gave you, both mine, both verified

1. My schema citation was short. I wrote :90-126. The report runs to :136dependencies occupies :130-135 and the object closes at :136. My range cut off a whole field. Your version-disposition reading is over the wider, correct region:

version in :90-136 : 0 control status : 5

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 PluginHealthReport. From health-monitor.ts:109-120:

constcheckResult=awaitthis.raceCheckTimeout((pluginasany)[config.checkMethod](),// ← invoked with NO argumentsif(checkResult===false||(checkResult&&checkResult.status==='unhealthy')){checks.push({name: config.checkMethod,status: 'failed', message });

The author's method returns false or {status:'unhealthy'}; the monitor builds the report and pushes the checks entry. The shape is still owed to the reader — via getHealthReport(pluginName) at :281, as a read-back shape — so A stands. But it stands for a different reason than the one I gave, and separating "the route is right" from "the PM's reason is right" is the distinction that keeps a lean from quietly becoming an unexamined rule.

The lean was falsified properly rather than assumed

I 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: "status": "healthy" and "timestamp": "2024-01-15T11:00:00.000Z" appear as context lines, not -/+ pairs. That is what makes "the sample now matches the producer" distinguishable from "the sample was replaced wholesale and happens to match" — the two fields that were already right were never touched.

And ### Health Status Response is untouched, proven structurally: the diff has exactly two hunks (:633 callout, :694+ the block) and the GET /health sample at :643-664 sits between them with no line in the diff. Confusing those two samples is the entire defect this pair of cards is about, so proving separation by hunk boundaries rather than by inspection is the right instrument.

Field-by-field, every example field traced to both the schema line and the producer line — including 'healthCheck' being the checkMethod value the repo's own tests use (plugin-lifecycle-advanced.test.ts:52, health-monitor.test.ts:108) rather than an invented name.

The callout edit was right to make and right to flag

:636 said "The richer per-subsystem report … below" and pointed at a block route A removes. Leaving it would have made the page reference something no longer there. Narrowing the change to the noun phrase, and confirming the rest — including #11789's timestamp addition — byte-identical, is the correct scope: a dangling reference is part of the edit, not a drive-by.

#11811 — graded pm:queue + domain:devx, and your refusal to fold it in was right

The TypeScript example declares healthChecks as a map of async functions; the real config is PluginHealthCheckSchema.checkMethod — a method name, invoked with no arguments. I confirmed checkMethod = 8 occurrences.

You declined the in-place fix on a condition, not on effort: the callout already marks the block aspirational ("not yet … as a declarative plugin field"), so choosing between rewrite against checkMethod and keep the intended shape and mark it unimplemented is a design question, and the bounded-exemption condition that the correct form be pinned by existing evidence fails. That is exactly the right test, and it is the second time today a dev has declined a fix they could have written because the exemption's conditions were not met. Filing it unassigned with a duplicate scan that had a positive control is the rest of the job done properly.

Flipping to ready. Arming once every check is green.


Generated by Claude Code

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/sskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

lifecycle.mdx's internal health-report sketch does not match PluginHealthReport

2 participants

@os-steve@claude