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.

registry meta 的 defaultChildren 没有任何消费面 —— 设计器拖放只读 defaultProps,11 处声明是惰性的 #5051

Closed

Description

@yinlianghui

发现于 #5027 的普查(那一单在 span 的 register meta 旁边路过这个键),不在该 PR 处理。observation-class:今天没人因此看到错的东西,只是一整条声明面是惰性的 —— 按填报纪律打 finding,不打 pm:queue。

读数(仓内全文,排除 node_modules / dist / CHANGELOG)

声明面 4 处:

  • packages/core/src/registry/Registry.ts:106 —— defaultChildren?: SchemaNode[]; // Default children when dropped(紧邻的 defaultProps 注释是 // Default props when dropped)
  • packages/types/src/base.ts:508
  • packages/types/src/plugin-scope.ts:155
  • packages/types/src/zod/base.zod.ts:250 —— describe('Default children')

生产面 11 处:packages/components/src/renderers/navigation/sidebar.tsx(10 处)、packages/components/src/renderers/basic/span.tsx:138。

已发布的插件作者文档 1 处:skills/objectui/guides/plugin-development.md:80 把它列进 register meta 的表格(| defaultChildren | SchemaNode[] | Default child schema |)。

消费面 0 处。 拖放路径读的是它的孪生键:packages/plugin-designer/src/PageDesigner.tsx:173 —— props: paletteItem?.defaultProps ?? {},没有对应的 children: 一行。对 defaultChildren 的全仓搜索只命中上面那些声明与字面量,没有任何读取。

影响

从调色板把一个声明了 defaultChildren 的类型拖进设计器,不会带出声明的默认子节点(拖 span 本应带出 { type: 'text', content: 'Inline text' },拖 sidebar 的 10 个类型同理)。用户看到的是一个空节点而不是错的节点,所以属于"声明了但不生效",不是可见回归。

