You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
[Decision] The declared plugin contract does not govern what the runtime accepts: LiteKernel never runs PluginSchema, and isDefault survives the kernel that does #16721
Measured out of #16599, whose own premise turned out to be false. Two questions, filed as one card because they share a root: PluginSchema does not govern what the runtime actually accepts. Filed by the domain:services PM seat; the subject is the plugin contract, so it is routed to domain:spec.
⚠️ The measurements below were taken by the #16599 dev and independently re-measured by the seat, each with controls. Line numbers move — ⭐ re-measure before acting.
Question ①: does LiteKernel enforce the plugin contract?
LiteKernel.use() calls registerPluginByName directly. #16334 wired PluginSchema into PluginLoader.validatePluginContract only, and PluginLoader is reached only from ObjectKernel.use().
⇒ The same plugin object is accepted by one published kernel and refused by the other. Demonstrated end-to-end by the #16599 dev: a type: 'ui' plugin with no slug is refused by ObjectKernel with PLUGIN_CONTRACT_VIOLATION … PLUGIN_UI_REQUIRED_KEY_MISSING, and is accepted by LiteKernel, where it mounts a route derived from the package name.
⚠️ Why this bites in the direction that hurts
AGENTS.md assigns LiteKernel to tests. So the lenient kernel is the one plugin authors develop against, and the strict one is production. ⇒ A plugin can be green in vitest and refused at boot — the failure lands after the work is done, on the person least able to see why.
B — declare the divergence.LiteKernel stays unvalidated by design; say so in its own docblock and in the AGENTS.md Kernel table, so "tests pass" stops implying "boots". Cost: the trap remains, now written down.
C — validate in ObjectKernelBase, so both kernels inherit it and the question cannot recur on a third kernel. Cost: the largest blast radius, and it presumes the base is the right home.
① 项目长远合理性(权重 ≥50%) —— 一个声明了却不被执行的契约,是本仓反复付学费的形状。两个已发布 kernel 对同一个对象给出不同答案,意味着「插件契约」这个词今天没有单一所指。A/C 缩小特例;B 把特例写进文档、永久保留。⇒ 指向 A,而 C 是它的更彻底版本。⛔ 但 C 预设了 base 是正确的归宿,而这一点没测。
② 实际业务拉动 —— ⚠️具名但间接:今天仓内 type: 'ui' 的生产者是 0,所以这块自动发现只服务外部作者的插件。⇒ 拉动不是内部的,是「外部插件作者在测试里绿、在生产里崩」。⛔ 无法从本仓测量它已经发生过几次。这是本卡最大的置信缺口。
③ 防 AI 犯错 —— 现状最坏:宽松的那个 kernel 正是文档指定用来跑测试的那个,于是「测试通过」这个信号本身是误导的。A/C 让失败提前到作者手上;B 只让失败可解释,不让它提前。⇒ 指向 A/C。
④ 创业阶段不扩散 —— B 零改动;A 动一个 kernel 但会惊动仓内每一个 LiteKernel 测试(失败集未测);C 更宽。⇒ ④ 是唯一指向 B 的一棱,而且它的理由是实的:A 的代价现在不知道有多大。
推荐:A,但先测代价。 具体次序:先派一张只做测量的卡 —— 在本地给 LiteKernel.use() 临时接上 PluginSchema,跑全仓,数出有多少测试会红、红在哪些键上,⛔ 停在测量、不落地。那个数字出来之后 A 与 B 才是可比的。⛔ 本席不替你在 A/B/C 之间定,因为 ④ 的成本项现在是空的。
回退:B —— 若测出来的失败集很大,B 是诚实的暂缓,且不阻止以后再收敛。
Question ②: isDefault is an undeclared alias that works on the validated path
Measured
PluginSchema is a plain z.object with no .strict(), and PluginLoader.toPluginMetadata is a cast, not a copy. ⇒ isDefault: true passes through ObjectKernel.use() verbatim (stored.isDefault === true, stored.default === undefined) and drives the / redirect — the #16599 dev measured routes coming back as ['/','/dflt','/dflt','/dflt/*','/dflt/*'].
⇒ An author-writable key that works today, on the fully validated path, which the declared contract will never validate and never reject. The consumer reads plugin.default || plugin.isDefault.
Options
A — declare it: add isDefault to PluginSchema as a deprecated alias with an ADR-0087 conversion entry, so the contract describes what the runtime already honours.
B — retire it under ADR-0049 enforce-or-remove: drop the || plugin.isDefault read, add a tombstone so an author writing isDefault gets a named refusal with the prescription instead of a silent non-redirect, and ship the breaking changeset with its FROM/TO.
① —— 一个未声明但生效的别名,是「声明面窄于实际受理面」的教科书形状;两个选项都关掉它,B 关得更彻底(契约与运行时都只剩一个拼写)。指向 B。
② —— ⛔ 未测是否有人真的在写 isDefault(仓内 type: 'ui' 生产者为 0,外部不可测)。⇒ B 的破坏性代价落在一个规模未知的人群上。这是本问的置信缺口。
Measured out of #16599, whose own premise turned out to be false. Two questions, filed as one card because they share a root:
PluginSchemadoes not govern what the runtime actually accepts. Filed by thedomain:servicesPM seat; the subject is the plugin contract, so it is routed todomain:spec.Question ①: does
LiteKernelenforce the plugin contract?Measured on
origin/mainlite-kernel.tsmentionsPluginSchema/validatePluginContract/PluginLoaderkernel.ts(ObjectKernel)PluginSchemaZZZrepo-wideLiteKernelbarrel-exportedpackages/core/src/index.ts:20extends ObjectKernelBaserepo-widelite-kernel.ts:24AGENTS.mdnamesLiteKernelLiteKernel.use()callsregisterPluginByNamedirectly. #16334 wiredPluginSchemaintoPluginLoader.validatePluginContractonly, andPluginLoaderis reached only fromObjectKernel.use().⇒ The same plugin object is accepted by one published kernel and refused by the other. Demonstrated end-to-end by the #16599 dev: a
type: 'ui'plugin with noslugis refused byObjectKernelwithPLUGIN_CONTRACT_VIOLATION … PLUGIN_UI_REQUIRED_KEY_MISSING, and is accepted byLiteKernel, where it mounts a route derived from the package name.AGENTS.mdassignsLiteKernelto tests. So the lenient kernel is the one plugin authors develop against, and the strict one is production. ⇒ A plugin can be green in vitest and refused at boot — the failure lands after the work is done, on the person least able to see why.Options
LiteKernelvalidates too. ⭐ core(finding): duplicate plugin registration throws onLiteKerneland silently overwrites onObjectKernel— a fourth instance of the two-kernel semantic split #9864's precedent is that the two kernels converge, not diverge (it already converged them on duplicate-registration semantics). Cost: real and wide — everyLiteKernel-based test in this repo starts being schema-checked, and some will fail. That failure set is unmeasured today.LiteKernelstays unvalidated by design; say so in its own docblock and in the AGENTS.md Kernel table, so "tests pass" stops implying "boots". Cost: the trap remains, now written down.ObjectKernelBase, so both kernels inherit it and the question cannot recur on a third kernel. Cost: the largest blast radius, and it presumes the base is the right home.type: 'ui'的生产者是 0,所以这块自动发现只服务外部作者的插件。⇒ 拉动不是内部的,是「外部插件作者在测试里绿、在生产里崩」。⛔ 无法从本仓测量它已经发生过几次。这是本卡最大的置信缺口。推荐:A,但先测代价。 具体次序:先派一张只做测量的卡 —— 在本地给
LiteKernel.use()临时接上PluginSchema,跑全仓,数出有多少测试会红、红在哪些键上,⛔ 停在测量、不落地。那个数字出来之后 A 与 B 才是可比的。⛔ 本席不替你在 A/B/C 之间定,因为 ④ 的成本项现在是空的。回退:B —— 若测出来的失败集很大,B 是诚实的暂缓,且不阻止以后再收敛。
Question ②:
isDefaultis an undeclared alias that works on the validated pathMeasured
PluginSchemais a plainz.objectwith no.strict(), andPluginLoader.toPluginMetadatais a cast, not a copy. ⇒isDefault: truepasses throughObjectKernel.use()verbatim (stored.isDefault === true,stored.default === undefined) and drives the/redirect — the #16599 dev measured routes coming back as['/','/dflt','/dflt','/dflt/*','/dflt/*'].⇒ An author-writable key that works today, on the fully validated path, which the declared contract will never validate and never reject. The consumer reads
plugin.default || plugin.isDefault.Options
isDefaulttoPluginSchemaas a deprecated alias with an ADR-0087 conversion entry, so the contract describes what the runtime already honours.|| plugin.isDefaultread, add a tombstone so an author writingisDefaultgets a named refusal with the prescription instead of a silent non-redirect, and ship the breaking changeset with its FROM/TO.isDefault(仓内type: 'ui'生产者为 0,外部不可测)。⇒ B 的破坏性代价落在一个规模未知的人群上。这是本问的置信缺口。isDefault会静默生效,写default也生效,两者都对 —— 直到某天有人按契约收紧。B 把「写错拼写」变成具名拒绝并给出处方,这正是本仓 ADR-0049 的姿态。指向 B。推荐:B(ADR-0049 enforce-or-remove,且修复应在生产者/契约侧而非消费者的
||里)。⛔ 但这是公共契约的取舍,不是席位能定的。Refs
#16599(本卡的来源;其前提被证伪,已改判为订正散文 + 补 pin)· #16334(把⚠️ 其主题同样只死了一半 —— 在
PluginSchema接进PluginLoader的那次)· #16363 · #9864(两个 kernel 应当收敛的先例)· ADR-0049(enforce-or-remove)· ADR-0087(转换条目)· #15638(ObjectKernel上被拒,在LiteKernel上仍活)