docs(skills): make the plugin guide's node interface extend BaseSchema - #8109
Conversation
`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
|
ACCEPT — governed in-seat review of PR #8109, head Implemented-by: Verified in a detached compare worktree at the PR head, not from the report:
Rulings on the two open questions
Landing regime: governed ( Generated by Claude Code |
维护者速读事情:发布技能包里的《插件开发指南》教插件作者写节点类型时,把平台已定义的基础节点字段手抄了一遍,还抄错两行—— 改了什么:示例改成 验证:门禁全绿,另用 要请您留意的:这段 fence 没加 席位意见:建议合并。受管面( Generated by Claude Code |
Fixes #8103
The "Type definitions" fence in
skills/objectui/guides/plugin-development.mdtaught 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 declarestwo of them narrower than the shipped renderer accepts. It now imports
BaseSchemafrom@object-ui/typesthe way this file's other fences import,extends it, and keeps only the widget's own two members.
never in angle-bracket shape. This repo's
AGENTS.mdrecords that GitHub'sbody 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
skills/objectui/guides/plugin-development.md⛔ No other file.
git diff --staton 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.mjsandscripts/render-budget-comment.mjs— mention
skillszero times each (grep -c skills), andscripts/check-skill-eval-tokens.mjsmeasures evalmust_containtokensagainst 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:
⇒ 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
import type { BaseSchema } from '@object-ui/types';import type { MyWidgetProps } from './types';at line 194.BaseSchemais a real export:packages/types/src/base.ts:70, barrelpackages/types/src/index.ts:95export interface MyWidgetSchema {export interface MyWidgetSchema extends BaseSchema {packages/create-plugin/src/templates.ts:438-446emitsimport type { BaseSchema } from '@object-ui/types';thenexport 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 abandonedtype: 'my-widget';BaseSchema.type(base.ts:75,string) to a literal is the one member a widget legitimately owns; identical to what the scaffolder emitsid?: string;base.ts:81, declared identically. Re-stating it buys nothingclassName?: string;base.ts:171, declared identicallybind?: string;base.ts:248, declared identicallyprops?: MyWidgetProps;read by the entry point's {...schema.props}LazyMyWidget SPREAD-schema SPREAD-schema.props— the plugin renderer reads its config fromschema.props, which is what the dispatch asked to verify, and the answer is thatpropsis the right member for this example. (2) The platform'selement:*reader,packages/components/src/renderers/basic/readProps.ts:70—isConfigBag(schema?.props) ? schema.props : {}.BaseSchemadoes not declarepropsas a named member; it is admitted by the index signature atbase.ts:464([key: string]: any), which is the TS-side half of why the wire test, not the interface test, is the right onehidden?: string;packages/types/src/base.ts:376declareshidden?: boolean OR ExpressionWire.ExpressionWire(packages/types/src/expression.ts, exported atpackages/types/src/index.ts:119) is itself a string OR the CEL envelope object. The old line refusedhidden: trueand refused the envelope — both of which the shipped renderer accepts and acts ondisabled?: string;packages/types/src/base.ts:412, same wirepackages/types/src/base.tsProse 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 filereturns no prose sentence describing the hand-rolled members — the three other
disabledhits (lines 253, 263, 281) areFieldWidgetComponentProps.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
tscagainst the builtpackages/types/dist/index.d.ts(
--strict --skipLibCheck --moduleResolution bundler, the specifier mappedthrough
paths), a program carrying the fence verbatim (ColumnDefstubbed —that name is third-party and outside this card's ruled half):
MyWidgetSchemaliteral carryingid/className/bind/props(all now inherited) plushidden: true,disabledas a predicate string, and a second literal withhidden: { dialect: 'cel', source: "record.status == 'draft'" }; bothassigned onto a real
BaseSchema; plus a third literal carryingname/visible/visibleWhen/body/children/testId/ariaLabel—the members a copied subset silently loses.
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.
@object-ui/typesanswersModule "@object-ui/types" has no exported member ..., notTS2307. So the mapping reached the real built d.ts and thepositive arm's exit 0 is not vacuous.
The proof files lived in an untracked directory that was deleted before the
commit;
git status --porcelainon the committed tree is exactly one modifiedpath.
Why no
os:checkmarker was addedThe 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 everycandidate, marked or not — says it does not, for a reason this change does
not own:
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 theMyWidgetPropshalf:ColumnDefis declared nowhere in this workspace(
git grep -nE '(interface|type) ColumnDef' -- packages/*/srcis empty; thebarrel exports
LookupColumnDefandGridColumnDefinition, not this name), soit is a third-party name a plugin author would import. The ruled half —
MyWidgetSchema— contributes zero diagnostics on both trees, and the newimport contributes none either:
grep -c "TS2307.*@object-ui/types"over thewhole measurement is 0, so the specifier resolves.
⇒ Earning the marker would mean adding a
ColumnDefimport to a fence halfthis card does not rule, on a governed file, at net-positive lines. Left alone;
the standalone
tscproof above is the stronger evidence and costs the filenothing.
Gates — every exit captured before any pipe, on head
fc5f332bEach was run as
cmd > FILE 2>&1; EXIT=$?— no exit code was read through apipe, and each verdict below is the gate's own printed line, not a bare
$?.pnpm check:skill-examplesMarked: 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 itpnpm check:skill-eval-tokensRed under the chosen oracle: 0 (0 beyond the baseline);125 must_contain token(s)scored across 16 guide files. No eval token is spelledMyWidgetSchema,id-as-a-member, or the two removed predicate declarations, so nothing an eval grades was editedpnpm check:skills-paths88/89 stated path(s) resolve across 20 guide file(s); 1 baselined(unchanged; the newpackages/types/src/base.tsmention 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-fencesevery TypeScript block in 227 document(s) is fenced ts/tsx/typescriptnode scripts/check-governed-queue-guard.mjs --test skills/objectui/guides/plugin-development.mdnode scripts/check-changeset-presence.mjs1 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 noskip-changesetlabel; the gate's own verdict is followed, so no changeset file is addedpnpm check:control-bytesscanned 6494 tracked text file(s); skipped 85 binary. Self-scan of the edited file with the control-byte class regex: no matchpnpm exec vitest run --maxWorkers=2over 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-residueTest Files 6 passed (6) · Tests 236 passed (236). The population was derived bygit grep -lnfor the skills root acrossscripts/__tests__andscripts/*.mjs, not from the dispatch's list.doc-version-claims.test.tsis the one that pins rows in this file (itsreact": "^18.0.0andlucide-reactentries at:1168/:1174); neither row is touched and both still passpnpm lintthrough the shared verify lock (acquired once, held 5m04s, waited 0s)Tasks: 47 successful, 47 total,0 errors, 2900 warnings; every warning is pre-existingno-explicit-anyin package sourceAll of the above were run on the tree that is
fc5f332b— the single commit onthis branch — and
git rev-parse --short HEADat the time of the last run isfc5f332b.验收备注
Noted and deliberately not filed — none clears the maintainer's admission
threshold on its own:
ColumnDefname inMyWidgetPropsresolves to nothing in thisworkspace. 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:checkmarker today. Recorded ratherthan 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.
LazyMyWidget SPREAD-schema SPREAD-schema.props. That is what makespropsa real reader here and so it is load-bearing for this change, but it also
means a node-level key and a
propskey of the same name collide silently,with
propswinning. Adjacent to objectui#5123's precedence ruling on theplatform'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.
packages/create-plugin/src/templates.tsand this guide now agree, whichthey 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.
AGENTS.md, and objectui#8065 isrepaired 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.mdrecords: the parser does not read sentences, and a negation does not save you.
维护者速读(草稿)
我们发布给客户和 AI 的技能包里,有一份《插件开发指南》。它教插件作者:你的组件节点类型
长这样——然后把平台早就定义好的基础节点字段(
id、className、bind、hidden、disabled)手抄了一遍,自成一份接口。问题不在于啰嗦,而在于抄错了两行:hidden和disabled被写成纯字符串,而线上渲染器实际接受布尔值和 CEL 表达式对象。照着抄的人(尤其是 AI)写出来的类型,会拒绝
hidden: true,也会拒绝表达式信封——而这两种写法平台都支持、都会执行。
更要紧的是第二层:因为没有继承,这份手抄接口把没抄到的成员静默丢掉了——
name、visible、visibleWhen、body、children、testId、ariaLabel全都不在。插件作者不会收到任何警告,只会在某天写了
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