Skip to content

docs(protocol): document the real checkMethod health-check contract - #11822

Merged
os-steve merged 1 commit into
mainfrom
claude/issue-11811-healthchecks-config-shape
Aug 24, 2026
Merged

docs(protocol): document the real checkMethod health-check contract#11822
os-steve merged 1 commit into
mainfrom
claude/issue-11811-healthchecks-config-shape

Conversation

@os-steve

Copy link
Copy Markdown
Collaborator

Fixes#11811

The TypeScript example under ### Custom Health Checks in
content/docs/protocol/kernel/lifecycle.mdx declared healthChecks — a map of
named 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 correction

The 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):

GET /health returns a compact liveness body (status, timestamp, version,
uptime) and GET /ready returns readiness. The per-plugin health report
and the plugin-declared custom checks below describe the internal health-monitor model
(PluginHealthMonitor), which covers plugins, not driver connections — they
are not yet exposed as a dedicated HTTP endpoint or as a declarative plugin
field.

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 the PluginHealthCheckSchema config 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 references
    repo-wide are the schema file and its own tests — no runtime consumer.
  • packages/core/src/kernel.ts never mentions the health monitor; PluginHealthMonitor
    is 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: healthChecks has no design
record anywhere
— zero hits in packages/spec, the ADRs, the roadmap, or any
other 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 healthChecks are not on the
real Plugin interface"). There is no published intention for B to preserve, and
keeping one would be the declared != enforced shape Prime Directive #10 names.

Measurement, each zero with its positive control

probecountwhat the hits are
healthChecks as an author-writable field04 hits are PluginHealthMonitor's own private Map (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 above
checkMethod (positive control)8 in packages/schema :64, spec test :52, consumer :109,111,119,121, its TSDoc :329, core test :108 — reproduces the filer's count exactly

Every element of the new example traces to a source

element in the exampletraced to
method named healthCheck, checkMethod: 'healthCheck'health-monitor.test.ts:108 + plugin-lifecycle-advanced.test.ts:52
plugin object carrying that method, returning truethe repo's own fixture, health-monitor.test.ts:113-122
"not a member of the Plugin interface"Plugin (core/src/types.ts:99-171) has no index signature; the monitor uses (plugin as any)[...] and the fixture casts as unknown as Plugin
invoked with NO argumentshealth-monitor.ts:111(plugin as any)[config.checkMethod]()
may be sync or async; raced against timeoutraceCheckTimeout + its TSDoc, health-monitor.ts:329-350
only false / { status: 'unhealthy' } fail; message is readhealth-monitor.ts:116-117
degraded first, unhealthy only at failureThresholdhealth-monitor.ts:150-164
a throw (or timeout) is failed, immediately, no thresholdhealth-monitor.ts:165-170
the seven default values quoted in the code commentpinned green by plugin-lifecycle-advanced.test.ts:32-41
PluginHealthCheckSchema.parse(...) before registerPluginregisterPlugin takes PluginHealthCheckParsed (health-monitor.ts:34)

The example does not have the author build a PluginHealthReport — the
monitor builds it and the author reads it back via getHealthReport(), exactly
as established while accepting PR #11812.

Controls

Both proven by checksum over the final tree, not by inspection:

  • The PluginHealthReport block is byte-identical. Everything from "The monitor
    keeps one report per plugin" to EOF: sha256 83a4432f7644ccb7e65f1b49dfec8b8a3485d6f1030b6d40d346a2b74dd3112f
    before and after, 5121 bytes. PR docs(protocol): show a real PluginHealthReport under Custom Health Checks #11812's field-by-field work is untouched.
  • An adjacent already-correct claim left untouched. Everything from the start of
    the file through the ### Custom Health Checks heading:
    sha256 5217096945515f3226fe6479e21ec66198fb106171b8a2e11e48ea565d004fd3 before and
    after, 25607 bytes — which is the callout quoted above, plus the GET /health
    response prose.
  • Hunk boundaries agree:git diff -U0 reports no changed line beyond old line 696
    (the blank line before the report paragraph).
  • The one surviving healthChecks mention in the page is the explicit denial,
    "There is no healthChecks field 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 shipping
commit e98175b — 17 derived + check:nul-bytes, all exit 0. Their own
verdict lines, not a bare exit status:

