feat(core): LiteKernel.use() enforces the declared plugin contract, converged with ObjectKernel (#16721 step 2) - #16827
Conversation
…tep 2) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016N6xmWt5hYm94ffVEwGH8x
… prose Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016N6xmWt5hYm94ffVEwGH8x
📓 Docs Drift CheckThis PR changes 2 package(s): 6 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 1 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 39 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin b04750b8f51f46099590d810c3e0adccd5f4d71b && git checkout b04750b8f51f46099590d810c3e0adccd5f4d71b
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 7862fb711be2819b767fa28b05a036fc1172a50a 8443166a602fd34c6be296eab9446f624734bdbf && git checkout -B drift-repro 7862fb711be2819b767fa28b05a036fc1172a50a && git merge --no-ff 8443166a602fd34c6be296eab9446f624734bdbf
node scripts/docs-audit/affected-docs.mjs --json 7862fb711be2819b767fa28b05a036fc1172a50a
|
… kernel.use() enforces the contract on both kernels Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016N6xmWt5hYm94ffVEwGH8x
Contract review (
|
Contract review — VERDICT: PASS (at
|
| # | Item | Verdict | Measurement |
|---|---|---|---|
| 1 | One refusal, one shape | PASS (boundary named below) | (a) Faithful: mechanical diff of loader@base L521–547 (dedented) vs plugin-contract.ts@Head L151–175 — only two differences: the signature line and a two-line comment moved into the docblock. safeParse L152, version issues dropped L157, first remaining issue L162, code on the property L173 and at the head of the message L170. (b) One statement: product callers of assertPluginContract are lite-kernel.ts:73 and plugin-loader.ts:436 (reached from loadPlugin L178 ← kernel.ts:203). Non-test PluginSchema.safeParse|parse callers: plugin-contract.ts:152 only. Non-test literal stamp of 'PLUGIN_CONTRACT_VIOLATION': plugin-contract.ts:139 only. registerPluginByName callers: kernel.ts:210, lite-kernel.ts:75 only; extends ObjectKernelBase: LiteKernel only; plugins.set( in core non-test src: 0 (control: the comment at plugin-registration.ts:19 matched). (c) Byte parity holds structurally and is pinned: kernel.ts:206 builds Failed to load plugin: ${plugin.name} - ${result.error?.message}; loadPlugin's catch stores the thrown error unwrapped (L202–206); toPluginMetadata L391–401 casts the same object, so name/id are identical on both kernels. Pinned with toBe in core G "PARITY" and hono F1/F2. |
| 2 | validatePluginStructure stayed loader's own |
PASS | Base L420–432 and head L403–415 textually identical; call order L175 → L178 → L181 unchanged; the lite-kernel.ts diff adds only the one call. Keeping it is right: the ruling is about PluginSchema; moving name/init/semver onto LiteKernel would refuse more than the ruled set, and isValidSemanticVersion's wider grammar is pinned by plugin-loader.test.ts. |
| 3 | Section F rewritten, not retired | PASS | F0 unchanged: F0's it body with comments stripped is identical base vs head (13 lines each, diff exit 0). F1/F2 bind on the wiring: bootLite L224–230 calls kernel.use(fixture); refusal() L138–145 throws expected kernel.use() to refuse the plugin, but it loaded when nothing throws. F1/F2 assert PLUGIN_CONTRACT_VIOLATION, at 'slug'/at 'staticPath', PLUGIN_UI_REQUIRED_KEY_MISSING, .code on the property, and toBe byte parity against boot() on ObjectKernel. By reading: remove lite-kernel.ts:73 and F1/F2 must fall while F0 stays green. Not "fixed into passing". |
| 4 | type: 'service' fixture line |
PASS | Repo-wide type: *['"]service['"]: base 2 hits, head 1 (an isAppPluginLike argument — not a registered plugin). .type reads in the fixture at head: 0 code reads (file 576 lines). 'service' ∉ 'standard' + CORE_PLUGIN_TYPES (plugin.zod.ts L89–97, enum L168–171) ⇒ already refused by ObjectKernel at 'type'. |
| 5 | Accept-set movement | PASS — narrows only | (i) lite-kernel.ts:73: LiteKernel.use() now refuses what PluginSchema refuses minus version — exactly ObjectKernel's set. (ii) Loader delegation: body identical ⇒ ObjectKernel set unchanged. (iii) PluginMetadata→Plugin parameter: type-level only. (iv) Fixture, docs, types.ts comment, vocabulary row, tests: no runtime effect. Still accepted on both: unknown keys (no .strict(), L166), version any form, absent type. No widening found. |
| 6 | Changeset | PASS | @objectstack/core: minor + **BREAKING** + the adr-0087 not-required (no-migration-prescription) marker. Convention per scripts/check-changeset-no-major.mjs: during the launch window breaking changes ship as minor; precedent .changeset/enforce-plugin-schema-at-kernel-use.md (the ObjectKernel half) has the same shape. Gates at the PR SHA, exit captured before any pipe: check-changeset-no-major 0, check-adr-0087-registration 0, check-empty-changeset 0. Diff touches no packages/spec path; no key/schema/stored representation moves. |
| 7 | Docs | PASS | anatomy.mdx +L72 (slug "must provide") true per the superRefine L181–200; +L74 and +L263 (both kernels refuse at kernel.use(), "validated, never replaced") true. Its example plugin (L15–21, no type) is accepted. Five other pages read and left alone: packages.mdx:44, events.mdx:34,38, actions-as-tools.mdx:129–134, natural-language-queries.mdx:40–45 (MCPServerPlugin, type = 'standard' as const at packages/mcp/src/plugin.ts:250 ⇒ accepted), skills-reference.mdx:46,64. None shows a now-refused plugin or claims LiteKernel skips validation. |
| 8 | content/docs/releases/** absent |
PASS | git diff --name-status merge-base..head: 10 files, none under content/docs/releases/ (control: content/docs/plugins/anatomy.mdx present). v15.mdx's only LiteKernel line is L505, inside the kernel:bootstrapped bullet — a hook, not validation; PluginSchema|kernel.use()|plugin contract|PLUGIN_CONTRACT in v15.mdx: 0. No docs-only card owed. |
| 9 | AGENTS.md / governed paths untouched |
PASS | GOVERNED_SURFACES (scripts/pm/check-governed-merges.mjs): docs/adr/**, .claude/**, skills/**, AGENTS.md, CLAUDE.md. None of the 10 diff paths matches; AGENTS.md absent. Fences: body opens Part of #16721, no Fixes/Closes; isDefault in diff 0 (control: default 7); Clause-②: yes present. |
| 10 | Ablation / self-assessment | PASS by reading | Ablation NOT MEASURED (reviewer is read-only). By reading, the reported reds are exactly the ones that must fall: in core group G six cases route through refusalSync(() => kernel.use(...)) and throw "but it loaded" without lite-kernel.ts:73; the other eight G cases and the 20 A–F cases do not depend on it ⇒ 6 red / 28 green is the required signature. Hono: F1/F2 depend, F0 does not. The cli option-b self-assessment is correct and is upheld: the recorder now has no type ⇒ PluginSchema accepts it (.default('standard')), so kernel.use() returns with or without the check; option-b-reader-acceptance.pin.test.ts has 0 refusal assertions (control: 7 it( blocks at L207–341) ⇒ 7 pass under both wirings. It pins nothing about the wiring — its evidence is step 1's reverse reading. |
| 11 | Vocabulary row + doc-authoring | PASS | Row file: 'packages/core/src/plugin-contract.ts', shape: 'assignconst'; plugin-contract.ts:139 is const PLUGIN_CONTRACT_VIOLATION_CODE = 'PLUGIN_CONTRACT_VIOLATION';. Gates at the PR SHA: check-dispatcher-error-vocabulary.mjs 0, check-doc-authoring.mjs 0. Caveat: main legs only, not --self-test. Commit cb787324f removed #16721) from the runtime why string; at head the only tracker ids near the row are comments (L802 [#16049], L805 #16721), none inside the string. |
| — | CI | NOT MEASURED — PM-held gate | Not polled, no timer, no watcher. |
Boundary named for row 1 (a probe, not agreement). "One refusal, one shape" holds for the schema refusal and stops exactly at the loader's structural fence: ObjectKernel runs validatePluginStructure (name/init/semver) before the contract (L175 → L178); LiteKernel never runs it. For an input failing both (no init + bad type) the kernels still differ (Plugin init function is required vs PLUGIN_CONTRACT_VIOLATION … at 'type'), and an input failing structure only is refused on ObjectKernel and accepted on LiteKernel. That is the divergence the ruling deliberately kept (row 2), the PR states it, and it is pre-existing — but it is the precise edge of the claim. Two smaller pre-existing carriers: the code property survives on LiteKernel only (message head identical), and toPluginMetadata writes version='0.0.0' onto a version-less object on the ObjectKernel path (L396–397; no effect on the contract message).
Non-blocking notes. packages/runtime/src/dispatcher-error-vocabulary.ts changes a registry row (file path + why prose) with no changeset of its own; the fixed group versions in lockstep and check-empty-changeset passed, so nothing is owed. The last commit 8443166a6 touches only content/docs/plugins/anatomy.mdx (1 file, +3/−1), as the PR body states.
PM disposition. needs:contract-review cleared from this PR on the strength of this verdict. This PR is not governed (row 9 measured it against the register), so it is eligible to be flipped ready and enqueued once every check on 8443166a6 reads completed with success/skipped — three were still in_progress at 09:13Z, so the flip waits on them, not on question ②. Question ② of #16721 (retire isDefault under ADR-0049) is untouched by this diff (isDefault count 0) and remains a separate PR.
Generated by Claude Code
Part of #16721 — question ① step 2 only. Question ② (
isDefault) is fenced out of this PR and #16721 remains open.Clause-②: yes
What this does
LiteKernel.use()now runs the same declared-plugin-contract checkObjectKernel.use()has run since thekernel.use()enforcement release — and it is now literally the same check. The measured divergence on #16721 was thatPluginSchemawas run byPluginLoader.validatePluginContractonly, andPluginLoaderis reached fromObjectKernel.use()alone;LiteKernel.use()wrote the object straight into its registry. So one plugin object got two answers from two published kernels, and the lenient one was the kernelAGENTS.mdnames for tests.Dev session (durable attribution, in prose by design): https://claude.ai/code/session_016N6xmWt5hYm94ffVEwGH8x
Ruling executed: comment
5578286478(option A — converge, two steps; maintainer 「同意」), step 1 measured and ACCEPTED in5581457564. This is step 2, landing it.Route taken: one shared helper, both kernels import it (PM's suggested route, confirmed by measurement)
packages/core/src/plugin-contract.ts—assertPluginContract(plugin):PluginSchema.safeParsefor validation only, dropversionissues, refuse on the first remaining issue withPLUGIN_CONTRACT_VIOLATION(code on the property AND at the head of the message). This is the exact bodyPluginLoader.validatePluginContracthad; it moved, it did not change. ⛔ Not barrel-exported — the same mechanism and posture asplugin-registration.ts(the module both kernels import by relative path, becauseObjectKerneldoes not extendObjectKernelBase).PluginLoader.validatePluginContractis now a one-line delegate to it;validatePluginStructure(name / init / semver) stays the loader's own. The convergence is on the schema, per the ruling; the loader-structure boundary step ①-1 kept is kept here too, and I found no reason to move it:LiteKernelhas never judgedname/init/version, and the ruling text is aboutPluginSchema.LiteKernel.use():validateIdle()→assertPluginContract(plugin)→registerPluginByName(...). Exactly the wiring that produced step ①-1's number.dispatcher-error-vocabulary.ts: thePLUGIN_CONTRACT_VIOLATIONrow'sfilemoves with the stamp site (assignconstresolves the constant in the file it is stamped from), and itswhynames both kernels.check:dispatcher-error-vocabularygreen.One refusal, same code, same message shape, from either kernel — pinned, not asserted: for one input, the
ObjectKernelmessage is byte-for-byteFailed to load plugin: NAME -+ theLiteKernelmessage. That prefix is the loader's existing wrapper for every load failure and is untouched. OnLiteKernel(synchronous, no re-wrap) thecodeproperty survives too.The three refused fixtures from step ①-1, and the split
packages/cli/test/fixtures/option-b-reader-probe.ts—type: 'service' as constdeleted (one line, plus a comment saying why). Re-confirmed on this tree: it is the onlytype: 'service'on a plugin object repo-wide (the other hit,graft-runtime-hooks.test.ts, is anisAppPluginLikeargument, not a registered plugin); nothing in the fixture reads.type;PluginSchema.typedefaults tostandardwhen absent;ObjectKernelalready refused this object.ui-plugin-auto-discovery.pin.test.tssection F — rewritten, not deleted, and not "fixed into passing". F0 (the fully declared control) is unchanged and still mounts onLiteKernel— that is what proves the convergence is on the schema and the harness still mounts under this kernel. F1/F2 now pin the opposite of what they pinned: the same two inputs (noslug, nostaticPath) are refused byLiteKernel.use()with the same envelopeObjectKernel.use()gives them — code, key,PLUGIN_UI_REQUIRED_KEY_MISSING,codeon the property — and the byte parity above. Why rewrite rather than retire: the file's whole point is stating this block's reachability PER KERNEL; deleting F would leave the file saying nothing about the kernel the card was about, and re-pinning the old leniency by injecting intokernel.pluginsdirectly would pin dead code against an input no kernel can produce. The header,bootLite's docblock, the B/D comments that said "⛔ NOT dead code … LIVE AND LOAD-BEARING onLiteKernel" and the C (#15638) narration were all rewritten because each asserted a fact this PR makes false; each now carries both readings (before / after) rather than the new one alone.Tests added
packages/core/src/plugin-contract-enforcement.test.tsgroup G — the SAME contract on LiteKernel.use() (11it/it.eachblocks, 16 cases): theui-pluginrefusal withcodeon the property, its calibration twin, the twouirequired-key refusals (the inputs #16721 was filed on),nullon a declared key, untyped plugin loads with notypewritten back, class-based plugin keeps identity/prototype/methods,versionexclusion holds (1.0.0-alpha.1,1.0.0+20230101,0.0.0-fixture, version-less), the cross-kernel byte-parity pin, and two ORDER pins — a refused plugin never reaches the registry (cannot supersede an earlier registration by name), and state is checked before the contract (post-bootstrap refusal is the idle one, noPLUGIN_CONTRACT_VIOLATION, nocode).Verification
Code readings at
cb787324f(every source file of this PR is at that commit); the one later commit,8443166a6, touches onlycontent/docs/plugins/anatomy.mdx(below), and the gate union was re-derived and re-reconciled at8443166a6:88 derived, 88 run, 0 NOT-MEASURED, 0 UNRUN. The tree equals HEAD,git status --porcelainempty. Exit codes captured before any pipe; lock verdict lines quoted fromscripts/pm/os-verify-lock.sh.Builds (through the lock).
pnpm --filter '@objectstack/core...' build—VERDICT command-exit 0 · held the lock 201s. Then the hono-server + cli closures: 58 packages built; the run'sVERDICT command-exit 2ispackages/cli's own script refusing a forwarded--concurrency=2(sh: Syntax error: word unexpected— my flag placement, the documented pnpm trap), not a compile error; cli's owndist/is not an input to its vitest unit tier, so nothing owed was blocked. ⭐packages/corewas rebuilt before every dist-mediated run (cli resolves@objectstack/corethroughexportstodist/).Tests (through the lock).
pnpm --filter @objectstack/core test→core-test-exit=0—Test Files 49 passed (49) · Tests 1211 passed (1211);pnpm --filter @objectstack/core typecheck→core-typecheck-exit=0(check:test-typecheck: OK).plugin-contract-enforcement.test.tsalone, verbose reporter →Tests 34 passed (34), group G's 14 cases listed by name (file went from 18 to 29it/it.eachblocks).ui-plugin-auto-discovery.pin.test.ts→Tests 17 passed | 1 todo (18);pnpm --filter @objectstack/plugin-hono-server typecheck→ exit 0 (hono aliases@objectstack/coretosrc, so it read the source directly).pnpm --filter @objectstack/cli exec vitest run --project unit test/option-b-reader-acceptance.pin.test.ts→Tests 7 passed (7)— the 7 tests step ①-1 saw skipped by thetype: 'service'boot helper now run, on the rebuilt core dist;pnpm --filter @objectstack/cli typecheck→ exit 0. The cliintegrationproject is declared to CI (the diff touches no spawn entry).packages/restrest-api-plugin-objectql-provider-three-state.test.ts→Tests 8 passed (8)— the oneLiteKernel-using test file that changed between the measurement commit78bc4ad58and this base (its drift is in a door handler, not a plugin object).packages/runtime— the four tests that read the vocabulary rows →Test Files 4 passed (4) · Tests 70 passed (70).Blast-radius drift since the measurement. 10 commits between
78bc4ad58and base30b099078; of the 156 tracked files mentioningLiteKernel, 5 changed (one rest test above, two rest product files, one skill README — prose — and one more prose file); none adds a plugin object. Step ①-1's 813 / 807 / 6 / 0 stands; the rest of theLiteKernel-consuming suites (30 packages) are declared to CI's test job.Gates. Derived with
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackatcb787324f(60 families); reconciled with--ran:60 derived, 60 run, 0 UNRUN. 57 exit 0. The remaining three:check:doc-authoringwent red on my own edit (a tracker id inside the vocabulary row's runtimewhystring) and is green after removing it (exit 0, re-run);check:dual-build-cjs-loadsandcheck:type-check-debtboth exit 3 —PREREQUISITE NOT MET, they read every package'sdist/and a fresh worktree lacks 82 / the full closure — NOT MEASURED locally, declared to CI, not a pass and not a finding. Also green:check:dispatcher-error-vocabulary(the movedassignconstrow resolves),check:nul-bytes,check:error-code-casing(outside the derivation, ran anyway),check:cross-package-test-inputs,check:test-source-alias,check:adr-0087-registration --base origin/main,check:changeset-no-major --base origin/main,check:empty-changeset --base origin/main.Lint, narrowed and proven.
pnpm exec eslint --no-inline-config --format jsonover the 8 touched files → exit 0,files linted: 8 errors: 0 warnings: 0(count read from the JSON). Population pereslint.config.mjs(packages/**/*.{ts,tsx,mts,cts}+ the root**/*block,NEVER_LINTEDexcluded). Invariance: the config states it runs noparserOptions.projectand no typed@typescript-eslintrules (line 328), so this diff cannot move any untouched file's verdict — the repo-widepnpm lintis CI's.Ablation
Committed first (
HEAD= the fix), mutation and restore both under the lock,trap … EXIT INT TERMrestore, absolute paths, blob hashes —VERDICT command-exit 0 · held the lock 50s.A first attempt was a no-op and is reported as such: the mutation deleted the call and the DTS build refused the now-unused import (
TS6133), so its pins never ran against a mutated artifact; that failed tsup run also left core'sdist/truncated with the marker in 2 files, which the second run's rebuild replaced. The second attempt keeps the import referenced.assertPluginContract(plugin);inLiteKernel.use()withvoid assertPluginContract; const ablation16721 = 'ABLATION_16721_NO_CONTRACT'; …. On disk: anchor count 0, marker count 1, blob6a368df0…(HEAD38912c87…).pnpm --filter @objectstack/core buildexit 0;node scripts/ablation-dist-preflight.mjs core 'ABLATION_16721_NO_CONTRACT'exit 0 (marker PRESENT in dist).plugin-contract-enforcement.test.ts:Tests 6 failed | 28 passed (34). The six:refuses the legacy ui-plugin type …,refuses a ui plugin with no staticPath …,… no slug …,refuses null on a declared key …,PARITY — …,ORDER — a refused plugin never reaches the registry …. Every calibration twin, the version-exclusion cases andORDER — state is checked before the contractstayed green — so the six are attributable to the wiring and not to the harness.ui-plugin-auto-discovery.pin.test.ts:Tests 2 failed | 15 passed | 1 todo (18)— F1 and F2 red withexpected kernel.use() to refuse the plugin, but it loaded; F0 green.option-b-reader-acceptance.pin.test.ts:Tests 7 passed (7)under the mutation too.type: 'service'makes the fixture legal on both kernels, so it passes with and without the check. Its evidence is the reverse: before the fixture fix, ①-1 measured it red (7 skipped) under the same wiring.git checkout HEAD -- packages/core/src/lite-kernel.ts(never bare); blob back to38912c87…= HEAD;git diff HEAD --statempty;git status --porcelainempty; anchor count 1, marker 0; rebuild exit 0; preflight--absent→marker absent from all 12 built files; core pinsTests 34 passed (34). Final marker-in-dist count 0.Changeset
.changeset/litekernel-enforces-plugin-contract.md—@objectstack/core: minor, BREAKING banner,adr-0087: not-required (no-migration-prescription). Level derived fromscripts/check-changeset-no-major.mjs's header (an accept-set narrowing is breaking; during the launch window breaking ships asminor, breaking-ness carried by the banner + the ADR-0087 disposition) and from the precedent changeset of theObjectKernelhalf (enforce-plugin-schema-at-kernel-use.md, same shape). The body states the narrowing plainly, enumerates all EIGHT keys (the loader comment's ⛔ rule), states what is still accepted, the ordering, the measured blast radius (813 / 807 / 6 / 0), and the migration.Docs-drift rows (coordinator reading, answered per page)
The question per page: does it show or describe a plugin object
LiteKernelnow refuses, or state thatLiteKerneldoes not validate? Read on this tree, page by page.content/docs/plugins/anatomy.mdxname,version = '1.0.0',dependenciesand notype(reads asstandard) — accepted. Its UI-plugin bullets saidstaticPath"must" be provided but namedslugonly as the mount segment, and nothing on the page saidkernel.use()validates.8443166a6):slugis now "must provide" besidestaticPath, one bullet says auiplugin missing either is refused atkernel.use()withPLUGIN_CONTRACT_VIOLATIONon both kernels, and the lifecycle list gains akernel.use()step stating the contract check (closed-settype, URLhomepage, nonull, the twouikeys) and that the object is validated, never replaced. Nothing else on the page moved.content/docs/plugins/packages.mdxLiteKernelamong@objectstack/core's exports; everykernel.use()example registers an in-repo plugin class (ObjectQLPlugin,DriverPlugin,AuthPlugin, …) onObjectKernel.content/docs/kernel/events.mdxkernel:shutdownfail-soft "onObjectKernelandLiteKernelalike".content/docs/ai/actions-as-tools.mdxnew LiteKernel(); kernel.use(new MCPServerPlugin({ … })).MCPServerPlugindeclarestype = 'standard'(packages/mcp/src/plugin.ts), accepted by the contract. Untouched.content/docs/ai/natural-language-queries.mdxLiteKernel+MCPServerPluginexample.content/docs/ai/skills-reference.mdxLiteKernelis still the test kernel, now also strict. Untouched.content/docs/releases/v15.mdx(READ-ONLY)Also checked, per the coordinator's note: no page under
content/docsstates thatLiteKernelskips validation (git grep -i LiteKernel -- content/docsfiltered on valid/schema/contract/strict/lenient/skip → only theevents.mdxhook sentence above).AGENTS.mduntouched: its Kernel-table sentence stays true under option A.packages/core/src/types.tsyielded no docs anchor, so its doc comment (updated in this PR to name both kernels) is the only prose about it.Gates re-derived with the docs path in the delta: 88 families (28 added for
content/docs), all run; the one that reddened,check:skill-examples, was a prerequisite refusal (@objectstack/client-reactunbuilt) and is green after building that package (258 prose examples type-check across 3 surface(s)).验收备注
noted, not filed:with both kernels refusing auiplugin missingslugorstaticPath, theplugin.slug || plugin.name.split('/').pop()fallback and the&& plugin.staticPathconjunct inhono-plugin.ts's auto-discovery block are reachable through neither published kernel'suse(). Dead code by observation, not a defect — and only reachable by a caller writing intokernel.pluginsdirectly. Not touched here (scope: this card is the kernel, not the block). 承接者:none queued; the natural carrier is [finding]plugin-hono-serverstill accepts the legacyui-plugintype thatPluginSchemarefuses — an unreachable arm under ADR-0049 #15638 (theui-pluginarm in the same block), whose C-todo narration in the pin file I updated to say the arm is now refused on both kernels.noted, not filed:[finding]plugin-hono-serverstill accepts the legacyui-plugintype thatPluginSchemarefuses — an unreachable arm under ADR-0049 #15638's premise shifted under this PR: its arm is no longer "half dead" —type: 'ui-plugin'is refused atuse()on both kernels. The pin file's C narration records both readings; the ruling on [finding]plugin-hono-serverstill accepts the legacyui-plugintype thatPluginSchemarefuses — an unreachable arm under ADR-0049 #15638 (remove vs. declare/convert) is untouched and still owed.noted, not filed:check:dual-build-cjs-loadsneeds a full-repo build (PREREQUISITE NOT MET, 82 packages withoutdist/on a fresh worktree); declared to CI, recorded as NOT MEASURED locally.维护者速读(草稿)
改了什么 —
LiteKernel.use()现在跑与ObjectKernel.use()完全相同的插件契约检查(PluginSchema),两个 kernel 共用一个函数(packages/core/src/plugin-contract.ts),拒收的错误码与文案完全一致。之前只有ObjectKernel检查,LiteKernel直接入库。为什么改 — 同一个插件对象,一个 kernel 接受、另一个拒收;而文档指定测试用的正是宽松的那个,所以「测试绿、生产启动崩」。裁决(决策批次 #82,「同意」)选 A:收敛。第一步已测出代价:仓内 813 次
LiteKernel.use()调用中 807 次原样通过,6 次拒收全部来自 2 个文件里的 3 个测试夹具对象,产品代码 0 处受影响。风险与代价(含回滚) — 对外是一次受理面收窄:此前
LiteKernel接受、ObjectKernel拒收的插件对象,现在两边都拒收。收窄范围与ObjectKernel已有的完全一致,不多一个键。changeset 为minor+ BREAKING 横幅(启动期约定)。回滚:撤销本 PR 即恢复LiteKernel的旧行为;不涉及元数据、schema 或存储形态,无需迁移。席位意见 — (留空,席位定稿)
你要做的 — 合并前由合约评审席位(
needs:contract-review)确认收窄范围;确认后按常规流程合并。问题 ②(isDefault退役)是独立的下一张 PR,本 PR 不涉及。Generated by Claude Code