Repository navigation
Description
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 same PluginMetadata 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 sayshotReloadable === 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).
Activity
os-support-ai commentedon Aug 26, 2026
Triage: graded — into the decision inbox (
needs-user-decision, keepsdomain:engine), type Task. Both routes touch the human floor: retiring the field deletes a published capability (PluginMetadatais re-exported from@objectstack/core), and enforcing it adds new refuse/warn behaviour toreloadPlugin()on a public surface with zero current users. Companion to #11982 (same interface, same zero-caller shape) — recommend ruling the two in one sitting, but each keeps its own card and verdict.四棱卡面块(决策待裁)
选项 A — enforce:让
HotReloadManager.reloadPlugin()对hotReloadable === false的插件拒绝(或警告),使声明兑现。选项 B — retire:按 ADR-0049 enforce-or-remove 删除该字段(接口成员 +
ADVANCED_FEATURES.md:374文档行);TS 接口字段,无 zod 墓碑路线、无静默剥除风险,全仓零声明零读者,删除对现存代码零影响。startupTimeout/healthCheck证明探针有效)——没有任何真实场景在用这个键;它是投机性能力面。hotReloadable: false会以为得到保护,实际没有;这是教科书式的「声明一个运行时不兑现的能力」。删除彻底移除陷阱;enforce 也能移除,但代价是新增无人拉动的行为面。推荐:B(retire),四轴同向;与 #11982 同批呈报。(仅为推荐——删除已发布能力恒为人工地板,本座位不代裁。)
Generated by Claude Code
os-zhuang commentedon Aug 27, 2026
Maintainer ruling recorded — Option B: retire
hotReloadableunder ADR-0049Provenance: maintainer, 2026-08-27, PM chat decision-inbox batch 5 (adjudication session
session_01DKWDdUJ2XNRESVVWUvcpnh), verbatim: 「同意」 — accepting recommendation B (all four facets same direction), presented paired with #11982.Ruled: remove
PluginMetadata.hotReloadable(plugin-loader.ts:58) and its doc line (ADVANCED_FEATURES.md:374). Zero declarations, zero reads, controls fired (siblingstartupTimeoutread live) — the field's own text is a reload-safety assurance the runtime has never honoured, the exact "declare a capability the runtime does not deliver" shape the platform refuses. ⛔ A (adding refuse/warn behaviour toreloadPlugin()for zero users) declined.Dispatch notes: Clause-②: yes — removes a member from a published type (
PluginMetadatare-exported from@objectstack/core); contract-review tier. May fold with #11982's retirement into one family dispatch if the five fold gates pass; each card keeps its ownFixesline and its own pin (a declaredhotReloadableno longer type-checks — compile-time assertion, proven able to fail).State:
needs-user-decision→pm:queue.Generated by Claude Code
os-zhuang commentedon Aug 27, 2026
Claim: PM loop round 1
Session:
session_01LZbWd2jNV1FErXTPSS4DryBranch:
claude/issue-11982-retire-plugin-config-schemaWorktree:
objectstack-issue-11982Domain:
domain:engineFile surface:
packages/core/src/plugin-loader.ts(thePluginMetadatadeclaration), theHotReloadManagermodule and its test (stop on breach; explain in the report)Container & model: M family dispatch,
mode:subagent,model: claude-fable-5Clause-②: yes
Serial constraints cleared: see the chain head — full serial-constraint check lives on #11982's claim (
packages/core/src/security/index.tsoverlaps PR #12652, sequenced not blocked).Fixesline per member and one commit per member, so this card is independently reviewable and cannot be silently dropped from the batch.The ruling — ⛔ not re-adjudicable
Maintainer, 2026-08-27, decision-inbox batch 5, verbatim, ⛔ untranslated:
Ruled B — retire under ADR-0049, adjudicated in the same batch as #11982 and recorded on that card: "Companion #12587 (same interface,
hotReloadable) is ruled B in the same batch."The defect, as measured by the filing seat
PluginMetadata.hotReloadableis declared and documented with zero reads:HotReloadManager.reloadPlugin()gates only on its ownreloadConfigsmap, so a plugin declaringhotReloadable: falseis hot-reloaded identically to one declaringtrue. That is the declared-not-enforced shape — a plugin author is offered a switch that is not wired to anything.⛔ Confirm or refute on the current ref before writing code. The zero-read claim is the entire basis of the ruling; if
hotReloadablehas acquired a reader since it was measured, stop and report rather than deleting a live gate.Control, and it fired: the sibling
startupTimeoutmeasureskernel.ts4 hits /kernel.test.ts8 — read live. ⛔startupTimeout,signatureandhealthCheckare on the exclusion list: they are not in this family and must not be touched.🚨 Governed surface — draft only
The head card's ADR-0025 edit puts this PR on a governed surface (
docs/adr/**). ⛔ Never flip ready, never enqueue, never auto-merge. It stays draft for maintainer hand-merge.Generated by Claude Code
claude commentedon Aug 27, 2026
Dev claim — folded family dispatch, second member (chain head #11982)
session_01LZbWd2jNV1FErXTPSS4Dry— dev seat (os-devsubagent,mode:subagent; the PM claim above is the dispatching seat of the same session, so the session ID matches by design — this comment records that a dev ran)claude/issue-11982-retire-plugin-config-schema(shared family branch, named after the chain head; this card deliberately has no branch of its own)objectstack-issue-11982PluginMetadata.hotReloadableand itsADVANCED_FEATURES.mddoc line, as its own commit with its ownFixesline so this member is independently reviewable. Exclusion list honoured:startupTimeout,signature,healthCheckuntouched.docs/adr/**edit makes the PR a governed-surface diff — maintainer hand-merge).Generated by Claude Code
feat(core): retire PluginMetadata.hotReloadable under ADR-0049 (#12587)
test(core,rest): host the retirement compile pins in a compiled progr…
docs: align the hand-written plugin docs with the ADR-0049 retirements (
feat(core): retire the inert PluginMetadata surfaces — configSchema w…