Skip to content
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
This repository was archived by the owner on Nov 4, 2026. It is now read-only.

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

Closed

Description

@os-steve

Filed by the domain:skills @ objectui execution seat while implementing objectui#8065 (branch claude/issue-8065-skills-baseschema), unassigned, finding only — grading and domain:* are triage's. ⛔ Not repaired in that PR: the dispatch's file surface is exactly two files (skills/objectui/guides/architecture.md, skills/objectui/rules/protocol.md) and skills/** is a governed surface, so this cannot ride along.

⚠️ Generic parameters are spelled in WORDS below, 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, and this card is about type declarations.

The observation

skills/objectui/guides/plugin-development.md:219-228, under "Type definitions", teaches a plugin author to declare their widget's node type like this (verbatim except for the angle-bracket avoidance, which does not occur in these ten lines):

export interface MyWidgetSchema {
  type: 'my-widget';
  id?: string;
  className?: string;
  bind?: string;
  props?: MyWidgetProps;
  hidden?: string;
  disabled?: string;
}

Two readings, measured at origin/main d3499b31:

  1. hidden and disabled are narrower than what the renderer accepts. The real declarations are packages/types/src/base.ts:376 and :412 — each is boolean OR ExpressionWire, and ExpressionWire (packages/types/src/expression.ts:67) is itself a string OR the CEL envelope object. SchemaRenderer evaluates them through evaluateCondition, whose parameter is string | boolean | undefined with the envelope routed to @objectstack/formula (documented in that same base.ts docblock, ruled 2026-09-04 on objectui#7530). So a plugin author who copies this fence declares a type that refuses hidden: true and refuses the envelope — both of which the shipped renderer accepts and acts on. This is the same shape objectui#8092 was graded a card for on root AGENTS.md.
  2. The interface re-states members rather than extending the published one. MyWidgetSchema does not extend BaseSchema, so a plugin schema typed this way silently loses visible / visibleWhen / children / body / testId / ariaLabel and the rest. The repo's own published node types do the opposite — ActionSchema extends BaseSchema (packages/types/src/crud.ts:40), EventableSchema extends BaseSchema (packages/types/src/api-types.ts:278).

Re-derive:

sed -n '217,231p' skills/objectui/guides/plugin-development.md
sed -n '376p;412p' packages/types/src/base.ts

Why it is worth a card rather than a footnote

Under the maintainer's admission threshold this is the metadata-authoring-trap class, and it is the published surface: a plugin author (or an agent) copying this block gets a declaration that is wrong in the quiet direction — the JSON still renders, and the mismatch only surfaces later at the member, exactly the failure direction objectui#8092 recorded.

⚠️ Nothing catches it. pnpm check:skill-examples is opt-in and this fence carries no marker (the next marker in the file is on the fence below it), so no run has an opinion about it; and even if it were marked it would pass, because the gate compiles a fence as a self-contained program — that is objectui#7646's finding, measured with a two-arm control there.

Deliberately NOT proposed here

⛔ This seat is not choosing the repair. The obvious shapes each have a cost worth pricing by whoever takes it: make the fence extends BaseSchema and drop the four re-stated members (shortest, and correct, but changes what the tutorial is showing); keep the standalone interface and correct the two declarations only; or leave it and add the pointer sentence. ⛔ Also not proposed: a new gate over unmarked fences — that is a new scanning population, the class objectui#7646 and objectui#5250 already hold.

Dedup — run with a live control

Channel: repo-scoped REST (/repos/.../issues?state=open, 4 pages, 383 open issues read) plus local grep of every title and body; the REST /search/* endpoint answers 403 from this container, and no MCP search call was spent. Live control: the token BaseSchema matches 43 of the 383, so a zero for the query terms is a real zero and not a broken read. Query terms plugin-development, MyWidgetSchema, MyWidgetProps matched three cards, none of them this:

  • objectui#7646 — 26 doc fences re-declare a published type name instead of importing it, so the gate judges a private copy. Adjacent and the reason nothing catches this one; its census keys on fences declaring a name a workspace package already exports, and MyWidgetSchema is not such a name.
  • objectui#7493 — ComponentInput.defaultValue?: any and the bare-any baseline row at plugin-development.md:92. Same file, different line, different contract.
  • objectui#5174 — the doc-snippet scanner collecting only .mdx under content/docs. Coverage, not content.

Same family, checked, neither is this: objectui#8065 (the two fences carrying the retired UIComponent name — being repaired now, and this file is outside its ruled surface), objectui#8092 (root AGENTS.md's member list), objectui#8093 (extending the vocabulary pin's corpus once both governed halves land — that pin bans two name spellings, so it would not see this at all).

Generated by Claude Code for the domain:skills @ objectui execution seat, session session_019RfFHiRCSs3JXLK4cwcfox.


Generated by Claude Code

Activity

  1. claude commented on Sep 6, 2026

    @claude
    Contributor

    Claim: PM loop round 8 (continued) — flight O, one governed objectui skills card: skills/objectui/guides/plugin-development.md's example plugin node interface (MyWidgetSchema) hand-rolls BaseSchema's members instead of extending it and declares hidden / disabled as plain strings, so a copied declaration refuses hidden: true and the expression envelope the shipped renderer accepts. The seat's ruling: the example extends BaseSchema and keeps only the widget's own members; any member it still spells takes the real declaration (the amended #8092 test — a member stays only with a real reader). Same class as #8092 / #8065, fourth surface.
    Session: session_019RfFHiRCSs3JXLK4cwcfox
    Branch: claude/issue-8103-plugin-guide-extends-baseschema
    Worktree: objectui-issue-8103
    Domain: domain:skills
    File surface: skills/objectui/guides/plugin-development.md only (net-zero or net-negative lines). ⛔ No other file.
    Container & model: XS, mode:subagent, opus — no dispatch-gates.mjs in objectui; prose in a published skill, no contract move.
    Clause-②: no
    Serial: no open PR touches this guide (PR #8104 holds guides/architecture.md + rules/protocol.md; PR #8091 holds guides/schema-expressions.md; PR #8090 holds root AGENTS.md). GOVERNED (skills/**) ⇒ draft PR, in-seat review, os-zhuang + hotlong, human merge; objectui has no skip-changeset label — the changeset gate decides; at ACCEPT the seat adds needs-user-decision + the 维护者速读. The PR carries Fixes #8103.


    Generated by Claude Code

  2. claude commented on Sep 6, 2026

    @claude
    Contributor

    os-dev-report

    {
    "issue": 8103,
    "status": "done",
    "branch": "claude/issue-8103-plugin-guide-extends-baseschema",
    "pr": "#8109",
    "premise_still_valid": true,
    "summary": "Premise re-verified on origin/main d327b9c (the card measured d3499b3) before editing: the fence is still at skills/objectui/guides/plugin-development.md:219-237, still lists type / id / className / bind / props / hidden / disabled, and BaseSchema still declares hidden and disabled as boolean OR ExpressionWire at packages/types/src/base.ts:376 and :412. The fence now imports BaseSchema from @object-ui/types the way this file's other fences import (line 194 is the model), extends it, and keeps only the widget's own two members: the type literal and props. props was KEPT under the amended objectui#8092 test (a member stays only with a real reader on the wire) and the dispatch's verification question is answered in the direction it asked about: this guide's OWN renderer example at line 158 spreads schema.props onto the component, and the platform's element:* reader is packages/components/src/renderers/basic/readProps.ts:70 (the dispatch named packages/react/src/readProps.ts, which does not exist on this tree). id / className / bind / hidden / disabled were dropped as inherited. The strongest authority found is in-repo and was not in the card: packages/create-plugin/src/templates.ts:438-446 — the shipped scaffolder — already emits exactly this shape for every generated plugin, and its docblock at :413-427 states the reason in the card's own terms ('a second dialect of a base node the protocol already defines', AGENTS.md commandment 0.1). The guide was teaching the shape the generator had already abandoned. File is line-neutral: 453 lines in, 453 out, 7 insertions / 7 deletions, one file. Measured and reported per dispatch clause 1: NOTHING in this repo line- or token-pins skills/** — the two scripts whose names carry 'budget' mention 'skills' zero times each, and check-skill-eval-tokens measures eval must_contain tokens against the bundle text, not file size. No prose to fold (clause 4): the only text between the fences is the heading '### Type definitions'; the three other 'disabled' hits in the file are FieldWidgetComponentProps.disabled, a different contract. The fence is UNMARKED (markers re-taken on this tree: 91, 243, 333, 369 — the one at 243 opts in the fence at 244), and no marker was added; the reason is measured, see tests. PR #8109 is a DRAFT at the governed terminal: not flipped ready, not enqueued, no auto-merge, no approval sought, no assignee or label touched.",
    "tests": "Every gate run as 'cmd > FILE 2>&1; EXIT=$?' — no exit code read through a pipe — and every verdict quoted below is the gate's own printed line. Dependency closure built first through the shared verify lock (the filter set came from 'node scripts/check-skill-examples.mjs --build-filter'), exit 0, held 202s / waited 63s. [1] 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 the edit: 13 ts / 70 json. AFTER: 13 ts / 70 json — unchanged, and the edited fence is not in that population. [2] pnpm check:skill-eval-tokens exit 0 — 'Red under the chosen oracle: 0 (0 beyond the baseline)', 125 must_contain tokens over 16 guides. [3] pnpm check:skills-paths exit 0 — '88/89 stated path(s) resolve across 20 guide file(s); 1 baselined' (unchanged). [4] pnpm check:doc-fences exit 0 — 'every TypeScript block in 227 document(s) is fenced ts/tsx/typescript'. [5] node scripts/check-governed-queue-guard.mjs --test skills/objectui/guides/plugin-development.md — EXIT 3, 'GOVERNED — 1 of 1 path(s) are on a governed surface: skills/** x1 — the published skills catalog'. Followed: draft, no ready flip, no queue, no auto-merge. [6] 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.' objectui has no skip-changeset label; the gate's own verdict is followed, so no changeset file was added, and CI's 'Changeset Declaration' check is green on the head. [7] pnpm check:control-bytes exit 0 — 'scanned 6494 tracked text file(s)'; plus a self-scan of the edited file with the control-byte class regex: no match. [8] Whole-repo pnpm lint through bash scripts/pm/os-verify-lock.sh (objectstack's, slot issue-8103-dev, acquired once, held 5m04s, waited 0s) — VERDICT command-exit 0, 'Tasks: 47 successful, 47 total', '0 errors, 2900 warnings' (all pre-existing no-explicit-any in package source). [9] Gate population re-derived rather than taken from the dispatch (objectui has no dispatch-gates.mjs): 'git grep -ln' for the skills root across scripts/tests and scripts/.mjs found six suites that scan this tree. All six run through the lock in one vitest call: 'Test Files 6 passed (6) · Tests 236 passed (236)', exit 0. The one that pins rows IN THIS FILE is doc-version-claims.test.ts (its react peer range and lucide-react entries at :1168 / :1174); neither row is touched and both pass. [10] MARKER MEASUREMENT, both trees: 'node scripts/check-skill-examples.mjs --measure' (judges every candidate, marked or not) reports the fence at 219 as FAIL before AND after, and the file's semantic diagnostic list is IDENTICAL across the two trees — 25 rows, same lines, same codes. The one 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 (the barrel exports LookupColumnDef and GridColumnDefinition, not this name), so it is a third-party name. The ruled half (MyWidgetSchema) contributes ZERO diagnostics on both trees, and the new import contributes none: grep -c on 'TS2307.@object-ui/types' over the whole measurement is 0, so the specifier resolves. No marker was added: earning it would mean importing a third-party name into a fence half this card does not rule, on a governed file, at net-positive lines. [11] TSC PROOF against the BUILT packages/types/dist/index.d.ts (--strict --skipLibCheck --moduleResolution bundler, specifier mapped through paths), three arms, all three needed: POSITIVE exit 0 — a MyWidgetSchema literal carrying id / className / bind / props (all now inherited) plus hidden: true and disabled as a predicate string, a second literal with hidden as the CEL envelope object, both assigned onto a real BaseSchema, and 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 boolean not assignable to string, TS2322 the CEL envelope object not assignable to string, TS2353 ariaLabel does not exist. Without this arm the positive arm proves nothing. RESOLUTION CONTROL — a planted missing export from @object-ui/types answers TS2305 ('has no exported member'), not TS2307, so the mapping reached the real built d.ts and the positive exit 0 is not vacuous. The proof files lived in an untracked directory deleted before the commit; git status --porcelain on the committed tree was exactly one modified path. [12] CI on head fc5f332, polled in the foreground until every run completed (dispatch clause 7): 28 check runs, 0 pending, 25 success, 3 skipped, ZERO failures. Named green: Lint, Type Check, Build & E2E, Build Docs, Test shards 1-4, Skill Example Check, Skill Eval Token Check, Skill Guide Path Check, Doc Fence Language Check, Doc Snippet Type Check, Doc Component Type Check, Control Byte Scan, Changeset Declaration, Changeset Fixed Group Check, Governed Surface Queue Guard, README Export Check, Internal Docs Link Check, Pre-Install Import Graph Check, Shell Escape Residue Scan, Docs Route Eager Closure Check, Inert vi.mock Specifier Check. All local readings above were taken on the tree that is commit fc5f332 — 'git rev-parse --short HEAD' at the last run: fc5f332, the single commit on this branch. PR body read back in full after creation: byte-identical to what was sent except one blank line GitHub normalised away after a trailing rule; the session-URL signature footer survived (create-only form, never edited since). Worktree cleaned up: node_modules removed, 'git worktree remove' WITHOUT --force, exit 0.",
    "mcp_calls": "0 — the whole run went through repo-scoped REST with curl (probe: GET /repos/objectstack-ai/objectui = 200); gh CLI is absent from this container. Zero GraphQL/MCP quota spent.",
    "open_questions": [
    {
    "question": "The fence cannot earn an os:check marker while MyWidgetProps names ColumnDef, which resolves to nothing in this workspace. Should a follow-up make the block compile so the gate protects it, and if so how?",
    "options": [
    "A. Leave it unmarked (what this PR did). The standalone tsc proof already establishes the ruled half against the built d.ts, at zero cost to a governed file's line count.",
    "B. Import ColumnDef from its real home (TanStack) and add the marker. Makes the block gate-protected forever, but costs net-positive lines on a governed file and pulls a third-party specifier into the skills resolution map.",
    "C. Replace ColumnDef with a locally-declared stub inside the fence so the block is self-contained, then add the marker. Self-contained and net-small, but the example then teaches a fake column type where a plugin author needs the real one."
    ],
    "recommendation": "A for this card, and B or C only as a separately dispatched card if the seat wants the coverage. 实际业务需求: the measured harm this card exists to stop was the two narrowed predicate declarations and the silently-lost inherited members, and both are gone regardless of the marker; the marker buys future drift protection, not a present defect. 项目长远合理性: contract-first argues for the marker eventually, but the fence's remaining obstacle (ColumnDef) belongs to the MyWidgetProps half, which is a different question from the node contract this card rules — bundling them makes the card's ruling harder to review, not easier. 防 AI 写错: the ruled half is what an AI copies into its first schema, and it is now correct and provably so; a marker would guard against a future edit, which is a weaker threat than the one just removed. 创业阶段不扩散: adding a third-party specifier to the skills type-resolution map is a scanning-population expansion with no pull behind it, and this repo already holds two cards (objectui#7646, objectui#5250) on exactly that class."
    },
    {
    "question": "The guide's entry-point fence at line 158 spreads the node twice — schema first, then schema.props — so a node-level key and a props key of the same name collide silently with props winning. Is that the shape the guide should teach?",
    "options": [
    "A. Leave it (this PR did). It is load-bearing evidence that props has a real reader here, and it is outside this card's ruled fence.",
    "B. File a card to align the example with objectui#5123's precedence ruling on the platform's own two channels.",
    "C. Fold a one-line warning into the existing example."
    ],
    "recommendation": "A now, B if the seat judges it above the admission threshold. 实际业务需求: nobody has reported the collision, and the example still renders; the harm is hypothetical where this card's was measured. 项目长远合理性: if it IS aligned, it should be aligned with the platform's ruled precedence rather than by taste, which is a ruling this seat has not taken. 防 AI 写错: a silent same-name collision is exactly the quiet direction, so it is a genuine candidate — but it is a different fence and a different contract from the one ruled here. 创业阶段不扩散: recorded in the PR's 验收备注 rather than filed, so nothing is lost if the seat rules it a note."
    }
    ],
    "out_of_scope_findings": [
    "NOT filed as cards — recorded in the PR's 验收备注 section per dispatch clause 6 (the maintainer's admission threshold: three classes file a card, the rest is noted), so they reach the seat without spending a card: (1) ColumnDef in MyWidgetProps resolves to nothing in this workspace and is the sole reason the fence cannot be marked — see open_questions[0]; (2) the entry-point fence at line 158 double-spreads the node, see open_questions[1]; (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 authority behind the change and a future edit to either should keep them in step. If the seat wants any of these as a card, say so and this branch's session will file it with a dedup run."
    ]
    }


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions