Skip to content

docs(skills): make the plugin guide's node interface extend BaseSchema - #8109

Merged
os-zhuang merged 1 commit into
mainfrom
claude/issue-8103-plugin-guide-extends-baseschema
Sep 7, 2026
Merged

docs(skills): make the plugin guide's node interface extend BaseSchema#8109
os-zhuang merged 1 commit into
mainfrom
claude/issue-8103-plugin-guide-extends-baseschema

Conversation

@claude

@claude claude Bot commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

Fixes #8103

The "Type definitions" fence in skills/objectui/guides/plugin-development.md
taught a plugin author to declare their widget's node type as a standalone
interface that re-states five of BaseSchema's members by hand — and declares
two of them narrower than the shipped renderer accepts. It now imports
BaseSchema from @object-ui/types the way this file's other fences import,
extends it, and keeps only the widget's own two members.

⚠️ Generic parameters are spelled in WORDS below (e.g. "array-of-SchemaNode"),
never in angle-bracket shape.
This repo's AGENTS.md records that GitHub's
body sanitizer eats tag-shaped fragments even inside backticks and fenced
blocks; this PR is entirely about type declarations, so a literal rendering
would collapse both columns of the table into the same string and a table
written to show a change would read as "nothing changed".

Surface

file lines before lines after governed
skills/objectui/guides/plugin-development.md 453 453 yes

⛔ No other file. git diff --stat on the branch: 1 file changed, 7 insertions(+), 7 deletions(-).

Line- or token-pinned in this repo? Measured: no. No gate counts lines or
tokens of skills/**. The two scripts in this repo whose names carry "budget"
scripts/check-eager-closure-budget.mjs and scripts/render-budget-comment.mjs
— mention skills zero times each (grep -c skills), and
scripts/check-skill-eval-tokens.mjs measures eval must_contain tokens
against the bundle's markdown, not file size. The change is line-neutral in
fact as well as by budget: 453 lines before, 453 after, one line replaced per
line removed.

Governed verdict, quoted from the gate:

$ node scripts/check-governed-queue-guard.mjs --test skills/objectui/guides/plugin-development.md
⛔ GOVERNED — 1 of 1 path(s) are on a governed surface:
   skills/** x1 — the published skills catalog
     - skills/objectui/guides/plugin-development.md

   One governed path governs the WHOLE pull request — proportion is not a question.
   ⛔ Do not flip it ready, enqueue it, or arm auto-merge. Park it as a DRAFT and leave the merge
      to the maintainer; a human merge IS the review record for a governed surface.
exit 3

⇒ This PR stays a draft. ⛔ Not flipped ready, not enqueued, no auto-merge, no approval sought.

改前 → 改后 — the fence at 219, ten lines replaced by ten

改前 改后 the reader / authority that justifies it
(no import line) import type { BaseSchema } from '@object-ui/types'; the spelling this file already uses one fence up — import type { MyWidgetProps } from './types'; at line 194. BaseSchema is a real export: packages/types/src/base.ts:70, barrel packages/types/src/index.ts:95
export interface MyWidgetSchema { export interface MyWidgetSchema extends BaseSchema { the repo's own scaffolder already does exactly this. packages/create-plugin/src/templates.ts:438-446 emits import type { BaseSchema } from '@object-ui/types'; then export interface PASCALSchema extends BaseSchema { type: 'NAME'; } for every generated plugin, and its docblock (:413-427) states why in the card's own terms: a hand-rolled base "would hand every scaffolded plugin a second dialect of a base node the protocol already defines — precisely the 'one strict contract beats N dialects' failure in AGENTS.md commandment #0.1". The guide was teaching the shape the generator had already abandoned
type: 'my-widget'; unchanged narrowing BaseSchema.type (base.ts:75, string) to a literal is the one member a widget legitimately owns; identical to what the scaffolder emits
id?: string; gone — inherited base.ts:81, declared identically. Re-stating it buys nothing
className?: string; gone — inherited base.ts:171, declared identically
bind?: string; gone — inherited base.ts:248, declared identically
props?: MyWidgetProps; kept, with the trailing comment read by the entry point's {...schema.props} kept under the amended #8092 test: a member stays only with a real reader on the wire. Two readers, both measured. (1) This guide's own renderer, the fence at 158: LazyMyWidget SPREAD-schema SPREAD-schema.props — the plugin renderer reads its config from schema.props, which is what the dispatch asked to verify, and the answer is that props is the right member for this example. (2) The platform's element:* reader, packages/components/src/renderers/basic/readProps.ts:70isConfigBag(schema?.props) ? schema.props : {}. BaseSchema does not declare props as a named member; it is admitted by the index signature at base.ts:464 ([key: string]: any), which is the TS-side half of why the wire test, not the interface test, is the right one
hidden?: string; gone — inherited, and WIDER than what was taught packages/types/src/base.ts:376 declares hidden?: boolean OR ExpressionWire. ExpressionWire (packages/types/src/expression.ts, exported at packages/types/src/index.ts:119) is itself a string OR the CEL envelope object. The old line refused hidden: true and refused the envelope — both of which the shipped renderer accepts and acts on
disabled?: string; gone — inherited, same widening packages/types/src/base.ts:412, same wire
(nothing) three comment lines naming what is inherited and the real wire types, pointing at packages/types/src/base.ts the one abridged note this fence owes, and the reason a reader does not have to rediscover the wire type at the member

Prose folding (dispatch clause 4): measured, nothing to fold. The only text
between the fence above and this one is the heading ### Type definitions
(line 217). grep -n 'hidden|disabled|MyWidgetSchema|BaseSchema' over the file
returns no prose sentence describing the hand-rolled members — the three other
disabled hits (lines 253, 263, 281) are FieldWidgetComponentProps.disabled,
the field-widget host prop, a different key on a different contract. Nothing
else in the guide moves.

The rewritten interface type-checks against the built d.ts — with both controls

Compiled with tsc against the built packages/types/dist/index.d.ts
(--strict --skipLibCheck --moduleResolution bundler, the specifier mapped
through paths), a program carrying the fence verbatim (ColumnDef stubbed —
that name is third-party and outside this card's ruled half):

  • positive arm, exit 0 — a MyWidgetSchema literal carrying id /
    className / bind / props (all now inherited) plus hidden: true,
    disabled as a predicate string, and a second literal with
    hidden: { dialect: 'cel', source: "record.status == 'draft'" }; both
    assigned onto a real BaseSchema; plus a third literal carrying name /
    visible / visibleWhen / body / children / testId / ariaLabel
    the members a copied subset silently loses.
  • negative control, exit 2 — the SAME three assignments against the
    interface as it read before this change are each refused:
    TS2322: Type 'boolean' is not assignable to type 'string' ·
    TS2322: Type '{ dialect: string; source: string; }' is not assignable to type 'string' ·
    TS2353: ... 'ariaLabel' does not exist in type 'OldMyWidgetSchema'.
    Without this arm the positive arm proves nothing.
  • resolution control, TS2305 — a planted missing export from
    @object-ui/types answers Module "@object-ui/types" has no exported member ..., not TS2307. So the mapping reached the real built d.ts and the
    positive arm's exit 0 is not vacuous.

The proof files lived in an untracked directory that was deleted before the
commit; git status --porcelain on the committed tree is exactly one modified
path.

Why no os:check marker was added

The fence at 219 is unmarked (re-taken on this tree: the file's markers sit
at lines 91, 243, 333 and 369; the one at 243 opts in the fence at 244, not
this one). The dispatch permits adding one only on showing the block compiles,
and node scripts/check-skill-examples.mjs --measure — which judges every
candidate, marked or not — says it does not, for a reason this change does
not own
:

before:  skills/objectui/guides/plugin-development.md:219  [typescript]  FAIL + 1 bare `any`
after:   skills/objectui/guides/plugin-development.md:219  [typescript]  FAIL + 1 bare `any`

and the file's semantic diagnostic list is identical before and after — 25
rows, same lines, same codes. The single diagnostic inside this fence is
plugin-development.md:233:13 TS2304: Cannot find name 'ColumnDef', on the
MyWidgetProps half: ColumnDef is declared nowhere in this workspace
(git grep -nE '(interface|type) ColumnDef' -- packages/*/src is empty; the
barrel exports LookupColumnDef and GridColumnDefinition, not this name), so
it is a third-party name a plugin author would import. The ruled half —
MyWidgetSchema — contributes zero diagnostics on both trees, and the new
import contributes none either: grep -c "TS2307.*@object-ui/types" over the
whole measurement is 0, so the specifier resolves.

⇒ Earning the marker would mean adding a ColumnDef import to a fence half
this card does not rule, on a governed file, at net-positive lines. Left alone;
the standalone tsc proof above is the stronger evidence and costs the file
nothing.

Gates — every exit captured before any pipe, on head fc5f332b

Each was run as cmd > FILE 2>&1; EXIT=$? — no exit code was read through a
pipe, and each verdict below is the gate's own printed line, not a bare $?.

