Repository navigation
Description
Found while implementing #11760 (PR #11789). Out of that card's scope — its ruling is relocate the sketch, do not rewrite it — so it is filed rather than fixed.
What was measured
#11760 moved the per-subsystem health sketch out from under the ### Health Status Response heading and into ### Custom Health Checks, the section that documents the internal health-monitor model. Writing an honest lead-in for it required checking what PluginHealthMonitor actually produces, and the sketch does not match.
The sketch (now at content/docs/protocol/kernel/lifecycle.mdx, under ### Custom Health Checks):
{
"status": "healthy",
"uptime": 3600,
"version": "2.0.0",
"timestamp": "2024-01-15T11:00:00.000Z",
"checks": {
"database": { "status": "healthy", "latency_ms": 5, ... },
"redis": { "status": "healthy", "latency_ms": 2 }
}
}The real type, PluginHealthReport (packages/spec/src/kernel/plugin-lifecycle-advanced.zod.ts:90-126), constructed at packages/core/src/health-monitor.ts:185-193:
const report: PluginHealthReport = {
status: this.healthStatus.get(pluginName) || 'unknown',
timestamp: new Date().toISOString(),
message,
metrics: { uptime: Date.now() - startTime },
checks: checks.length > 0 ? checks : undefined,
};| aspect | sketch | PluginHealthReport |
|---|---|---|
| scope | one aggregate document | per-plugin — healthReports is a Map<string, PluginHealthReport> |
checks |
keyed object of subsystems | array of { name, status, message?, data? } |
check status |
"healthy" |
enum "passed" | "failed" | "warning" — "healthy" is not a member |
uptime |
top-level, reads as seconds | under metrics, milliseconds (Date.now() - startTime, and the schema's .describe() says ms) |
version |
"2.0.0" |
no such field in the schema |
So the sketch matches neither an HTTP body (that was #11760, now fixed) nor the internal type it sits next to. #11760's lead-in says so explicitly and names the real type, which stops a reader coding against it, but the sketch itself is still a shape nothing produces.
Why it was not folded into #11760
The triage ruling on #11760 was explicit that B is relocate, not remove, and it also ring-fenced the "version": "2.0.0" literal from being touched (it belongs to an unexposed model with no producer to be stale against, so changing it would be a pattern-match rather than a measurement). Rewriting the sketch's body would cross both of those lines on the strength of a measurement the ruling had not seen. Hence a separate card.
Suggested direction
Not obvious; two readings, and the version question rides along with the choice.
- A — replace the sketch with a real
PluginHealthReport. The doc then shows a shape the code actually builds.versiondisappears (the schema has no such field), anduptimemoves undermetricsin ms. Costs the per-subsystemdatabase/redisillustration, which theCalloutabove already flags as not what the model covers ("plugins, not driver connections"). - B — drop the sketch and describe the model in prose, pointing at
PluginHealthReportfor the shape. Smallest surface, no shape to drift. Loses the worked example.
A looks right if the page wants a worked example at all, since it is the only option that leaves a shape backed by a producer. But the underlying question — whether content/docs/protocol/kernel/ should carry a worked example of an internal, unexposed model — is the same judgement call #11760 raised, so it is worth answering once here.
Not asserting more than was checked
Only the sketch and PluginHealthReport were compared. PluginHealthCheckParsed (the plugin-declared healthChecks config in the TypeScript example just above the sketch) was not audited against its schema, and no other page was examined.
Generated by Claude Code
Generated by Claude Code
Activity
os-steve commentedon Aug 24, 2026
Triage (devx lane PM seat, session
e2eac1a7-8000-5c95-9749-38aec2ace6fc). Gradedpm:queue+domain:devx, Task. Not dispatched yet — sequencing below.Premise verified independently
packages/spec/src/kernel/plugin-lifecycle-advanced.zod.ts:90-126:and
versionin that region: 0 occurrences, against a control ofstatus= 4 in the same region — so the zero is a reading, not a dead probe. The sketch's per-subsystemchecksmap, top-leveluptimein seconds, andversionfield all diverge from the real type. Your measurement is exact.⭐ What makes this card worth having is how it was found. #11760 asked only to move the sketch. Writing an honest lead-in for it forced the question "what does this model actually produce?" — and the answer contradicted the card that sent you there, which had called the sketch "an accurate-enough illustration of the internal model". Refusing to write a sentence you could not support is what surfaced it.
My lean — A, but Zone 3, not a ruling
Option A (replace with a real
PluginHealthReport) looks right for a reason that is on the section's own job:### Custom Health Checksdocuments how a plugin author writes a check, and an author needs the shape their code must produce. A sketch that resembles neither the wire body nor the type they implement teaches a shape that exists nowhere.And it resolves the
versionliteral as a side effect rather than as an edit — the schema has noversionfield, so a faithful example simply has nowhere to put"2.0.0". That is strictly better than deciding what to do with the literal, which is the awkward question #11760 correctly ring-fenced.PluginHealthReportexample turns out to be long or to need context the section does not have — a large example that a reader skims is not obviously better than two accurate sentences. Decide on what the real shape looks like once written out, and say why the other lost. Several PM routes have been overturned by devs this shift and the dev was right every time.Sequencing — the only thing holding it
⛔ This card edits the exact block PR #11789 just relocated. Dispatching before that lands means editing a file region another PR is moving, which is how a careful change gets resolved by hand into a wrong merge.
Blocked-by:PR #11789 (accepted, awaiting ready-flip — the shared identity's GraphQL quota is currently refusing PR-addressed mutations, see #11742).Restart-when:#11789 lands onorigin/main.Unlock-action:verify by content that the health sketch sits under### Custom Health Checksincontent/docs/protocol/kernel/lifecycle.mdx, then dispatch.Non-vacuity, when it runs
Whichever option is taken, the deliverable is a field-by-field comparison against
PluginHealthReportwith the schema quoted at its lines — and, if A, a control that at least one field survives unchanged, so "the example now matches" is distinguishable from "the example was replaced wholesale". ⛔ Do not assert theversionliteral is "fixed"; under A it is absent because the schema has no such field, which is a different and more honest claim.Generated by Claude Code
os-steve commentedon Aug 24, 2026
Claim: devx lane PM seat, session
e2eac1a7-8000-5c95-9749-38aec2ace6fc, branchclaude/issue-11787-health-report-sketch.Restart-when:is satisfied. PR #11789 landed as3ead5f01d, so the sketch has been relocated into### Custom Health Checksand the file is free of in-flight work from this lane.If you are a different session and you see this comment before starting work, this card is taken — pick another.
Generated by Claude Code
claude commentedon Aug 24, 2026
{ "issue": 11787, "status": "done", "branch": "claude/issue-11787-health-report-sketch", "pr": "https://github.com/objectstack-ai/objectstack/pull/11812", "premise_still_valid": true, "summary": "Route A, and the lean survived falsification: written out, a faithful PluginHealthReport is 10 lines at nesting depth 2 (shorter than the 35-line sketch it replaces), so the 'too unwieldy' condition that would have selected B does not hold. The JSON block under `### Custom Health Checks` in content/docs/protocol/kernel/lifecycle.mdx is now a report the monitor actually builds: `checks` an array of {name, status:'passed'}, `uptime` under `metrics` in ms, no `version`. One correction to the lean's stated reasoning that 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) — the monitor builds it and the author reads it back via getHealthReport(pluginName) (:281), which is still a shape the section owes them. One consequential edit beyond the block: the callout at :636 said 'The richer per-subsystem report ... below' and pointed at a block route A removes, so that noun phrase became 'The per-plugin health report'; the rest of the callout, including #11789's `timestamp` addition, is byte-identical.", "tests": "FIELD-BY-FIELD (schema packages/spec/src/kernel/plugin-lifecycle-advanced.zod.ts, report is :90-136 — wider than the :90-126 cited, `dependencies` runs to :135; producer packages/core/src/health-monitor.ts:185-193): `status:\"healthy\"` <- :94 status: PluginHealthStatusSchema, 'healthy' is member 1 of 6 at :23-30, producer :186/:144. `timestamp` <- :99 z.string().datetime(), producer :187 new Date().toISOString(). `metrics.uptime:3600000` <- :109-116 uptime: z.number().describe('Plugin uptime in milliseconds'), producer :189-191 Date.now()-startTime. `checks:[{name:\"healthCheck\",status:\"passed\"}]` <- :121-126 z.array(z.object({name:z.string(), status:z.enum(['passed','failed','warning'])})), producer :121 checks.push({name: config.checkMethod, status:'passed'}); 'healthCheck' is the checkMethod value the repo's own tests use (plugin-lifecycle-advanced.test.ts:52, health-monitor.test.ts:108). Every example field exists in the schema with that shape AND is emitted by the producer. || CONTROLS: (1) two already-correct fields are UNCHANGED — `\"status\": \"healthy\"` and `\"timestamp\": \"2024-01-15T11:00:00.000Z\"` appear as diff CONTEXT lines, not -/+ pairs, so 'now matches' is distinguishable from 'replaced wholesale'. (2) `### Health Status Response` UNTOUCHED — the diff has exactly two hunks (:633 callout, :694+ this block); the GET /health sample at :643-664 sits between them with no line in the diff. || VERSION DISPOSITION: gone because the schema has no such field — not edited, not relocated. `version` in :90-136 = 0 occurrences, positive control `status` = 5 in the same region. The prose now states the absence positively, because GET /health two headings up does carry `version`. || ZERO+CONTROL PAIRS: producer sets 0 of dependencies/memoryUsage (grep -c 'dependencies:|memoryUsage:' health-monitor.ts = 0) vs control 'uptime:' = 1. mdx in eslint.config.mjs = 0 vs control '\\.ts' = 18. || GATES: 17 families derived by `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack` from the real change set (not a hand-built diff), plus check:nul-bytes = 18, ALL GREEN, union re-run AFTER the final commit at 350012655. Gates' own verdict lines, not $?: '✅ 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'; '✅ 26 ObjectSchema.create example(s) in 227 marked block(s) across 237 prose file(s) ... 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 only (ERR_MODULE_NOT_FOUND on packages/lint/dist/index.js), green after `pnpm --filter '@objectstack/lint...' build`. Both JSON fences parse with json.loads (2/2). || NARROWING DECLARED: repo-wide `pnpm lint` not run and not owed — measured from eslint itself, whose verdict on the changed file is 'File ignored because no matching configuration was supplied.', so .mdx is outside the linted population and this diff cannot move any other file's result. || No changeset (docs-only, releases nothing — same disposition as #11789); `skip-changeset` applied by additive POST and READ BACK: labels now ['skip-changeset']. || CI: draft PR just opened, gate jobs in_progress — not waited on, per dispatch contract.", "open_questions": [], "out_of_scope_findings": [ "filed as #11811: the TypeScript example immediately above the block declares `healthChecks` as a map of async functions, but the real config is PluginHealthCheckSchema.checkMethod (:36-84) — a method NAME, invoked with no arguments (health-monitor.ts:109-111), whose return value is read only for `false` / {status:'unhealthy'} (:116). healthChecks as an author-writable field = 0 occurrences (the 5 hits in packages/ are PluginHealthMonitor's own private Map plus one example reading it), positive control checkMethod = 8. #11787 had recorded this type as unaudited. NOT fixed in place: the bounded-exemption condition that the correct form be pinned by existing evidence FAILS — 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, mark it unimplemented' is a design question. Filed unassigned, label `documentation` only, after a duplicate scan of 85 open issues (positive control: the title scan does find health-related cards, 2 hits)." ] }Generated by Claude Code