Skip to content

fix(plugin-hono-server): drop the retired ui-plugin arm from UI auto-discovery - #20086

Merged
os-steve merged 1 commit into
mainfrom
claude/issue-15638-ui-plugin-arm-delete
Sep 25, 2026
Merged

os-steve merged 1 commit into
mainfrom
claude/issue-15638-ui-plugin-arm-delete

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #15638

Clause-②: no

What

This PR deletes the plugin.type === 'ui-plugin' disjunct and its "Support legacy 'ui-plugin' and new 'ui' type" comment from the UI auto-discovery block in HonoServerPlugin.start() (packages/plugins/plugin-hono-server/src/hono-plugin.ts:521-522 on 66960564). The condition is now plugin.type === 'ui' && plugin.staticPath.

This is item 1 of the maintainer ruling B+ (5557089123), carried out on the director seat's execution terms (5644357902). Item 2, the boot seam refusing the retired spelling, was delivered by #16049's enforcement. This PR reads that refusal and does not re-implement it. There is no hint specific to the retired spelling: the generic closed-set refusal is the message.

This PR fills pin C of #16050 (the it.todo in ui-plugin-auto-discovery.pin.test.ts). #16050 stays open here; the seat handles it at landing.

Reachability probe (the precondition, run before any edit)

A temporary probe (deleted, never committed) ran against the unmodified tree at 66960564. It drove the real ObjectKernel.use() and LiteKernel.use(), then the real HonoServerPlugin.init() and start(), and recorded the route registrations:

input ObjectKernel LiteKernel
declared type: 'ui-plugin', with staticPath and slug refused: Failed to load plugin: NAME - PLUGIN_CONTRACT_VIOLATION: ... at 'type'; not stored refused: the same text, code: PLUGIN_CONTRACT_VIOLATION; not stored
control: type: 'ui' 4 routes (/probe-console and /probe-console/*, two registrations each) 4 routes
control: type: 'driver' [] []
an admitted ui entry flipped to ui-plugin before start() 4 routes 4 routes

Static census on the same tree:

  • The arm iterates ctx.getKernel().plugins, and that map has exactly two writers, both registerPluginByName:

    • from ObjectKernel.use() (kernel.ts:253), after loadPlugin, validatePluginContract and assertPluginContract;
    • from LiteKernel.use() (lite-kernel.ts:75), after assertPluginContract.

    No other plugins.set( or registerPluginByName( call exists outside tests.

  • The CLI's config-object path wraps a bundle that has no init in AppPlugin. AppPlugin's type = 'app' is a class field and is never copied from the bundle, and the wrapped object still enters through kernel.use().

  • type values handed to kernel.use() in the repo:

    • Class fields are only standard, server, driver, objectql and app.
    • The object-literal type: 'plugin' hits are manifest.register(...) package manifests, not kernel plugins.
    • No in-repo production code produces ui-plugin as a type value. The three non-test, non-doc mentions are package names in comments.
    • No in-repo code writes to a registered plugin's type.

Reading. No path admits a DECLARED ui-plugin to the map on either kernel. The disjunct was unreachable through either kernel's use(), which is the pin file's own reachability standard.

The one residual is the last table row. The contract validates at use() and stores the object by reference, so an object admitted as ui that later rewrites its own type did reach the arm. That is a bypass of the contract, not a path through it: the same bypass works for every key the contract checks, and nothing in the repo produces it. The changeset names it, C2 pins it, and it is flagged for the contract reviewer below.

Clause-②

Declared from the delivered diff: no.

  • No file under packages/spec/src/** is touched.
  • No accept or reject verdict moves for any declared value: ui-plugin was already refused at use() on both kernels before this diff.
  • The only class whose outcome moves is the post-admission self-rewrite above: mounted before, not mounted after. That object is off-contract by the time start() reads it, so I read it as outside the conformance limb. Reviewer: please confirm or overturn that reading.

No **BREAKING** banner and no ADR-0087 marker are carried.

Pin C

These cases are in packages/plugins/plugin-hono-server/src/ui-plugin-auto-discovery.pin.test.ts, using the file's own harness:

  • C1 reads [finding] PluginSchema has zero runtime callers — the boot path validates name, init and semver only, so the declared plugin contract is never enforced #16049's refusal. A declared ui-plugin fixture, otherwise a complete ui fixture, is refused:
    • by LiteKernel.use(), with code: PLUGIN_CONTRACT_VIOLATION at 'type';
    • by ObjectKernel.use(), with exactly that text behind the loader's Failed to load plugin: NAME - prefix. This is the F1/F2 parity shape.
  • C2 runs on both kernels. An admitted ui entry flipped to ui-plugin before start() mounts []. The same case carries a firing control: same kernel, same fixture, no flip, 4 routes.
  • observe() gains an optional beforeStart hook that receives the kernel's own admitted entry. C2 is its only caller.
  • B, E and F0 still pin the modern arm, and all three are green.

Evidence (at 7363f0f4)

  • pnpm --filter @objectstack/plugin-hono-server test: 26 files, 314 tests passed.
  • The pin file alone, verbose reporter: 20 passed, up from 17 plus 1 todo. C1, C2 on ObjectKernel and C2 on LiteKernel are listed by name.
  • pnpm --filter @objectstack/plugin-hono-server typecheck: exit 0, and check:test-typecheck reports OK. tsconfig.test.json --listFilesOnly includes the pin file.
  • Ablation, on the committed tree through scripts/ablation-replace.mjs in wrap mode:
    • Mutation: restore (plugin.type === 'ui' || plugin.type === 'ui-plugin') && plugin.staticPath. The anchor went from 1 hit to 0 and the blob from 201c5866 to ca844899.
    • Result: 2 failed, 18 passed. Both failures are C2 (ObjectKernel and LiteKernel), each with expected [ '/console-fixture', …(3) ] to deeply equal []. The failure went the expected way (to red).
    • Restore proven: the blob is back to 201c5866, equal to HEAD, and git diff HEAD is empty.
  • node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derives 60 commands, identical to the dispatch list. All 60 ran and exit 0. --ran answers "60 derived, 60 run, 0 NOT-MEASURED, 0 UNRUN (a DERIVED zero)".
  • check:dual-build-cjs-loads, check:lean-entry-closure and check:type-check-debt first exited 3 (PREREQUISITE NOT MET). After turbo run build --filter='./packages/*' --filter='./packages/*/*' (72/72 tasks), all three exit 0.
  • The roster families whose roster sits under a touched directory all exit 0: check-changeset-fixed, check:authz-resolver, check:error-code-casing and check:filter-alias-parity.
  • pnpm lint, the full union: exit 0.

Acceptance notes

  • Two more parts of the same condition are also unreachable through either kernel's use(): the plugin.slug || plugin.name.split('/').pop() derivation and the && plugin.staticPath conjunct. That is already recorded on [Decision] The declared plugin contract does not govern what the runtime accepts: LiteKernel never runs PluginSchema, and isDefault survives the kernel that does #16721. This ruling does not cover them, so they are left untouched.
  • The post-admission rewrite residual follows from the documented validation-only design (packages/core/src/plugin-contract.ts: the parse output is discarded and the object is stored by reference). It applies to every contract key, not only type, and it is not a defect of this card.
  • The default/isDefault redirect is still unpinned, as the pin file's header already says.
  • packages/core/src/plugin-loader.ts and packages/spec/** are not touched. CHANGELOG.md and package.json are left to the Version Packages PR.

Generated by Claude Code

…discovery

The auto-discovery block matched `type === 'ui' || type === 'ui-plugin'`
under a "Support legacy" comment. `ui-plugin` is outside the closed
plugin-type set and both kernels' `use()` already refuse it through the
shared plugin contract, so the disjunct was reachable through neither
kernel's `use()`. Delete it and its comment.

Pin C in ui-plugin-auto-discovery.pin.test.ts replaces its placeholder:
C1 reads the contract's refusal of a declared `ui-plugin` on both
kernels (envelope parity), and C2 proves the arm is gone behaviourally:
an admitted `ui` entry flipped to `ui-plugin` before start() mounts
nothing, with an in-case firing control.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TnPAC1UsTGfHPXVUCL6iLn
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Sep 25, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

Nothing in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 1 changed package(s)), so this run has no opinion about the docs.

What this run could not see

Coarse fallback — 7 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 7ddf396109b9d423cf52c2e5b69b0e34f235e7f5 → packageMentionDocs.

@github-actions

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 36089109896 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

失败的 job(日志抽取,best effort):

  • Test Core (1/6) — 失败步骤: Run this shard's tests

    @objectstack/spec:test:  FAIL   local  scripts/liveness/check-liveness.test.ts > check:liveness — evidence pointers (#5623) > is green against a verbatim copy of the shipped ledgers
      ↳ 失败原因: @objectstack/spec:test: AssertionError: Spec liveness gate (registry-rooted) — governed types: object, field, flow, action, hook, permission, position, agent, tool, skill, dataset, page, view, report,
    @objectstack/spec:test:  FAIL   local  scripts/liveness/check-liveness.test.ts > check:liveness — evidence pointers (#5623) > stays green when the missing path is attributed to ANOTHER repo
      ↳ 失败原因: @objectstack/spec:test: AssertionError: Spec liveness gate (registry-rooted) — governed types: object, field, flow, action, hook, permission, position, agent, tool, skill, dataset, page, view, report,
    @objectstack/spec:test:  FAIL   local  scripts/liveness/check-liveness.test.ts > check:liveness — evidence pointers (#5623) > never bounds a citation attributed to ANOTHER repo
      ↳ 失败原因: @objectstack/spec:test: AssertionError: Spec liveness gate (registry-rooted) — governed types: object, field, flow, action, hook, permission, position, agent, tool, skill, dataset, page, view, report,
    @objectstack/spec:test:  FAIL   local  scripts/liveness/check-liveness.test.ts > check:liveness — evidence pointers (#5623) > prints the citation count and how many are in range, in the documented two
      ↳ 失败原因: @objectstack/spec:test: AssertionError: Spec liveness gate (registry-rooted) — governed types: object, field, flow, action, hook, permission, position, agent, tool, skill, dataset, page, view, report,
    @objectstack/spec:test:  FAIL   local  scripts/liveness/check-liveness.test.ts > check:liveness — symbol anchors (#12516) > stays GREEN on the drifted BEFORE-state — the honest residual this grammar e
      ↳ 失败原因: @objectstack/spec:test: AssertionError: Spec liveness gate (registry-rooted) — governed types: object, field, flow, action, hook, permission, position, agent, tool, skill, dataset, page, view, report,
    @objectstack/spec:test:  FAIL   local  scripts/liveness/check-liveness.test.ts > check:liveness — symbol anchors (#12516) > prints the anchor count and how many resolve, equal on a green run
      ↳ 失败原因: @objectstack/spec:test: AssertionError: Spec liveness gate (registry-rooted) — governed types: object, field, flow, action, hook, permission, position, agent, tool, skill, dataset, page, view, report,
    @objectstack/spec:test:  FAIL   local  scripts/liveness/check-liveness.test.ts > check:liveness — the evidence-scan population (#13041) > stays GREEN when a `dead` entry carries the SAME rotted pointe
      ↳ 失败原因: @objectstack/spec:test: AssertionError: Spec liveness gate (registry-rooted) — governed types: object, field, flow, action, hook, permission, position, agent, tool, skill, dataset, page, view, report,
    @objectstack/spec:test:  FAIL   local  scripts/liveness/check-liveness.test.ts > check:liveness — the evidence-scan population (#13041) > declares every status either scanned or explicitly unscanned, 
      ↳ 失败原因: @objectstack/spec:test: AssertionError: Spec liveness gate (registry-rooted) — governed types: object, field, flow, action, hook, permission, position, agent, tool, skill, dataset, page, view, report,
    @objectstack/spec:test:  FAIL   local  scripts/liveness/check-liveness.test.ts > check:liveness — the live-elsewhere criteria (#13483) > is green on the shipped ledgers and publishes the population be
      ↳ 失败原因: @objectstack/spec:test: AssertionError: Spec liveness gate (registry-rooted) — governed types: object, field, flow, action, hook, permission, position, agent, tool, skill, dataset, page, view, report,
    @objectstack/spec:test:  FAIL   local  scripts/liveness/check-liveness.test.ts > check:liveness — the README state table (#7257) > is green against a verbatim copy, and says how many rows it checked
      ↳ 失败原因: @objectstack/spec:test: AssertionError: Spec liveness gate (registry-rooted) — governed types: object, field, flow, action, hook, permission, position, agent, tool, skill, dataset, page, view, report,
    @objectstack/spec:test:  FAIL   local  scripts/liveness/check-liveness.test.ts > check:liveness — the generated count artifact (#7377) > is green against a verbatim copy, and says the artifact is curr
      ↳ 失败原因: @objectstack/spec:test: AssertionError: Spec liveness gate (registry-rooted) — governed types: object, field, flow, action, hook, permission, position, agent, tool, skill, dataset, page, view, report,
    @objectstack/spec:test:  FAIL   local  scripts/liveness/check-liveness.test.ts > check:liveness — the evidence summary line (#5623) > prints declared and resolved as separate numbers, equal on a green
      ↳ 失败原因: @objectstack/spec:test: AssertionError: Spec liveness gate (registry-rooted) — governed types: object, field, flow, action, hook, permission, position, agent, tool, skill, dataset, page, view, report,
    

↳ 失败原因 是判读的关键:超时(Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言(AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

⚠️ 断言这一侧有一类例外,判据是断言在测什么,不是它是不是 AssertionError。 断言的对象是产品行为(一个值、一个形状、一次拒收)⇒ 照上面读:真实的行为改变,去查,⛔ 不要重排掉;
断言的对象是这次实验自身的有效性前提(跑完的耗时、负载下的先后、任何只在时间预算内才成立的条件)⇒ 它跟超时是同一类,同样对负载敏感,重排一次是合法的判别手段。
识别是机械的:断言的消息或它比较的值本身点名了一段时长、一个时间戳、一个耗时计数。实测过的一对 —— AssertionError: SecurityPlugin.init() ran: expected false to be true 测的是产品行为(真回归);
AssertionError: this run took over a second, so second-precision stamps could have differed too: expected 1006 to be less than 1000 测的是实验前提:它守护的那条不变式当时是绿的,同一个 head 原样重排一次即成功。
穿着 AssertionError 外衣的时间测量,仍然是时间测量。(⛔ 这只改「怎么读一次红」,不改「哪些测试可以重排」——后者由别处管。)

跨 PR 相同签名(24h,按失败测试文件聚合):

历史信号:

  • 本 PR 过去 24h 无队列失败记录(首次)。
  • 过去 24h 队列共有 1 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Merge-queue red on this PR's group: not this PR's; no action taken; the queue has rebuilt it on main

domain:cli execution PM seat #6024 · session session_01TnPAC1UsTGfHPXVUCL6iLn · 2026-09-25T03:24Z

Merged via the queue into main with commit 3c48234 Sep 25, 2026
36 checks passed
@os-steve
os-steve deleted the claude/issue-15638-ui-plugin-arm-delete branch September 25, 2026 03:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[finding] plugin-hono-server still accepts the legacy ui-plugin type that PluginSchema refuses — an unreachable arm under ADR-0049

2 participants