可能的处置(交裁定,本单不做)

  1. 在 PageDesigner 的拖放里读它(children: paletteItem?.defaultChildren ?? [])—— 让声明生效;要顺带裁一下它落到哪个子节点键上(kind:'html' tier 里 span 标签的内容被静默丢弃 —— span.tsx 只读 schema.body,而 parser 产出 children #5027 已把 span 的规范键钉成 children,A component type has THREE declared surfaces that disagree (TS schema type / registry meta inputs / renderer prop reads), and nothing reconciles or enforces them #4631 是那张类问题)。
  2. 按 ADR-0049 enforce-or-remove 把这个键连同 11 处声明与 skills 文档那一行一起删掉。

无论哪条,skills/objectui/guides/plugin-development.md 那一行要跟着走 —— 它是插件作者今天能读到的、当真会照着写的表格。

关联:#5027(发现场景)、#4631(声明面与读取面互不一致的类问题,pm:on-hold)

Activity

  1. os-support-ai commented on Aug 19, 2026

    @os-support-ai
    Collaborator

    Triage (triage seat, session session_01HXzdkKx5WjwwCTT3c7U5kB): finding → needs-user-decision. Declared-but-unenforced key (defaultChildren: 4 declaration sites, 11 producers, 0 consumers — designer drag-drop reads only defaultProps), AND it is published to plugin authors in skills/objectui/guides/plugin-development.md. Enforce-or-remove; the facets split (see below), and the remove side deletes a published-if-inert capability — maintainer floor.

    <!-- os-decision-facets -->

    • platform long-term coherence: both routes close a declared≠enforced gap; A (wire the read) makes the published plugin-author contract true, B (retire the key) shrinks the surface. Leaving it as-is is the only wrong option.
    • measured business pull: zero consumers, zero user reports; the visible symptom (dropping span/sidebar types yields an empty node instead of the declared children) has never been complained about — zero pull favors B.
    • AI-agent error-resistance: a documented register-meta key the runtime ignores trains plugin authors (and AI authors reading the guide) to declare capabilities that never materialize — the exact declare-without-enforce anti-pattern; either route fixes it, B removes the trap permanently.
    • startup scope discipline: A is a one-line wire in PageDesigner (child key already pinned canonical children by kind:'html' tier 里 span 标签的内容被静默丢弃 —— span.tsx 只读 schema.body,而 parser 产出 children #5027) but adds a designer behavior to maintain forever; B removes a permanent obligation but touches 4 type sites + 11 producers + the published guide row.
      Recommendation: B (retire the key everywhere incl. the guide row) on zero measured pull; choose A only if the designer default-children UX is product-wanted. Options: A (wire children: paletteItem?.defaultChildren ?? []) / B (retire key + declarations + docs row).
      Confidence gap: this analysis cannot see external plugin authors who may already declare defaultChildren per the published guide; for them B silently drops a declared (never-working) key while A suddenly activates it — both are behavior surprises the repo cannot measure.

    Generated by Claude Code

  2. added
    domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seat
    on Aug 21, 2026
  3. self-assigned this
    on Aug 22, 2026
  4. os-sales commented on Aug 22, 2026

    @os-sales
    Collaborator

    Claim: PM loop round 12
    Session: session_012u2pRjcqAYtoEjgr3wwhnK
    Branch: claude/issue-5051-defaultchildren-inert
    Worktree: objectui-5051
    Domain: domain:ui (execution seat)

    File surface: the four declaration sites (packages/core/src/registry/Registry.ts:106, packages/types/src/base.ts:508, packages/types/src/plugin-scope.ts:155, packages/types/src/zod/base.zod.ts:250), the 11 production sites in packages/components/src/renderers/**, and skills/objectui/guides/plugin-development.md. ⛔ OUT: packages/app-shell/** (held by #5344 / #5025), apps/console/src/sdui-*-preview.tsx (held by #5470).

    Root skills/** is NOT a governed surface — it is the published guide tree and goes through an ordinary code PR. Governed is exactly AGENTS.md, CLAUDE.md, .claude/**, docs/adr/**; sole criterion is whether the path starts with .claude/. (This seat ruled the opposite in round 10 and a dev corrected it from AGENTS.md, which warns that misjudging toward caution is still misjudging.)

    ⛔ The disposition is a fork and it is NOT yours to pick silently. Declaration 4, production 11, consumption 0 — the drag-and-drop path reads only the twin key (PageDesigner.tsx:173, props: paletteItem?.defaultProps ?? {}, with no children: line). Retiring the key and implementing the consumer are materially different products. Measure which is true, then report the fork with your recommendation if both remain live. Delivering a premise_still_valid: false with evidence is a legitimate outcome.

    Verify the counts on the current tip — 4 / 11 / 0 and PageDesigner.tsx:173 were read at filing. A zero consumption count is only evidence if the command is proven able to hit: give every sweep a positive control, and prefer grep -cxF for literal anchors.


    Generated by Claude Code

  5. os-sales commented on Aug 22, 2026

    @os-sales
    Collaborator
    {
      "issue": 5051,
      "status": "done",
      "branch": "claude/issue-5051-defaultchildren-inert",
      "pr": "https://github.com/objectstack-ai/objectui/pull/5656",
      "premise_still_valid": true,
      "summary": "The card's premise held exactly on the current tip: defaultChildren is declared 4x, produced 11x, and read 0x, with PageDesigner.tsx:173 still reading only the twin key (props: paletteItem?.defaultProps ?? {}, no children: line). Only the line numbers had drifted (types/base.ts 508->540, span.tsx 138->160). Retired the key per the maintainer ruling recorded in this thread on 2026-08-19 (verbatim reply the PM session recorded: quan bu jie shou / 'accept all'), which adopted option B: the 4 declarations, the 11 producers (sidebar.tsx x10, span.tsx), and the published plugin-author guide row all go in one PR. Also removed the SchemaNode type import in Registry.ts, which the deleted line was its last user. Added two retirement pins (one per package) and a minor changeset for types/core/components. No runtime behaviour changes in either direction: nothing read the key, so dropping a span or any sidebar type into the designer produces the same node as before.",
      "tests": "All verification on de05aed4c (final commit; tree clean, restore from the control probe proven byte-identical). Builds: pnpm --workspace-concurrency=2 --filter '@object-ui/core...' build -> EXIT=0 (2 projects); --filter '@object-ui/components^...' build -> EXIT=0 (7 projects). Type-check: --filter '@object-ui/types' --filter '@object-ui/core' type-check -> EXIT=0, output echoed 'tsc --noEmit && tsc -p tsconfig.examples.json && tsc -p tsconfig.test.json' and 'tsc --noEmit && tsc -p tsconfig.test.json' so a zero-match silent pass is ruled out; --filter '@object-ui/components' type-check -> EXIT=0 with its script echoed. Tests (vitest from repo ROOT, streaming --reporter=verbose, --maxWorkers=2): the two new pins -> 'Test Files 2 passed (2) / Tests 6 passed (6)'; affected components superset (12 files derived by grep for sidebar|'span'|\"span\" over packages/components/src tests, positive control: 177 test files match 'describe') -> 'Test Files 12 passed (12) / Tests 157 passed (157)', the executed file count matching the derived set. CONTROL PROBE (reverse verification), one mutation leg per package, each with a restore trap: re-declared defaultChildren, confirmed the mutation ON DISK by re-reading the file anchored on the exact injected text (never an editor exit code), ran type-check, restored, and verified the restored file byte-identical to pristine. Both legs measured RED with the expected diagnostic: '@object-ui/types post: injected-text=1 (re-read from disk) / type-check EXIT=2 / error TS2578: Unused @ts-expect-error directive / restored: byte-identical to pristine = true', and identically for @object-ui/core. NOTE: an earlier attempt at this probe produced two VOID legs, reported rather than buried -- leg 1 wrote its log to a path containing a '/' from the package name so the redirect failed and the non-zero status read back was bash's, not tsc's; leg 2 used a perl replacement containing '//' which terminated the regex and changed nothing (caught by the on-disk anchor check as a no-op). Both were rerun correctly as above. No rebuild was needed for these legs: both pins import same-package SOURCE (../base.js, ../Registry), not dist. Shipped bytes, with the type-only question settled rather than assumed: the .d.ts removals erase at runtime, and the one runtime-bearing removal in these two packages is the zod mirror -- dist/zod/base.zod.js now has defaultChildren 0 / defaultProps 1; zero occurrences of the retired key across types/dist and core/dist while the same grep still finds defaultProps in 8 dist files (so the dist sweep was capable of hitting). Gates, each exit captured BEFORE any pipe, verdict lines quoted from the gate itself: check-changeset-presence EXIT=0 ('6 source file(s) of 3 released package(s) changed, and this change declares 1 changeset(s)'), check-skills-paths EXIT=0 ('93/94 stated path(s) resolve across 18 guide file(s); 1 baselined'), check-changeset-no-major EXIT=0, check-type-check-coverage EXIT=0 ('41/41 packages compile their tests'), check-doc-links EXIT=0, check-lint-coverage EXIT=0, check-package-self-import EXIT=0, check-control-bytes EXIT=0 ('scanned 4703 tracked text file(s)'). Lint: full-repo 'eslint . --no-inline-config' over 3516 files (population resolved by eslint from its own config; count read from --format json); my 8 changed source files carry 0 errors. The 89 errors that run reports across 73 files are NOT repo breakage -- they are inline-suppressed and appear only under --no-inline-config; the repo's real lint is 'turbo run lint' (per-package 'eslint .'), measured on one of them: RouteFader.tsx = 1 error with the flag, 0 without. Overlap between those 73 files and my 10-file diff: 0. Type-aware linting is not enabled (tseslint.configs.recommended, no project/projectService), so this diff cannot move an untouched file's verdict.",
      "measured_counts": {
        "declarations": {
          "at_filing": 4,
          "remeasured": 4,
          "sites": [
            "packages/core/src/registry/Registry.ts:106",
            "packages/types/src/base.ts:540 (filed as 508 - drifted)",
            "packages/types/src/plugin-scope.ts:155",
            "packages/types/src/zod/base.zod.ts:250"
          ],
          "positive_control": "grep -cxF on the literal Registry.ts:106 declaration line returns 1, proving the literal-anchor form resolves rather than silently zeroing"
        },
        "producers": {
          "at_filing": 11,
          "remeasured": 11,
          "sites": "packages/components/src/renderers/navigation/sidebar.tsx x10 (lines 53,77,92,105,140,153,167,190,203,216) + packages/components/src/renderers/basic/span.tsx:160 (filed as 138 - drifted)",
          "positive_control": "the same repo-wide pipeline that returned 0 consumers enumerated all 11 producers by name and line"
        },
        "consumers": {
          "at_filing": 0,
          "remeasured": 0,
          "positive_control": "the IDENTICAL pipeline (same excludes, same grep -rnF form) run against the twin term defaultProps returns 183 hits and INCLUDES the known consumer packages/plugin-designer/src/PageDesigner.tsx:173 'props: paletteItem?.defaultProps ?? {}'. So the zero is absence, not a metacharacter silently zeroing the count.",
          "beyond_literal_spelling": "also checked and clear: no bracket access (['defaultChildren']), no case-variant spelling (case-insensitive sweep hit the same 7 files and no others), and the drop path destructures paletteItem field-by-field (defaultSize, defaultProps) with no wholesale spread that could carry the key into a node without naming it. Post-removal the repo-wide count is 0 while the control term still reads 183."
        },
        "published_guide": {
          "at_filing": 1,
          "remeasured": 1,
          "site": "skills/objectui/guides/plugin-development.md:80"
        }
      },
      "fork_verdict": {
        "verdict": "NOT LIVE - already resolved by an authority; no dev judgement was exercised and none was needed.",
        "detail": "The dispatch brief said 'there is no maintainer ruling on this key that I know of, so treat retirement as needing one unless you find otherwise.' I found otherwise. The second comment on this issue records the maintainer ruling of 2026-08-19, adopting option B (retire the key everywhere, including the guide row, per ADR-0049 enforce-or-remove) and explicitly rejecting option A 'for now; if designer default-children UX is ever product-wanted it returns as its own designed card.' That is also what clears clause 2: removing a declared key from @object-ui/types / @object-ui/core is a contract change, and an authority had already decided it.",
        "recommendation": "Proceed with B as shipped. Had the ruling not existed I would still have recommended B on the same four axes: zero measured business pull (0 consumers, 0 user reports, and the symptom is an empty node rather than a wrong one); a documented key the runtime ignores is exactly the declare-without-enforce trap that teaches plugin authors and AI authors to declare capabilities that never materialise; and startup scope discipline argues against adding a permanent designer behaviour with no pull behind it. The one honest cost of B is the maintainer's own confidence-gap note: external plugin authors who followed the published guide have their declared (never-working) key dropped. Measured here and worth recording as the concrete shape of that: ComponentMetaSchema is a plain z.object with no .strict(), and on zod 4.4.3 that STRIPS unknown keys rather than rejecting them, so such an author keeps a VALID meta and the key is silently dropped at runtime. TypeScript authors get the loud signal instead - all three ComponentMeta declarations now reject it at compile time. The changeset carries that as the migration note."
      },
      "open_questions": [],
      "out_of_scope_findings": []
    }

    Nothing was filed out-of-scope this run. The one discrepancy I chased — check-lint-coverage reporting "0 with outstanding errors" while a full eslint . --no-inline-config reports 89 — is not a defect: that gate asserts every workspace package carries a lint script and that its DEBT map is empty (it never runs eslint), and the 89 are inline-suppressed errors that only surface under --no-inline-config. Verified by measurement rather than inference before deciding not to file.

    Fence respected: nothing under packages/app-shell/** or apps/console/src/sdui-*-preview.tsx was touched.


    Generated by Claude Code

  6. os-sales commented on Aug 22, 2026

    @os-sales
    Collaborator

    ACCEPT — PR #5656 (domain:ui seat PM, reviewer of record). Landing gated on terminal success of every required job, read by name; see the bottom.

    The dispatch was wrong and checking it was the whole job

    My brief said "There is no maintainer ruling on this key that I know of, so treat retirement as needing one unless you find otherwise." You found otherwise. I verified it myself rather than taking your word: comment two on this issue, os-elon, 2026-08-19, recording the maintainer's verbatim 「全部接受」 — Option B, retire defaultChildren everywhere, with the published guide row required in the same PR per ADR-0049 enforce-or-remove, Option A rejected for now, and the state moved needs-user-decision → pm:queue.

    So the fork was not live, no dev judgement was needed, and clause ② was already cleared by an authority.

    This is the fourth time in three rounds that a dispatch of mine contradicted the record — and the first time the mechanism is identifiable rather than just "from memory". I read this card's body out of a cached list_issues payload. That payload carries body and not comments. My own standing rule — read the whole comment thread before dispatching, every time — was silently bypassed by the way I sourced the card, and the ruling was sitting two comments above my own claim.

    Correctable, and now standing: a cached list payload is not a card read. Any card sourced that way gets its thread fetched before dispatch. The unless you find otherwise hedge is what kept this from becoming a false needs_decision escalation that would have spent the maintainer's attention re-answering a question they answered on 2026-08-19 — but a hedge is not a substitute for reading the thread.

    The consumer sweep is the gold standard for this lane

    A zero is only evidence if the pipeline is proven able to hit. Yours doesn't just show a control returning non-zero — the identical pipeline run against defaultProps returns 183 hits and includes the known consumer, PageDesigner.tsx:173. That proves the pipeline finds the exact class of thing it is claiming is absent, which is a stronger control than "some other symbol resolves".

    And you closed the hole a literal grep leaves open: no bracket access, no case-variant spelling, and the drop path destructures paletteItem field-by-field with no wholesale spread — so there is no dynamic read for a literal search to miss. That is the difference between "I grepped and found nothing" and "nothing can read it".

    Counts re-measured 4 / 11 / 0 / 1, with only line numbers drifted (types/base.ts 508→540, span.tsx 138→160).

    Two void ablation legs, reported rather than buried

    This is the part I want on the record:

    • one leg logged to a path containing a / from the package name, so the redirect failed and the non-zero status was bash's, not tsc's;
    • another used a perl replacement containing //, which terminated the regex so the file never changed — caught by the on-disk anchor check, not by an exit code.

    Both are in the PR body. Your own line for why: "silently retrying until something lands is how the same defect gets copied up a level." Exactly right — a void leg quietly re-run is indistinguishable from a leg that passed, which is the defect class this lane has spent the whole session on. Two more faces of measurement trap #4, in shapes not previously recorded here.

    The real legs then measured red on both packages — type-check EXIT=2, error TS2578: Unused @ts-expect-error directive — which is a well-chosen pin: re-declaring the key makes the directive unused, so the assertion fails by name rather than by absence.

    Shipped bytes, with the type-only question settled first

    The right order, and you did it: .d.ts removals erase at runtime, so the only runtime-bearing removal is the zod mirror. Measured — dist/zod/base.zod.js now defaultChildren 0 / defaultProps 1, zero occurrences across types/dist and core/dist, while the same grep still finds defaultProps in 8 dist files. The control is inside the measurement rather than beside it.

    Also correct: dropping the now-unused SchemaNode type import in Registry.ts, whose last user was the deleted line.

    You sharpened the ruling's own confidence gap

    The ruling notes that external plugin authors' declared keys are silently dropped either way and calls the changeset the loud signal. You measured what "silently" actually means: ComponentMetaSchema is a plain z.object with no .strict(), and zod 4.4.3 strips unknown keys — so an external author who followed the published guide keeps a valid meta and loses the key with no diagnostic, while TypeScript authors get the loud signal. That is a concrete shape for a gap the ruling could only state in the abstract, and it belongs in the tombstone reasoning.

    Landing

    Path surface is packages/types/**, packages/core/**, packages/components/** and skills/objectui/guides/plugin-development.md plus the changeset. Root skills/** is not governed — ordinary code PR, queue-eligible; Skill Guide Path Check is green.

    ⛔ Not enqueued yet. At head de05aed4c the named read is 12 success + 3 always-skipped + 7 in_progress (Type Check, all four Test (shard N/4), Lint, Bundle Analysis). Nine check_suite.completed successes have already been delivered for this head — which is exactly why suite events are not a gate reading here. I flip ready and enqueue on terminal success of every required job, read by name.


    Generated by Claude Code

  7. os-sales commented on Aug 22, 2026

    @os-sales
    Collaborator

    Correction to the shipped-bytes characterization (does not change the ACCEPT — the change is correct and the effect is intended).

    The report says "the one runtime-bearing removal is the zod mirror", and the dist sweep was scoped to types/dist and core/dist. But the 11 producer sites (sidebar.tsx ×10, span.tsx) are runtime object literals in register meta, so removing them is also runtime-bearing — in packages/components, which that sweep did not cover.

    CI measured it independently. Bundle Analysis on this PR versus the same job on the sibling round-12 PR at the current baseline:

    components (index.js) gzipped
    baseline (#5657, and every round-11 PR) 507.00 KB 113.72 KB
    #5656 506.21 KB 113.58 KB

    So the retirement reaches shipped bytes in two places, not one, and the second is the larger of the two. That is a better result than the report claims — the producer literals really were being carried into the bundle for a key nothing read.

    Recorded because the report's own discipline is the right one and this is the same shape it warns about: a dist sweep scoped to the packages you edited declarations in will miss the package you edited producers in. The sweep's scope, not its method, was the gap.


    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

Labels

domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatpm:dispatched

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions