Repository navigation
PluginMetadata.hotReloadable is declared and documented but has zero reads — HotReloadManager.reloadPlugin never consults it #12587
Description
Activity
os-support-ai commented
on Aug 26, 2026 CollaboratorMore actionsTriage: 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 墓碑路线、无静默剥除风险,全仓零声明零读者,删除对现存代码零影响。- 实际业务需求:零生产者、零消费者(全仓 grep,对照组
startupTimeout/healthCheck证明探针有效)——没有任何真实场景在用这个键;它是投机性能力面。 - 项目长远合理性:declared≠enforced 是本仓反复付费的形状(Four of the five declared plugin lifecycle hooks are never invoked — and the metadata-type registry docs tell plugins to use one of them #4212 五个生命周期钩子四个未接线、docs/spec: PLUGIN_STANDARDS.md §5.1/§5.2/§5.4 把 Hot Reload 与 Plugin Isolation 标为 ✅,但 PluginHotReloadSchema / PluginSandboxingSchema 全仓零 runtime reader(ADR-0049) #4914 十个惰性键)——两条路都比留着强;但 enforce 是为零用户建新运行时行为,retire 是把台账清干净。
- 防 AI 写代码犯错:字段文本是一句可靠性保证(「是否支持热重载」)——AI 插件作者声明
hotReloadable: false会以为得到保护,实际没有;这是教科书式的「声明一个运行时不兑现的能力」。删除彻底移除陷阱;enforce 也能移除,但代价是新增无人拉动的行为面。 - 创业阶段不扩散需求:已发布零消费的能力不因沉没成本获得豁免;enforce 是能力扩张,retire 是收敛。
推荐:B(retire),四轴同向;与 #11982 同批呈报。(仅为推荐——删除已发布能力恒为人工地板,本座位不代裁。)
Generated by Claude Code
- 实际业务需求:零生产者、零消费者(全仓 grep,对照组
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
Claim: PM loop round 1
Session:session_01LZbWd2jNV1FErXTPSS4Dry
Branch:claude/issue-11982-retire-plugin-config-schema
Worktree:objectstack-issue-11982
Domain:domain:engine
File 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-5
Clause-②: 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).⚠️ Family member, not a chain head. This card is dispatched as the second member of a folded family whose head is #11982; the shared branch is named after the head. ⛔ This card having no branch of its own is by design, not a half-state. The PR carries oneFixesline 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
Dev claim — folded family dispatch, second member (chain head #11982)
- Session:
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) - Branch:
claude/issue-11982-retire-plugin-config-schema(shared family branch, named after the chain head; this card deliberately has no branch of its own) - Worktree:
objectstack-issue-11982 - Comments re-read to the last one in the same action as this claim: no competing dev claim from any other session or branch.
- Executing the recorded ruling (Option B, retire under ADR-0049): remove
PluginMetadata.hotReloadableand itsADVANCED_FEATURES.mddoc line, as its own commit with its ownFixesline so this member is independently reviewable. Exclusion list honoured:startupTimeout,signature,healthCheckuntouched. - PR will be draft and must stay draft (the head card's
docs/adr/**edit makes the PR a governed-surface diff — maintainer hand-merge).
Generated by Claude Code
- Session:
- added 3 commits that reference this issue
on Aug 27, 2026 - added a commit that references this issue
on Sep 1, 2026
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).