Uh oh!
There was an error while loading. Please reload this page.
docs(protocol): document the real checkMethod health-check contract - #11822
Conversation
The TypeScript example under "Custom Health Checks" declared `healthChecks`,
a map of async functions taking `({ context })`, on the plugin default export.
No such field exists: measured as an author-writable field it has 0
occurrences (the hits in packages/ are PluginHealthMonitor's own private Map
plus one example reading it). The real configuration is
PluginHealthCheckSchema.checkMethod — a method NAME, invoked with no
arguments, whose return is read only for `false` / `{ status: 'unhealthy' }`.
Rewrite the example against that contract, in the two halves it actually has:
the plugin exposes a plain method, and the embedding application constructs
PluginHealthMonitor and names the method in the parsed config. No declarative
plugin field is shown, which keeps the section's callout ("not yet ... as a
declarative plugin field") true — that callout was verified correct and is
left untouched, as is the PluginHealthReport block below.
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 ⭐ You read the callout first, as instructed, and it reframed the defectI told you to read it before choosing because the defect might be narrower than the card implied. It was not narrower — it was different:
So the page was not aspirational-but-imprecise. It was self-contradictory: a callout saying the field does not exist, immediately above a present-tense example of that field. The defect is not "the example names the wrong field" — it is "the example showed a declarative plugin field at all." ⭐ And route A as the card sketched it would have reproduced the error one field overThis is the part I would not have caught. Nothing in the kernel reads Rewriting Route B rejected on evidence, which is what I asked forI wrote that B's cost was losing a "deliberately-published intention". You measured whether one existed: There was no intention for B to preserve. That converts a judgement call into a measurement, which is the right way to close one. Controls by checksum, not inspection⭐ The I asked you to prove the #11812 block untouched by hunk boundaries, not by inspection. Hashing two spans and pinning the diff's line extent is stronger than what I asked for. On a page where the whole defect class is confusing two adjacent samples, that is the right instrument. Contract verified end to end, including the parts the card did not name: invoked with no arguments, may be sync or async and is raced against The #11812 ruling held: the author does not build a report; the monitor does, and The two findings#11823 — graded ⭐ #11825 — escalated to the decision box ( You were right not to choose, and right not to touch Flipping to ready. Arming once every check is green. Generated by Claude Code |
Fixes#11811
The TypeScript example under
### Custom Health Checksincontent/docs/protocol/kernel/lifecycle.mdxdeclaredhealthChecks— a map ofnamed async functions taking
({ context })— on the plugin default export.No such field exists anywhere in the runtime.
Route taken: rewrite against
checkMethod(route A), with one correctionThe card offered two routes and ruled neither. I took A (rewrite against the
real contract), but not in the form the card sketched, because reading the
callout first changed what "the real contract" is.
The callout, quoted in full (it sits immediately above the section and is
unchanged by this PR):
That callout is correct, and I verified it rather than assuming it. It is
also what made the page self-contradictory: it says there is no declarative
plugin field, and the block underneath it then demonstrated one, under the
present-tense lead-in "Plugins can register custom health checks:". A reader
following the example writes a key that nothing reads and nothing rejects.
So the defect is not "the example names the wrong field". It is that the example
showed a declarative plugin field of any kind. Route A as sketched — "a
healthCheck()method plus thePluginHealthCheckSchemaconfig that names it" —would have reproduced that error one field over if the config were shown sitting
on the manifest, because nothing in the kernel reads that config either:
AdvancedPluginLifecycleConfigSchema.health(plugin-lifecycle-advanced.zod.ts:435)is the only container for
PluginHealthCheckSchema, and its only referencesrepo-wide are the schema file and its own tests — no runtime consumer.
packages/core/src/kernel.tsnever mentions the health monitor;PluginHealthMonitoris constructed in exactly one place,
packages/core/examples/phase2-integration.ts:53.PluginHealthMonitoris exported from@objectstack/core(src/index.ts:76),so the capability is real — it is just host-driven, not declarative. The
example now shows those two halves and no manifest field, which is what keeps
the callout true.
Route B was rejected on evidence, not taste:
healthCheckshas no designrecord anywhere — zero hits in
packages/spec, the ADRs, the roadmap, or anyother doc. A repo audit note already classed it with the fictional
definePlugin()(docs/audits/2026-06-handwritten-docs-accuracy-followups.md:319:"the documented manifest fields ... and top-level
healthChecksare not on thereal Plugin interface"). There is no published intention for B to preserve, and
keeping one would be the
declared != enforcedshape Prime Directive #10 names.Measurement, each zero with its positive control
healthChecksas an author-writable fieldPluginHealthMonitor's own privateMap(health-monitor.ts:19,35,51,301); 1 reads that private map (examples/phase2-integration.ts:151); 1 was this doc block; 1 is the audit note abovecheckMethod(positive control)packages/:64, spec test:52, consumer:109,111,119,121, its TSDoc:329, core test:108— reproduces the filer's count exactlyEvery element of the new example traces to a source
healthCheck,checkMethod: 'healthCheck'health-monitor.test.ts:108+plugin-lifecycle-advanced.test.ts:52truehealth-monitor.test.ts:113-122Plugininterface"Plugin(core/src/types.ts:99-171) has no index signature; the monitor uses(plugin as any)[...]and the fixture castsas unknown as Pluginhealth-monitor.ts:111—(plugin as any)[config.checkMethod]()timeoutraceCheckTimeout+ its TSDoc,health-monitor.ts:329-350false/{ status: 'unhealthy' }fail;messageis readhealth-monitor.ts:116-117degradedfirst,unhealthyonly atfailureThresholdhealth-monitor.ts:150-164failed, immediately, no thresholdhealth-monitor.ts:165-170plugin-lifecycle-advanced.test.ts:32-41PluginHealthCheckSchema.parse(...)beforeregisterPluginregisterPlugintakesPluginHealthCheckParsed(health-monitor.ts:34)The example does not have the author build a
PluginHealthReport— themonitor builds it and the author reads it back via
getHealthReport(), exactlyas established while accepting PR #11812.
Controls
Both proven by checksum over the final tree, not by inspection:
PluginHealthReportblock is byte-identical. Everything from "The monitorkeeps one report per plugin" to EOF:
sha256 83a4432f7644ccb7e65f1b49dfec8b8a3485d6f1030b6d40d346a2b74dd3112fbefore and after, 5121 bytes. PR docs(protocol): show a real PluginHealthReport under Custom Health Checks #11812's field-by-field work is untouched.
the file through the
### Custom Health Checksheading:sha256 5217096945515f3226fe6479e21ec66198fb106171b8a2e11e48ea565d004fd3before andafter, 25607 bytes — which is the callout quoted above, plus the
GET /healthresponse prose.
git diff -U0reports no changed line beyond old line 696(the blank line before the report paragraph).
healthChecksmention in the page is the explicit denial,"There is no
healthChecksfield to declare".Verification
Gate families derived mechanically (
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack, no hand-built path list), re-run at the shippingcommit e98175b — 17 derived +
check:nul-bytes, all exit 0. Their ownverdict lines, not a bare exit status:
check:doc-formula-expressionsandcheck:doc-security-posturewere red on thefirst pass purely because
packages/lint/distand@objectstack/formula/distdidnot exist yet in a fresh worktree; both are green after
pnpm --filter '@objectstack/lint...' build.Repo-wide
pnpm lintnarrowed, and the narrowing is measured rather thanassumed.
eslint --format jsonon the changed file returns 1 result, 0 errors,and the single message
File ignored because no matching configuration was supplied.— eslint's own config resolution, not my reading of it. The broadestfilesentry ineslint.config.mjsis line 891,**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs},which no
.mdxpath can match, so the linted population of this changeset iszero files. No type-aware linting applies, and the diff touches no eslint
config, so no untouched file's verdict can move either.
Docs-only, so no changeset; carries
skip-changesetinstead.Generated by Claude Code