gate verdict line
pnpm check:skill-examples exit 0 — Marked: 13 ts fence(s) (floor 13), 70 json fence(s) (floor 70) · Semantic phase: 13 of 13 ts fence(s) judged, 0 failed · JSON phase: 70 fence(s) parsed, 0 failed. Marked population before this change: 13 ts / 70 json. After: 13 ts / 70 json — unchanged, and the edited fence is not in it
pnpm check:skill-eval-tokens exit 0 — Red under the chosen oracle: 0 (0 beyond the baseline); 125 must_contain token(s) scored across 16 guide files. No eval token is spelled MyWidgetSchema, id-as-a-member, or the two removed predicate declarations, so nothing an eval grades was edited
pnpm check:skills-paths exit 0 — 88/89 stated path(s) resolve across 20 guide file(s); 1 baselined (unchanged; the new packages/types/src/base.ts mention sits inside a fence, which that gate reads by design as a worked example rather than a prose claim — and the path exists either way)
pnpm check:doc-fences exit 0 — every TypeScript block in 227 document(s) is fenced ts/tsx/typescript
node scripts/check-governed-queue-guard.mjs --test skills/objectui/guides/plugin-development.md exit 3 — GOVERNED, quoted in full above
node scripts/check-changeset-presence.mjs exit 0 — 1 file(s) changed, 0 of them published source of a package the release covers … ✅ No source or published contract of a released package changed in this range, so no changeset is owed. This repo has no skip-changeset label; the gate's own verdict is followed, so no changeset file is added
pnpm check:control-bytes exit 0 — scanned 6494 tracked text file(s); skipped 85 binary. Self-scan of the edited file with the control-byte class regex: no match
pnpm exec vitest run --maxWorkers=2 over every suite that scans the skills tree — doc-version-claims, check-skill-examples, check-skills-paths, check-skill-eval-tokens, layered-read-declared-path-4016, check-shell-escape-residue exit 0 — Test Files 6 passed (6) · Tests 236 passed (236). The population was derived by git grep -ln for the skills root across scripts/__tests__ and scripts/*.mjs, not from the dispatch's list. doc-version-claims.test.ts is the one that pins rows in this file (its react": "^18.0.0 and lucide-react entries at :1168 / :1174); neither row is touched and both still pass
whole-repo pnpm lint through the shared verify lock (acquired once, held 5m04s, waited 0s) exit 0 — Tasks: 47 successful, 47 total, 0 errors, 2900 warnings; every warning is pre-existing no-explicit-any in package source

All of the above were run on the tree that is fc5f332b — the single commit on
this branch — and git rev-parse --short HEAD at the time of the last run is
fc5f332b.

验收备注

Noted and deliberately not filed — none clears the maintainer's admission
threshold on its own:

  1. The ColumnDef name in MyWidgetProps resolves to nothing in this
    workspace.
    It is almost certainly TanStack's, which a plugin author would
    import, and the fence has carried it since before this card. It is the sole
    reason the block cannot earn an os:check marker today. Recorded rather
    than filed because the disposition is a judgement call between three shapes
    (import it and mark the fence, rename it to a locally-declared stub, or
    leave the fence unmarked), and none of them is this card's ruled surface.
  2. The entry-point fence at 158 spreads the node twice
    LazyMyWidget SPREAD-schema SPREAD-schema.props. That is what makes props
    a real reader here and so it is load-bearing for this change, but it also
    means a node-level key and a props key of the same name collide silently,
    with props winning. Adjacent to objectui#5123's precedence ruling on the
    platform's own two channels; the guide's example is not that code and is not
    pinned by it. Recorded so whoever revisits the entry-point example sees it.
  3. packages/create-plugin/src/templates.ts and this guide now agree, which
    they did not before. Nothing to do; noted because it is the strongest single
    authority in the table above and a future edit to either should keep them in
    step.
  4. objectui#8092 stays open for root AGENTS.md, and objectui#8065 is
    repaired by PR docs(skills): teach BaseSchema in the two published node fences #8104 on two other files. Both card numbers are written here
    bare, with no keyword anywhere near them, for exactly the reason AGENTS.md
    records: the parser does not read sentences, and a negation does not save you.

维护者速读(草稿)

我们发布给客户和 AI 的技能包里,有一份《插件开发指南》。它教插件作者:你的组件节点类型
长这样——然后把平台早就定义好的基础节点字段(idclassNamebindhidden
disabled)手抄了一遍,自成一份接口。问题不在于啰嗦,而在于抄错了两行:hidden
disabled 被写成纯字符串,而线上渲染器实际接受布尔值和 CEL 表达式对象。照着抄的人
(尤其是 AI)写出来的类型,会拒绝 hidden: true,也会拒绝表达式信封——而这两种写法平台
都支持、都会执行。

更要紧的是第二层:因为没有继承,这份手抄接口把没抄到的成员静默丢掉了——name
visiblevisibleWhenbodychildrentestIdariaLabel 全都不在。插件作者
不会收到任何警告,只会在某天写了 visible 之后发现类型报错,然后误以为平台不支持。

这次的改法是把示例改成 extends BaseSchema,只保留插件自己真正拥有的两个成员:type
(组件类型字面量)和 props(配置袋)。判据不是"TS 接口上有没有",而是"线上有没有真实
的读取方"——props 留下,是因为这份指南自己的渲染器示例就在读它
(SPREAD-schema.props),平台的 element:* 渲染器也读它;其余五行删掉,是因为它们本来
就继承得到,手抄只会引入偏差。

最有力的佐证是:我们自己的插件脚手架早就这么写了
packages/create-plugin/src/templates.ts 生成的每一个插件都是
interface XxxSchema extends BaseSchema,它的注释里写得很清楚——手抄一份基础节点,等于
给每个脚手架插件塞进"协议已经定义过的基础节点的第二种方言",正是 AGENTS.md 第 0.1 条
戒律要防的事。也就是说,指南教的是生成器早已放弃的写法,两边现在终于对上了。

风险面很小:改的是纯文档,没有任何运行时代码,文件行数不变(453 行进、453 行出),门禁
全绿(含全仓 lint 47/47、六个技能包门禁、以及所有会扫这棵树的 6 个测试套件 236 项)。
另外单独用 tsc 对着构建产物做了正反两腿证明:新写法接受布尔、字符串、CEL 三种
hidden 写法并可赋给真实 BaseSchema;旧写法对同样三行逐条拒绝。因为命中受管面
skills/**,这个 PR 停在 draft 等人工合并,没有翻 ready、没有入队、没有挂 auto-merge。

席位意见:


🤖 Generated with Claude Code

https://claude.ai/code/session_019RfFHiRCSs3JXLK4cwcfox


Generated by Claude Code

`MyWidgetSchema` in the "Type definitions" fence hand-rolled five of
`BaseSchema`'s members instead of extending it, and declared two of them
narrower than the shipped renderer accepts: `hidden` / `disabled` were
`string`, while `packages/types/src/base.ts:376` / `:412` declare each as
`boolean | ExpressionWire`. A plugin author copying the fence got a type
that refuses `hidden: true` and refuses the CEL envelope, and silently
lost every member the interface did not re-state.

The block now imports `BaseSchema` from `@object-ui/types` the way this
file's other fences import, extends it, and keeps only the widget's own
two members — `type` and `props`, the latter read by the entry-point
example above it via `{...schema.props}`. Line-neutral.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019RfFHiRCSs3JXLK4cwcfox
@claude

claude Bot commented Sep 6, 2026

Copy link
Copy Markdown
Contributor Author

ACCEPT — governed in-seat review of PR #8109, head fc5f332b (skills seat, 2026-09-06T16:4xZ).

Implemented-by: claude/issue-8103-plugin-guide-extends-baseschema (os-dev subagent, opus — no dispatch-gates.mjs in this repo; prose in a published skill, no contract move)
Reviewed-by: session_019RfFHiRCSs3JXLK4cwcfox (skills seat)

Verified in a detached compare worktree at the PR head, not from the report:

  • One file, skills/objectui/guides/plugin-development.md, +7/−7, 453 lines before and after. The 「Type definitions」 fence now imports BaseSchema from @object-ui/types the way the file's other fences import, extends BaseSchema, and keeps only the widget's own two members — type: 'my-widget' and props?: MyWidgetProps; the five hand-rolled inherited lines (id / className / bind / hidden / disabled) are gone, with a three-line comment naming what is inherited and that hidden / disabled are boolean | ExpressionWire on the real interface. props stays under the amended finding(docs): AGENTS.md section 4's node interface diverges from the real BaseSchema — two members that do not exist, one unexported element type, three declared differently #8092 test with its reader named in place — the guide's own entry-point example spreads schema.props, and the platform's element:* reader is packages/components/src/renderers/basic/readProps.ts (the brief named a path that does not exist; the dev corrected it from the tree). The seat confirmed the strongest authority the dev found: packages/create-plugin/src/templates.ts already scaffolds every plugin as interface XxxSchema extends BaseSchema — the guide taught the shape the generator had abandoned, and the two agree now.
  • Gates at the head: check-governed-queue-guard --test exit 3 (skills/**); check-changeset-presence "no changeset is owed" (objectui has no skip-changeset label); check-doc-fence-languages exit 0 — in the seat's worktree. The dev's report adds check:skill-examples with the marked population identical (13 ts / 70 json; this fence is unmarked and stays so, see below), check:skill-eval-tokens red 0, check:skills-paths 88/89 unchanged, the six scanning suites 236/236, whole-repo lint 47/47 through the shared verify lock in one acquisition, and a three-arm tsc proof against the built .d.ts (positive: the new shape accepts boolean / string / CEL-envelope predicates and the seven previously-lost inherited members; negative control: the old shape refuses each; resolution control: a planted missing export answers TS2305, so the mapping reached the real .d.ts). PR checks on fc5f332b: 25 success, 3 skipped, 0 failed (28 runs, converged).
  • Report hygiene: os-dev-report on finding(skills): the plugin-development guide's node interface hand-rolls BaseSchema's members and declares hidden / disabled as plain strings — the fourth surface of the same class #8103, JSON closes; PR body opens Fixes #8103, carries the 改前 → 改后 table, a ## 验收备注 with three noted items and a ## 维护者速读(草稿); no model identifier; no label or assignee touched by the dev; zero MCP calls.

Rulings on the two open questions

  1. os:check marker — A, unmarked. The only obstacle is ColumnDef in the MyWidgetProps half (a third-party name this card does not rule); importing it (B) adds a third-party specifier to the skills resolution map, and a stub (C) teaches a fake type. The ruled half is proven against the built types by the tsc arms; a marker guards a future edit, not a present defect. Noted, not a card.
  2. The entry-point fence's double spread (schema then schema.props, props winning on a same-name key) — A, leave it, noted under the threshold: no reader has hit it, and aligning it would be a taste call unless it follows the platform's own ruled precedence (propsproperties 同现时,alias 优先级按「读法」相反 —— 配置袋读到 properties,React prop 读到 props #5123) — a card only if a seat measures a real collision.

Landing regime: governed (skills/**) ⇒ the PR stays a draft; reviewers os-zhuang + hotlong requested in this same act, needs-user-decision added, the final 维护者速读 posted as the next comment; human merge. On the merge #8103 closes by Fixes — the fourth and last surface of the #7434 vocabulary class (docs/READMEs #8064 landed; AGENTS.md #8090, the two skill fences #8104, and this guide at the terminal).


Generated by Claude Code

@claude
claude Bot requested review from hotlong and os-zhuang September 6, 2026 16:43
@claude claude Bot added documentation Improvements or additions to documentation needs-user-decision labels Sep 6, 2026
@claude

claude Bot commented Sep 6, 2026

Copy link
Copy Markdown
Contributor Author

维护者速读

事情:发布技能包里的《插件开发指南》教插件作者写节点类型时,把平台已定义的基础节点字段手抄了一遍,还抄错两行——hidden / disabled 写成纯字符串,而渲染器实际接受布尔值与表达式信封;没抄到的成员(namevisiblechildren 等)被静默丢掉,作者写了 visible 才发现类型报错、误以为平台不支持。

改了什么:示例改成 extends BaseSchema,只保留插件自己的两个成员 typeprops(props 留下是因为指南自己的入口示例和平台的 element:* 渲染器都在读它)。一个文件,7 行换 7 行,453 → 453。最有力的佐证:我们自己的插件脚手架 create-plugin 早就生成 extends BaseSchema 的形状,指南教的是生成器已放弃的写法,现在两边对上了。

验证:门禁全绿,另用 tsc 对着构建产物做了正反两腿证明——新写法接受三种 hidden 写法并可赋给真实 BaseSchema,旧写法逐条拒绝。

要请您留意的:这段 fence 没加 os:check 标记,因为它另一半的 MyWidgetProps 用了第三方名字 ColumnDef,本卡不裁;席位裁定不为此把第三方类型拉进技能解析表。

席位意见:建议合并。受管面(skills/**),人工合并;合并后 #8103 关闭——这是 #7434 词汇缺口的第四也是最后一个面(文档与 README 已合;AGENTS.md、两段技能 fence 与本指南都在等您)。


Generated by Claude Code

@os-zhuang
os-zhuang marked this pull request as ready for review September 7, 2026 03:27
@os-zhuang
os-zhuang added this pull request to the merge queue Sep 7, 2026
Merged via the queue into main with commit c851160 Sep 7, 2026
30 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-8103-plugin-guide-extends-baseschema branch September 7, 2026 03:49
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 needs-user-decision

Projects

None yet

2 participants