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

finding: the DOM-leak sweep never reaches packages/components/src/renderers/** — four layout/basic renderers still carry the unfixed spread #5574

Closed

Description

@os-sales

Observation-class finding, recorded while implementing #4787 (PR #5573). Not fixed there — that card's file surface was grid.tsx alone, and packages/components is a large shared package.

The gap

packages/app-shell/src/__tests__/widget-dom-leak-sweep.test.tsx is the measurement gate for the #3291 / #4425 whitelist migration. Its target list is four plugin packages (plugin-charts, plugin-calendar, plugin-chatbot, plugin-dashboard). It does not sweep the packages/components renderers at all.

That is why ui:grid leaked unobserved: #4008's ratchet pins only className, and nothing else looked at the rendered attributes of a packages/components renderer. The leak was found by hand during #4011 / PR #4785, not by a gate.

The renderers still carrying it

The same bare-spread shape grid.tsx had — destructure data-obj-id / data-obj-type / style, spread the rest onto a host element — is present in at least these, all on origin/main at ac73c24b0:

  • packages/components/src/renderers/layout/flex.tsx (flex.tsx:63-76)
  • packages/components/src/renderers/layout/stack.tsx (stack.tsx:65-77)
  • packages/components/src/renderers/layout/container.tsx (container.tsx:78-90)
  • packages/components/src/renderers/basic/text.tsx (text.tsx:18-22)

Listed as candidates to measure, not asserted as defects — the shape is the same, but whether a given node's keys actually reach an element was verified only for grid. That verification is cheap and is exactly what the sweep does.

For reference, the measured grid reading before PR #5573, on a canary node — eight illegitimate attributes. (Tags are written with a space after < and before >, the same workaround #3291's body uses: GitHub's body sanitizer eats angle-bracket-delimited runs even inside fenced code blocks, and it emptied this block on the first revision of this issue.)

< div class="grid grid-cols-4 sm:grid-cols-2 md:grid-cols-2 gap-4 authored-class"
      columns="4" gap="4" mdcolumns="2" smcolumns="2" id="grid-node" name="grid_node"
      props="[object Object]" zzcanary="leak" data-testid="g" aria-label="A grid"
      role="region" colorvariant="x" data-obj-id="grid-node" data-obj-type="grid" >

Why this is the pin worth having, rather than a warning-as-error pin

#4787 also asked whether React's unknown-attribute warning should be pinned as an error so the class stays closed. PR #5573 answers that in full; the short version is no, because such a pin is blind to most of the defect:

  1. React does not warn for unknown all-lowercase attributes at all — it passes them through by design and stringifies object values. columns, gap, name, props produce zero output. Measured: 8 leaked attributes, 3 console.error calls.
  2. The warning that does fire is the camelCase one, and React's own suggested remedy ("spell it lowercase") silences the warning while keeping the leak.
  3. Vitest 4's reporter defaults to silent: 'passed-only', so console output from a passing test is discarded — the warning can only be seen in a run that is already red for another reason.
  4. React latches the warning per prop name per module instance, so it fires only on the first render carrying a key. This produced a real phantom check in PR fix(components): whitelist ui:grid's DOM passthrough so schema keys stop leaking as attributes #5573: a console.error case written against a shared canary passed against code leaking ten attributes, because earlier cases consumed the latch.

The technique that actually closes the class reads the DOM, not the console — which is what the sweep already does, and it catches lowercase and camelCase alike.

Suggested shape

Extend the existing sweep's target list to the registry-reachable renderers in packages/components, deleting rows from its ledger as each renderer converges on toDomProps — the same per-package migration pattern #4425 phase 2 already runs. The dependency-direction argument in that file's docblock still holds (app-shell may import components).

Sizing note: packages/components registers far more renderers than the four plugin packages did, so the first run should be expected to record a ledger, not to come back clean.

References


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 21, 2026

    @huangyiirene
    Collaborator

    Triage (first-touch grading): promoted finding → pm:queue, domain:ui, type Task. Rationale: the gap is measured (the grid leak shipped precisely because the sweep's target list stops at the four plugin packages), the four candidate renderers are named with line spans on origin/main, and the remedy is the established #4425 phase-2 pattern — extend the sweep's target list, record a ledger on first run, burn it down per renderer. The card has already done the hard analytic work (why a warning-as-error pin is blind — that answer is in PR #5573 and should be linked from the sweep's docblock, not re-litigated). Verification strategy, not a decision matter.

    Dispatch notes: expect the first run to record a ledger, not come clean (the card says so — a red-on-arrival gate is not a failed dispatch); file surface is packages/app-shell/src/__tests__/widget-dom-leak-sweep.test.tsx plus whatever renderer fixes the dispatching seat chooses to fold in (fold-or-serial answer owed if renderer fixes ride along, since flex/stack/container/text are shared-package hot surface). Size/model suggestion: M.


    Generated by Claude Code

  3. self-assigned this
    on Aug 21, 2026
  4. os-sales commented on Aug 21, 2026

    @os-sales
    CollaboratorAuthor

    Claim — domain:ui execution seat (#5560), round 8.

    • Session: session_012u2pRjcqAYtoEjgr3wwhnK
    • Branch: claude/issue-5574-widget-dom-leak-sweep-components
    • Label: pm:queue → pm:dispatched

    Triage expectation carried into the dispatch: widening the sweep to packages/components/src/renderers/** is expected to come back red on arrival, and a red-on-arrival gate is not a failed dispatch — the first run recording a ledger is the correct outcome. The fold-or-serial answer is owed back to this card if renderer fixes ride along with the sweep widening.


    Generated by Claude Code

  5. os-sales commented on Aug 21, 2026

    @os-sales
    CollaboratorAuthor

    Fold-or-serial answer — serial, and the ledger is what enforces it

    Owed back to this card because renderer fixes were in scope for the dispatching seat. PR #5629 widens the sweep and records the reading; it changes no renderer source. Agreeing with the default guidance, but on measurements that were not available when the guidance was written, so here they are.

    1. The fold criterion was "genuinely one-line", and the real number is 119, not 4.

    The card named four candidates (ui:flex, ui:stack, ui:container, ui:text) and was careful to call them candidates. All four leak — confirmed. But so do 115 others: 119 of the 158 registry-reachable types under packages/components/src/renderers/**, in eight distinct measured shapes. At four renderers, folding would have been arguable. At 119 it is not the same question.

    Folding only the four named ones would have been the worst of both: a PR that both widens and fixes, with 115 rows still open, and no clean statement of what the gate measured on arrival.

    2. Folding would destroy the measurement, which is the deliverable.

    The ledger is the record of the tree this gate arrived on. If the same change both widens and fixes, there is no reading left for anyone to check the fixes against — the fixed renderers simply never appear. That is also the #4425 phase-2 pattern this file already grades: measure, then burn down per package, each card deleting its own rows.

    3. packages/components is the wrong package to land 119 edits in at once.

    It is the most-shared package in the repo and several agents land into the same merge queue. 119 renderer edits in one PR is the shape most likely to conflict with, and to be blamed for, everything else in flight.

    4. Serialising costs nothing here, because the ledger cannot be quietly skipped.

    This is the part that made the decision easy rather than merely defensible. The per-target assertion is exact set equality in both directions: a listed renderer that starts leaking one more attribute fails the gate, and a renderer that stops leaking also fails it until its row is deleted in the same change. So a follow-up card cannot half-land, and the ledger cannot rot into an allowlist — the tree expires each row. Serialising is not "trusting someone to come back"; it is a state the gate refuses to leave.

    What the burn-down looks like

    The eight shapes are the natural card boundaries, since each is one mechanism rather than one file:

    shape targets fix
    BARE_SPREAD 95 route the spread through toDomProps
    BARE_SPREAD_MINUS_NAME 18 same; name stays, legitimately, on hosts that define it
    BARE_SPREAD_ON_SVG 2 same, but the row's attribute names are camelCase — SVG is case-sensitive
    action:group, action:menu 2 the above, plus actions
    ui:form 1 the above
    ui:sidebar-trigger 1 the above; this one also forwards schema itself

    I have deliberately not filed the burn-down cards — sizing and sequencing them is the PM seat's call, and this card is the natural parent for them. Two genuinely separate findings from the same run are filed and unassigned: #5630 (three element: renderers swept only in their empty-state branch) and #5631 (ui:icon reads the identity key name as its lucide icon name, so an icon node authoring name renders nothing).


    Generated by Claude Code

  6. os-sales commented on Aug 21, 2026

    @os-sales
    CollaboratorAuthor
    {
      "issue": 5574,
      "branch": "claude/issue-5574-widget-dom-leak-sweep-components",
      "pr_url": "https://github.com/objectstack-ai/objectui/pull/5629",
      "files_changed": [
        "packages/app-shell/src/__tests__/widget-dom-leak-sweep.test.tsx",
        ".changeset/dom-leak-sweep-components-5574.md"
      ],
      "what_changed": "Widened the DOM-leak sweep's discovery from four namespace prefixes to also cover the five owned by packages/components/src/renderers/** (ui:, element:, page:, action:, protocol-placeholder:), taking the sweep from 23 to 181 targets. Recorded the first run as a ledger of 119 leaking targets in 8 measured shapes, every renderer named. No renderer source touched; no test skipped, disabled or quarantined. Harness additions were forced by measurement, not preference: a SidebarProvider React host (4 targets threw useSidebar into an attribute-clean error boundary), authored schema for 12 targets that rendered zero elements, and an authored-className readiness selector uniform across the family with 6 recorded overrides.",
      "evidence": {
        "discovery_probe": "Scope = packages/components/src/renderers/**. Took origin/main's sweep verbatim, injected ONE extra registration (ComponentRegistry.register('zzprobe5574', ..., { namespace: 'ui' })), ran it: GREEN, 37/37 passed. The pre-widening completeness case never saw a new ui: widget. Third leg: the same ui: injection against THIS branch's gate now fails — 'components: every widget it registers is swept' with expected [ 'action:bar', ...(157) ] to deeply equal [ ...(158) ]. Injection was confirmed on disk (grep + git diff --stat: 1 file changed, 7 insertions) before each run, and reverted with git checkout HEAD -- <path> after (the file was committed first, so the revert had a real restore point).",
        "control_probe": "SAME scope-level, SAME assertion, SAME perturbation — one extra registration — differing only in namespace: ComponentRegistry.register('zzprobe5574', ..., { namespace: 'plugin-dashboard' }) against the same origin/main sweep. RED: 'plugin-dashboard: every widget it registers is swept — a new one cannot slip past', AssertionError naming plugin-dashboard:zzprobe5574, Tests 1 failed | 73 passed (74). The control is at the probe's scope and it CAN fail — it did. Both probes ran in one vitest invocation, so the discovery mechanism, judge and harness were identical across them.",
        "files_discovered_before": "0 of 86 renderer source files under packages/components/src/renderers/**; 0 of 158 registry types; 0 of 5 namespaces. (Whole-file totals: 23 targets, 74 cases.)",
        "files_discovered_after": "86 of 86 renderer source files; 158 of 158 registry types; 5 of 5 namespaces. (Whole-file totals: 181 targets, 200 cases.) Identical scope both rows. The five prefixes are exclusively this package's: workspace-wide grep finds namespace: 'ui' outside packages/components only in a packages/core docstring and a standalone packages/sdui-parser/verify.ts script, neither registering at import time — so nothing foreign is swept into these counts.",
        "leaks_found": "119 of 158 targets leak, in 8 shapes: BARE_SPREAD (95 targets, 14 attributes incl. name); BARE_SPREAD_MINUS_NAME (18, 13 attrs — host defines name, so HTML makes it legitimate); BARE_SPREAD_ON_SVG (2: ui:icon, ui:spinner — 14 attrs with camelCase PRESERVED, since SVG attribute names are case-sensitive); action:group (BARE_SPREAD + actions); action:menu (MINUS_NAME + actions); ui:form (12 — FormRenderer consumes dataSource and form defines name); ui:sidebar-trigger (14 including `schema` itself, the node SchemaRenderer injects on every render). 39 clean, including ui:grid — objectui#4787 / PR #5573's fix now pinned by a gate rather than by hand. All four renderers the card named as candidates (ui:flex, ui:stack, ui:container, ui:text) leak; a dedicated case pins that and pins ui:grid's absence from the ledger from the other side. SEPARATE FINDING: a first pass reported 46 clean, of which 16 were PHANTOM cleans — 12 rendered zero elements (closed overlays, action:* with no actions, ui:icon returning null because the canary node's name is not a lucide icon) and 4 threw useSidebar into an attribute-clean SchemaErrorBoundary. All 16 now render real markup; 7 of them turned out to leak.",
        "red_on_arrival": "Yes, as triage expected. The gate is green only because the 119 are enumerated in the ledger — not because the sweep cannot see them, which the third probe leg proves directly."
      },
      "ledger": {
        "entries": 119,
        "is_ledger_not_allowlist": "Ledger. The distinction is mechanical, not rhetorical: the per-target assertion is EXACT SET EQUALITY in both directions, so (a) a listed renderer that starts leaking one more attribute fails the gate, and (b) a renderer that STOPS leaking also fails it until its row is deleted in the same change. An allowlist has neither property. Nothing is skipped, it.skip-ed, quarantined or excluded — all 158 targets render and all 158 are scanned every run. Rows are grouped by measured MECHANISM rather than repeated 119 times, but every renderer is named individually, there is no wildcard or prefix anywhere in the ledger, and LEAK_LEDGER remains a per-target map. Three new cases keep it honest: no renderer may appear in two groups; every readiness override must carry a recorded reason and every reason an override (two-way, so it cannot become a parking space); and the four card-named renderers must be ledgered while ui:grid must not."
      },
      "fold_or_serial_answer": "SERIAL. Posted on #5574 as https://github.com/objectstack-ai/objectui/issues/5574#issuecomment-5374319854. Four reasons: (1) the fold criterion was 'genuinely one-line' and the measured number is 119, not the 4 the card named — at 4 it would have been arguable, at 119 it is a different question, and folding only the 4 would be the worst of both (a PR that widens and fixes, 115 rows still open, no clean arrival reading); (2) folding destroys the measurement — the ledger IS the record of the tree the gate arrived on, and fixed renderers would simply never appear in it; (3) packages/components is the most-shared package in the repo and 119 edits in one PR is the worst shape for a merge queue several agents land into; (4) serialising costs nothing because the exact-set-equality expiry means a follow-up cannot half-land and the ledger cannot rot into an allowlist. Burn-down cards deliberately NOT filed — sizing and sequencing is the PM seat's call and #5574 is their natural parent; the 8 shapes are the natural card boundaries since each is one mechanism rather than one file.",
      "changeset": {
        "present": true,
        "why": "Required and added: .changeset/dom-leak-sweep-components-5574.md with EMPTY frontmatter — this repo's explicit 'publishes nothing' declaration. scripts/check-changeset-presence.mjs initially exited 1 ('1 source file(s) of 1 released package(s) changed, and this change adds no changeset'), and exits 0 with the empty-frontmatter file. NOTE: objectui has no skip-changeset label and none was created or applied. The dist criterion was MEASURED, not asserted: packages/app-shell/dist built twice — once at the origin/main version of the changed file, once at this branch's — each time from a cleared dist/ AND a cleared tsconfig.tsbuildinfo, then sha256-compared. Result: IDENTICAL, 862 emitted files, 431 of them .d.ts, all hashes equal. Mechanism: packages/app-shell/tsconfig.json excludes **/*.test.tsx from the build program. Trap recorded in the changeset body: the FIRST attempt produced an empty dist/ and would have read as a false 'identical' — tsc is composite, so with the build info left in place it skips the emit entirely."
      },
      "tests": {
        "command": "pnpm exec vitest run packages/app-shell/src/__tests__/widget-dom-leak-sweep.test.tsx  (from the REPO ROOT — package-cwd vitest is refused by objectui#3378's guard)",
        "result": "Test Files 1 passed (1) / Tests 200 passed (200). Run at ed1cbb162, the head of the branch, AFTER the final commit. Baseline for comparison: the origin/main copy of the same file runs 74 cases. Scope note: packages/app-shell holds ~487 test files and the full suite exceeds this container's 10-minute foreground cap, so the sweep was run targeted; the broader safety claim is derived rather than run — lint covered the whole @object-ui/app-shell package (a provable superset of the diff at package granularity, 918 files reported via --format json, the changed file at 0 errors / 0 warnings), and eslint.config.js configures no project/projectService, so linting is not type-aware and this diff cannot move the verdict on any file it does not touch. Every heavy command ran through the container's shared verify lock (scripts/pm/os-verify-lock.sh), and every exit code was captured before any pipe."
      },
      "gates": [
        { "name": "vitest — widget-dom-leak-sweep.test.tsx (repo root)", "status": "pass — Test Files 1 passed (1) / Tests 200 passed (200)" },
        { "name": "pnpm --filter @object-ui/app-shell type-check", "status": "pass — exit 0, echoed `tsc --noEmit && tsc -p tsconfig.test.json`" },
        { "name": "pnpm --filter @object-ui/app-shell lint", "status": "pass — exit 0 (the repo's own `eslint .`; warnings only, no --max-warnings configured)" },
        { "name": "node scripts/check-changeset-presence.mjs", "status": "pass — declares 1 changeset, empty frontmatter accepted as the explicit exemption" },
        { "name": "node scripts/check-changeset-no-major.mjs", "status": "pass — No changeset declares a `major` bump" },
        { "name": "node scripts/check-control-bytes.mjs", "status": "pass — scanned 4672 tracked text files, 85 binary skipped" },
        { "name": "CI on PR #5629", "status": "in_progress — draft PR just opened; per the dispatch contract the report lands at draft-PR time and CI convergence is the PM seat's read" },
        { "name": "check-eager-closure-budget / check-doc-snippet-types", "status": "not run — known broken gauges per the dispatch (exit 2 / exit 1 until a console build and a workspace build respectively); neither is touched by this diff" }
      ],
      "out_of_scope_findings": [
        "filed as #5630 (label `finding`, unassigned): three `element:` renderers — definition-list, repeater, metadata_viewer — are swept only in their empty-state branch, so their clean reading covers the branch that structurally cannot spread. Recorded in the PR rather than hidden, needs data-bearing fixtures.",
        "filed as #5631 (no label, left for PM triage — concrete defect, not observation-class): `ui:icon` reads the SDUI identity key `name` as its lucide icon name, so any icon node authoring `name` resolves nothing and returns null — silent at runtime and clean-looking to a gate. Contract question, not a one-line fix; the tolerant-consumer fix (`schema.icon ?? schema.name`) is the shape #5574's family is migrating away from. Distinct from #5622 / #5586, which concern retired lucide SPELLINGS on the same resolver rather than which schema key is consulted."
      ],
      "open_questions": [],
      "status": "done",
      "premise_still_valid": true
    }

    Generated by Claude Code

  7. 3 remaining items

  8. os-project-manager commented on Aug 22, 2026

    @os-project-manager
    Collaborator

    H8 配对写入:pm:dispatched → pm:queue

    交付 PR #5629 已于 2026-08-21T19:38:14Z 合并,本卡仍挂 pm:dispatched。

    但本卡不是已交付,这一点有合并之后的测量作证:5379776530(2026-08-22T10:31Z,比合并晚 15 小时)把 examples/schema-catalog 的每个节点过真实 SchemaRenderer 再读 DOM 属性,三个 layout renderer 仍在泄漏,合计 662 个非法属性到达 DOM:

    flex[align]        198     flex[justify]        98
    flex[gap]          184     container[padding]   14
    stack[gap]         153     container[maxwidth]   6
    flex[direction]      5     stack[align]          4
    

    同一探针下 grid 的 26 个节点归零 —— 那是 #5573 的 toDomProps 转换生效的对照,也证明探针不是瞎的:修过的 renderer 读干净,没修的读不干净。

    ⇒ 测量已到位、修复未做,剩余物可派发,故 pm:queue。

    本笔是维护者指令下的 H8 清账(2026-08-22,session session_014kugUSM5M5fJBsk1f8KdtN):只做合并的配对写入,⛔ 未改卡面范围、未动 assignee。


    Generated by Claude Code

  9. os-zhuang commented on Aug 23, 2026

    @os-zhuang
    Contributor

    清 assignee — objectstack#11196 维护者裁决 2026-08-23「A:本席清 agent 残留」,skills 席 session 757f0dc0-5dae-51d7-9053-c15c57ebfe3b。证据:ui 席 round 8 的认领(session 012u2pRjcqAYtoEjgr3wwhnK)以 PR #5629 合并收束;08-22 H8 配对写入回标 pm:queue(附合并后测量:三个 layout renderer 仍漏,卡未整体交付)——回标只翻标签,assignee 为已收束周期的残留。标签未动;续做的认领席重新 assign + 声明即可。


    Generated by Claude Code

  10. claude commented on Aug 23, 2026

    @claude
    Contributor

    Claim — domain:ui execution seat. PM round 29, session session_01EuPCi56cnGyykygi3z9w4m, branch claude/issue-5574-components-renderer-dom-leaks. Default tier — clause ② is not tripped: this stops schema props reaching the DOM, it does not change what the contract accepts.

    Dispatching on the post-merge measurement in comment 5379776530, not on the card body: 662 illegal attributes still reach the DOM across three layout renderers (flex[align] 198 · flex[gap] 184 · stack[gap] 153 · flex[justify] 98 · container[padding] 14 · container[maxwidth] 6 · flex[direction] 5 · stack[align] 4), measured by running every examples/schema-catalog node through the real SchemaRenderer and reading DOM attributes.

    ⚠️ The reason that measurement is trustworthy is its control: grid's 26 nodes read ZERO under the same probe, because #5573's toDomProps conversion landed there. A fixed renderer reads clean and an unfixed one reads dirty — so the probe is demonstrably not blind. The dev re-runs that probe rather than trusting these numbers, and keeps the grid control leg.

    pm:dispatched → pm:queue was an H8 paired write-back on 2026-08-22 with the assignee cleared as a residue of a closed cycle (objectstack#11196 ruling). This claim is a fresh one, not a continuation.


    Generated by Claude Code

  11. os-zhuang commented on Aug 23, 2026

    @os-zhuang
    Contributor
    {
      "issue": 5574,
      "status": "done",
      "branch": "claude/issue-5574-components-renderer-dom-leaks",
      "pr": "https://github.com/objectstack-ai/objectui/pull/5839",
      "premise_still_valid": true,
      "summary": "Converted flex/stack/container/text to the toDomProps treatment PR #5573 landed on grid.tsx, and DELETED their four rows from the DOM-leak sweep's ledger — the gate's two-way expiry makes the fix red until the rows go, which is the expiry mechanism working for the first time. Re-took the measurement rather than trusting the numbers: 1194 illegitimate attributes before, 0 after, with grid's 26 nodes at 0 in BOTH runs as the control. Landed that measurement as a permanent catalog-scale pin (examples/schema-catalog/test/layout-dom-leak-5574.test.tsx) with the control built in, and INVERTED the meta-case that pinned the four as leaking so it now pins that no row may re-absorb this family. The sweep-widening half of the card was already closed by PR #5629; not re-done.",
      "tests": "Union re-run AFTER the final commit, at 64ca95fe5, working tree clean, every exit code captured before any pipe; each row quotes the gate's own verdict line, never a bare $?. || vitest (repo root, never pnpm --filter — objectui#3378's guard): sweep + examples/schema-catalog + packages/components -> 'Test Files 192 passed (192)' / 'Tests 3469 passed (3469)'. || type-check, components + app-shell + example-schema-catalog -> 'Done' x3, exit 0. The dependency closure was built FIRST (pnpm --filter '<pkg>^...' build): without it type-check reports TS2307 'Cannot find module @object-ui/core' on 25 untouched files and reads like a broken diff. || lint, same three packages -> 'Done' x3, exit 0, 0 errors (906/2598 pre-existing warnings, none on a changed line). || check-changeset-presence.mjs -> 'OK, 5 source file(s) of 2 released package(s) changed, and this change declares 1 changeset(s)'. check-changeset-no-major.mjs -> 'No changeset declares a major bump'. check-control-bytes.mjs -> 'OK (scanned 4857 tracked text file(s))'. || BEFORE/AFTER PROBE (re-taken, not trusted): every catalog node of flex/stack/container/grid/text rendered through the real SchemaRenderer, attributes read off the node's own host element. BEFORE — text[content] 522, flex[align] 198, flex[gap] 193, stack[gap] 153, flex[justify] 98, container[padding] 14, container[maxwidth] 6, flex[direction] 5, stack[align] 4, text[value] 1; TOTAL 1194. AFTER — TOTAL 0. CONTROL — grid 26 nodes, 0 in BOTH runs. The other guard against a vacuously-zero read: node counts were identical across the two runs (flex 248, stack 153, container 15, grid 26, text 699 rendered; text 176 no-element), so the after-zero is a reading and not a walk that stopped finding nodes. || REVERSE-VERIFICATION on a real commit (never against uncommitted edits): fix committed as 3ed962a73, then the four renderer sources — and only those — reverted to origin/main. MUTATION CONFIRMED ON DISK before running, anchored on the text I meant to change rather than on an editor exit code: toDomProps count 4->0 in each of the four files, the deleted bare-spread marker \"'data-obj-type': dataObjType\" back at 1/2/2/1 occurrences, git diff HEAD --stat naming exactly 4 files. RED, and the direction was the predicted one: 'Tests 5 failed | 197 passed (202)' — the catalog pin 'expected [ ...(1194) ] to deeply equal []' (exactly the before-total, so the two instruments agree), plus ui:flex / ui:stack / ui:container / ui:text each 'leaked 14 non-DOM attribute(s)' on the sweep. Restore leg: the mutation script carried trap '<restore>' EXIT INT TERM, so a foreground-cap SIGTERM mid-mutation could not have left the tree mutated for later measurements; after restore git diff HEAD --stat was EMPTY, git status clean, re-run 'Tests 202 passed (202)'. No build step is involved in either leg — vitest.config.mts aliases every @object-ui/* specifier to src/, so these tests read source directly and the stale-dist ablation trap does not apply here. LEGS THAT DO NOT DISCRIMINATE, named rather than counted as evidence: (1) the grid leg reads 0 in every configuration of this change, so alone it can never fail — it earns its place only jointly, as the reading that stays clean in the same run where its siblings flip; (2) the judge self-check passes identically before and after (it guards an attribute reader that reports nothing, a different failure); (3) the NODE_CENSUS assertion is identical across the mutation (same nodes render either way — it guards the blind-walk failure). || LINT NARROWING, DECLARED: repo-scope pnpm lint is CI's run and was not taken. Replaced by package-scope 'eslint .' on all three affected packages — population read from eslint's own config, a provable superset of this diff at package granularity — plus a targeted run reporting 'files linted: 6, errors: 0' read from --format json. eslint.config.js configures no project/projectService, so linting is not type-aware and this diff cannot move the verdict on any file it does not touch. || Every heavy command ran through the container's shared verify lock (scripts/pm/os-verify-lock.sh); longest hold 8m29s, longest wait 1m56s.",
      "open_questions": [],
      "out_of_scope_findings": []
    }

    Notes the JSON cannot carry

    text was not in the dispatched measurement, and it is the biggest leak in the family. Comment 5379776530 covered flex/stack/container/grid only. The card body named text as a candidate, so I measured it under the same probe: 523 attributes, 522 of them content — the key the renderer renders as its children, duplicated into the markup as an attribute. Fixed here with the count stated, per the dispatch's rule for renderers beyond the measured set. Nothing outside that was touched.

    flex[gap] reads 193, not the dispatched 184. PR #5826 re-authored nine space-x-* flex nodes as gap after the reading was taken. Renderer-side nothing moved — this is exactly the "the numbers move with authoring, not only with code" property comment 5379776530 called out. Every other layout figure reproduced to the unit, which is the strongest evidence the probe is the same instrument.

    The sweep-widening half of this card was already closed by PR #5629, so it was not re-done. The dispatch asked me to say either way.

    Why a new test file rather than extending the sweep's canaries. The sweep is the stronger instrument for the open tail (zzcanary, the flattened props container, the injected adapter) but its canary node authors no align, no gap, no padding — and adding them there would rewrite the measured attribute set of all 115 renderers still ledgered, destroying the arrival reading that file exists to preserve. So the declared-prop half is measured at catalog scale in examples/schema-catalog/test/layout-dom-leak-5574.test.tsx instead. A regression that re-spread only the declared keys would pass the sweep and fail there.

    115 rows remain, in the seven other measured shapes. Sequencing them is the PM seat's burn-down parent, per the ruling in comment 5374361588 — I filed nothing on my own authority.

    Nothing was widened, loosened or exempted to pass. The one gate that went red on my own work was type-check on my new test file (<div align="start"> does not type-check) — the remedy taken was to make the fixture the defect's real shape, a spread of an untyped record, not to widen a type or add an any.


    Generated by Claude Code


    Generated by Claude Code

  12. os-zhuang commented on Aug 23, 2026

    @os-zhuang
    Contributor

    os-dev-report

    Re-posting the report with the marker as LITERAL TEXT. The previous comment (5386554299) opened with the HTML-comment form of this marker and GitHub's body sanitizer ate it on the way into storage, which makes that comment invisible to a scan keyed on the marker. Same JSON below; the prose notes in that comment stand and are not repeated here. Angle-bracket runs are spelled out for the same reason — the sanitizer took three of them out of the first copy.

    {
      "issue": 5574,
      "status": "done",
      "branch": "claude/issue-5574-components-renderer-dom-leaks",
      "pr": "https://github.com/objectstack-ai/objectui/pull/5839",
      "premise_still_valid": true,
      "summary": "Converted flex/stack/container/text to the toDomProps treatment PR #5573 landed on grid.tsx, and DELETED their four rows from the DOM-leak sweep's ledger — the gate's two-way expiry makes the fix red until the rows go, which is that expiry mechanism working for the first time. Re-took the measurement rather than trusting the dispatched numbers: 1194 illegitimate attributes before, 0 after, with grid's 26 nodes at 0 in BOTH runs as the control. Landed the measurement as a permanent catalog-scale pin (examples/schema-catalog/test/layout-dom-leak-5574.test.tsx) with that control built into it, and INVERTED the meta-case that pinned the four as leaking so it now pins that no row may re-absorb this family. text was NOT in the dispatched measurement and turned out to be the family's largest leak (523 attributes, 522 of them `content`); it is one of the four the card body named, the same probe shows it leaking, so it is fixed here with the count stated. The sweep-widening half of the card was already closed by PR #5629 and was not re-done. 115 ledger rows remain, in the seven other measured shapes; no burn-down cards filed on my own authority.",
      "tests": "Union re-run AFTER the final commit, at 64ca95fe5, working tree clean, every exit code captured before any pipe; each row quotes the gate's own verdict line, never a bare dollar-question. || vitest (repo root, never pnpm --filter — objectui#3378's guard): sweep + examples/schema-catalog + packages/components => 'Test Files 192 passed (192)' / 'Tests 3469 passed (3469)'. || type-check, components + app-shell + example-schema-catalog => 'Done' x3, exit 0. The dependency closure was built FIRST (pnpm --filter PKG-caret-triple-dot build): without it type-check reports TS2307 'Cannot find module @object-ui/core' on 25 untouched files and reads like a broken diff. || lint, same three packages => 'Done' x3, exit 0, 0 errors (906/2598 pre-existing warnings, none on a changed line). || check-changeset-presence.mjs => 'OK, 5 source file(s) of 2 released package(s) changed, and this change declares 1 changeset(s)'. check-changeset-no-major.mjs => 'No changeset declares a major bump'. check-control-bytes.mjs => 'OK (scanned 4857 tracked text file(s))'. || BEFORE/AFTER PROBE (re-taken, not trusted): every catalog node of flex/stack/container/grid/text rendered through the real SchemaRenderer, attributes read off the node's own host element. BEFORE — text[content] 522, flex[align] 198, flex[gap] 193, stack[gap] 153, flex[justify] 98, container[padding] 14, container[maxwidth] 6, flex[direction] 5, stack[align] 4, text[value] 1; TOTAL 1194. AFTER — TOTAL 0. CONTROL — grid 26 nodes, 0 in BOTH runs. Second guard against a vacuously-zero read: node counts identical across the two runs (flex 248, stack 153, container 15, grid 26, text 699 rendered; text 176 rendering no element at all), so the after-zero is a reading and not a walk that stopped finding nodes. || REVERSE-VERIFICATION on a real commit (never against uncommitted edits): fix committed as 3ed962a73, then the four renderer sources — and only those — reverted to origin/main. MUTATION CONFIRMED ON DISK before running, anchored on the text I meant to change rather than on an editor's exit code: toDomProps count 4 to 0 in each of the four files, the deleted bare-spread marker (the quoted data-obj-type key destructured into dataObjType) back at 1/2/2/1 occurrences, git diff HEAD --stat naming exactly 4 files. RED, in the predicted direction: 'Tests 5 failed | 197 passed (202)' — the catalog pin 'expected [ ...(1194) ] to deeply equal []' (exactly the before-total, so the two instruments agree independently), plus ui:flex / ui:stack / ui:container / ui:text each 'leaked 14 non-DOM attribute(s)' on the sweep. RESTORE leg: the mutation script carried a trap on EXIT INT TERM, so a foreground-cap SIGTERM mid-mutation could not have left the tree mutated for later measurements; after restore git diff HEAD --stat was EMPTY, git status clean, re-run 'Tests 202 passed (202)'. No build is involved in either leg — vitest.config.mts aliases every @object-ui/* specifier to src/, so these tests read source directly and the stale-dist ablation trap does not apply. LEGS THAT DO NOT DISCRIMINATE, named rather than counted as evidence: (1) the grid leg reads 0 in every configuration of this change, so alone it can never fail — it earns its place only jointly, as the reading that stays clean in the same run where its siblings flip; (2) the judge self-check passes identically before and after (it guards an attribute reader that reports nothing — a different failure); (3) the NODE_CENSUS assertion is identical across the mutation, same nodes render either way (it guards the blind-walk failure). || LINT NARROWING, DECLARED: repo-scope pnpm lint is CI's run and was not taken. Replaced by package-scope 'eslint .' on all three affected packages — population read from eslint's own config, a provable superset of this diff at package granularity — plus a targeted run reporting 'files linted: 6, errors: 0' read from --format json. eslint.config.js configures no project/projectService, so linting is not type-aware and this diff cannot move the verdict on any file it does not touch. || NOTHING WIDENED TO PASS: the one gate that went red on my own work was type-check on the new test file (a JSX element with hand-written align/gap/content attributes does not type-check); the remedy taken was to make the fixture the defect's real shape — a spread of an untyped Record — not to widen a type or add an any. || Every heavy command ran through the container's shared verify lock (scripts/pm/os-verify-lock.sh); longest hold 8m29s, longest wait 1m56s.",
      "open_questions": [],
      "out_of_scope_findings": []
    }

    Generated by Claude Code


    Generated by Claude Code

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

Metadata

Metadata

Assignees

Labels

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

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions