Found while measuring #11982 (the PluginConfigValidator / configSchema measure-first card). Filed unassigned, not fixed — it is a sibling field on the same interface as that card's subject, but outside its completion scope: resolving #11982 would not touch this field.
Measured on origin/main @ 7bd6447f413d36d1f4f0112042de32beefa3e67c.
What is declared
packages/core/src/plugin-loader.ts:58, on PluginMetadata extends Plugin:
/** Whether plugin supports hot reload */
hotReloadable?: boolean;
Documented at packages/core/ADVANCED_FEATURES.md:374 — `hotReloadable?: boolean` - Whether plugin supports hot reload.
What is measured
hotReloadable has exactly 2 occurrences in the whole repo (packages, apps, examples, excluding dist/ and CHANGELOG.md), and both are the declaration and its own doc line:
packages/core/src/plugin-loader.ts:58 <- the declaration
packages/core/ADVANCED_FEATURES.md:374 <- the doc line describing it
Zero reads, and also zero declarations — no plugin in the repo sets it. In particular packages/core/src/hot-reload.ts never mentions it: HotReloadManager.reloadPlugin() (hot-reload.ts:360) gates on this.reloadConfigs.get(pluginName) — a separately registered reload config — and nothing else. A plugin declaring hotReloadable: false is hot-reloaded exactly like one declaring true, which is the stronger form of the claim: the field's own text is an assurance about reload safety.
Controls — an uncontrolled zero is not a reading
Same probe (git grep over the same pathspec, both the name: object-literal form and the name = class-property form), run against sibling optional fields of the samePluginMetadata interface:
| Field | X: + X = hits | Live read |
|---|
hotReloadable | 2 (decl + doc only) | none |
startupTimeout | 22 | packages/core/src/kernel.ts:596 — plugin.startupTimeout || this.config.defaultStartupTimeout! |
healthCheck | 104 | live |
version = / name = | 292 / 2349 | (proves the class-property = form is findable at all) |
So the probe finds sibling reads on the same interface, and finds class-property assignments generally. The zero is about this field, not about the pattern.
Why it is worth a card rather than a shrug
This is the declared ≠ enforced shape the repo has paid for repeatedly (#4212 — four of five declared plugin lifecycle hooks never invoked; #4914 — loading's ten inert keys, one of which looked like isolation). Two routes, and the choice is a real one, not prejudged here:
- Enforce it — have
reloadPlugin() refuse (or warn on) a plugin whose metadata says hotReloadable === false, making the declaration mean what it says. - Retire it under ADR-0049 enforce-or-remove — nothing declares it and nothing reads it, so removal is cheap. Note it is a TypeScript interface field, not a Zod key, so there is no
retiredKey() tombstone route and no silent-strip hazard; deletion is visible to consumers as a type error only if someone were setting it, and nobody is.
⚠️@objectstack/core re-exports this surface publicly (src/index.ts:16, export * from './plugin-loader.js'), so PluginMetadata is a published type and the field is part of a published shape. Deleting a published capability is a maintainer call, same floor as #11982 — this is filed as a finding for triage, not as a licence to remove.
Related: #11982 (same interface, configSchema, currently heading to the decision inbox with its own zero-caller measurement).
Found while measuring #11982 (the
PluginConfigValidator/configSchemameasure-first card). Filed unassigned, not fixed — it is a sibling field on the same interface as that card's subject, but outside its completion scope: resolving #11982 would not touch this field.Measured on
origin/main@7bd6447f413d36d1f4f0112042de32beefa3e67c.What is declared
packages/core/src/plugin-loader.ts:58, onPluginMetadata extends Plugin:Documented at
packages/core/ADVANCED_FEATURES.md:374—`hotReloadable?: boolean` - Whether plugin supports hot reload.What is measured
hotReloadablehas exactly 2 occurrences in the whole repo (packages,apps,examples, excludingdist/andCHANGELOG.md), and both are the declaration and its own doc line:Zero reads, and also zero declarations — no plugin in the repo sets it. In particular
packages/core/src/hot-reload.tsnever mentions it:HotReloadManager.reloadPlugin()(hot-reload.ts:360) gates onthis.reloadConfigs.get(pluginName)— a separately registered reload config — and nothing else. A plugin declaringhotReloadable: falseis hot-reloaded exactly like one declaringtrue, which is the stronger form of the claim: the field's own text is an assurance about reload safety.Controls — an uncontrolled zero is not a reading
Same probe (
git grepover the same pathspec, both thename:object-literal form and thename =class-property form), run against sibling optional fields of the samePluginMetadatainterface:X:+X =hitshotReloadablestartupTimeoutpackages/core/src/kernel.ts:596—plugin.startupTimeout || this.config.defaultStartupTimeout!healthCheckversion =/name ==form is findable at all)So the probe finds sibling reads on the same interface, and finds class-property assignments generally. The zero is about this field, not about the pattern.
Why it is worth a card rather than a shrug
This is the
declared ≠ enforcedshape the repo has paid for repeatedly (#4212 — four of five declared plugin lifecycle hooks never invoked; #4914 —loading's ten inert keys, one of which looked like isolation). Two routes, and the choice is a real one, not prejudged here:reloadPlugin()refuse (or warn on) a plugin whose metadata sayshotReloadable === false, making the declaration mean what it says.retiredKey()tombstone route and no silent-strip hazard; deletion is visible to consumers as a type error only if someone were setting it, and nobody is.@objectstack/corere-exports this surface publicly (src/index.ts:16,export * from './plugin-loader.js'), soPluginMetadatais a published type and the field is part of a published shape. Deleting a published capability is a maintainer call, same floor as #11982 — this is filed as a finding for triage, not as a licence to remove.Related: #11982 (same interface,
configSchema, currently heading to the decision inbox with its own zero-caller measurement).