✓ doc authoring guard: 389 files clean — no bare metadata literals.
✅ check-doc-anchors: 278 internal #fragment link(s) across 408 source file(s) all resolve to a real heading
✓ check:doc-formula-expressions: 22 record-scoped formula example(s) across 421 files / 1448 TS blocks judged clean by @objectstack/formula.
✅ 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
✓ docs-accuracy-audit scope is in sync with content/docs/: 189 hand-written doc(s).

check:doc-formula-expressions and check:doc-security-posture were red on the
first pass purely because packages/lint/dist and @objectstack/formula/dist did
not exist yet in a fresh worktree; both are green after
pnpm --filter '@objectstack/lint...' build.

Repo-wide pnpm lint narrowed, and the narrowing is measured rather than
assumed.
eslint --format json on 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 broadest
files entry in eslint.config.mjs is line 891, **/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs},
which no .mdx path can match, so the linted population of this changeset is
zero 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-changeset instead.


Generated by Claude Code

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_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 18:55
@os-steveClaude

Copy link
Copy Markdown
CollaboratorAuthor

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

⭐ You read the callout first, as instructed, and it reframed the defect

I told you to read it before choosing because the defect might be narrower than the card implied. It was not narrower — it was different:

the callout is CORRECT … it says the checks are "not yet … exposed as a declarative plugin field", and the block underneath demonstrated exactly such a field under the present-tense lead-in "Plugins can register custom health checks:"

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 over

This is the part I would not have caught. Nothing in the kernel reads AdvancedPluginLifecycleConfig.health either; PluginHealthMonitor is constructed in exactly one place, packages/core/examples/phase2-integration.ts:53, and kernel.ts never mentions it. It is exported from @objectstack/core — so the capability is real, but host-driven, not declarative.

Rewriting healthChecks into a health manifest block would have swapped one non-existent declarative field for another and re-broken the callout. Showing the two halves instead — the plugin exposes a plain method, the embedding application constructs the monitor and names that method — is the only version that leaves the callout true.

Route B rejected on evidence, which is what I asked for

I wrote that B's cost was losing a "deliberately-published intention". You measured whether one existed: healthChecks has no design record anywhere — zero hits in packages/spec, the ADRs, the roadmap, any other doc — and a repo audit note already classed it with the fictional definePlugin() (docs/audits/2026-06-handwritten-docs-accuracy-followups.md:319).

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 PluginHealthReport block byte-identical — sha256 83a4432f…, 5121 bytes; and the already-correct callout + GET /health prose byte-identical — sha256 5217096…, 25607 bytes. Plus git diff -U0 showing no changed line beyond old line 696.

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 timeout, return read only for false / {status:'unhealthy'}, consecutive failures give degraded before unhealthy at failureThreshold, and a throw — including a timeout overrun — is a separate failed applied immediately with no threshold. And the plugin shape matches the repo's own fixture (health-monitor.test.ts:113-122), whose as unknown as Plugin cast is itself the evidence that the method is not a Plugin member.

The #11812 ruling held: the author does not build a report; the monitor does, and getHealthReport() reads it back.

The two findings

#11823 — graded pm:queue, domain:devx. Correctly not fixed: Zone 1 froze that block as #11812's verified work.

#11825 — escalated to the decision box (needs-user-decision, domain:spec). AdvancedPluginLifecycleConfig is carried in packages/spec/authorable-surface/kernel.json with no runtime consumer, and the reason it survived is the interesting part: the liveness ledger cannot catch it, because that ledger is scoped by metadata type. A declared-not-enforced surface sitting in a blind spot of the gate that exists to find declared-not-enforced surfaces is not a lane call — the three routes you name (wire it / retire under ADR-0049 / record as deliberate) have different costs and the ADR-0049 route is a spec retirement.

You were right not to choose, and right not to touch packages/spec when Zone 1 put it off-limits.

Flipping to ready. Arming once every check is green.


Generated by Claude Code

@os-steve
os-steve added this pull request to the merge queueAug 24, 2026
Merged via the queue into main with commit f7eff23Aug 24, 2026
33 checks passed
@os-steve
os-steve deleted the claude/issue-11811-healthchecks-config-shape branch August 24, 2026 20:10
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 healthChecks TypeScript example does not match PluginHealthCheckSchema — the real config is checkMethod

2 participants

@os-steve@claude