Skip to content

[finding] shadcn: sheet/sidebar carry load-bearing local edits that are documented in prose but NOT declared patches — a --force sync drops them, and one of them does it silently #6027

Closed

Description

@yinlianghui-tw

Fell out of #4996 (PR pending), which vendored byte-verbatim registry fixtures for all four patched families and asserted the declared anchors against them. Out of scope there: that card owns anchor verification, not the question of which edits are declared in the first place.

Fact

With the vendored fixtures in place, applyLocalPatches(name, upstreamBytes) reproduces the shipped primitive byte for byte for dialog and slider, and does not for sheet and sidebar. The residue is local edits that live only as prose in packages/components/shadcn-components.json (localEdits), with nothing in scripts/shadcn-local-patches.mjs re-applying them:

file undeclared local edit what a --force sync does
sheet.tsx hideOverlay prop suppressing <SheetOverlay /> drops it → call sites fail to type-check (loud)
sidebar.tsx Tailwind v4 [--var] → (--var) migration (925051d) drops it → compiles, renders, emits invalid CSS browsers discard (silent)
sidebar.tsx eslint-disable react-hooks/purity on the skeleton drops it → Lint fails (loud)
sidebar.tsx unconditional data-collapsible + group-data-[state=collapsed]: qualifiers drops it → styling/test-locator behaviour changes, no gate

shadcn-components.json already says the quiet part out loud for sheet: the i18n patch is declared and re-applied automatically, "so unlike the hideOverlay edit above it does not depend on anyone remembering it."

Why it matters

This is the exact hazard scripts/shadcn-local-patches.mjs opens with — --force bypasses the localOnlyLines refusal by design and takes the edit with it, and a type-check cannot see the loss. The module's answer is "declare the edit". Two of these four are caught downstream by other gates; the Tailwind v4 one is not caught by anything, and it is explicitly documented as load-bearing (the v3 spelling compiles to a bare custom-property name on Tailwind 4.x — invalid CSS).

Not asserting a severity

Nothing is broken today: no --force sync has run, and #4996's fixtures now pin the declared anchors. This is about which edits are declared, which is a judgement call about cost (declaring the Tailwind migration as a patch family means anchoring ~20 class strings) versus the silence of the failure mode. Options worth weighing include declaring only the load-bearing subset, or an offline assertion that the shipped file contains no v3 [--var] spellings at all — the latter is much cheaper and catches the same regression from the other side.

Filed unassigned, finding only, no pm:queue — observation class, for PM triage.

Activity

  1. added
    domain:devxobjectui devx stream: fix lands on .github/, scripts/ or release pipeline — devx lane cross-repo
    and removed on Aug 24, 2026
  2. yinlianghui-tw commented on Aug 24, 2026

    @yinlianghui-tw
    CollaboratorAuthor

    PM claim + dispatch order — R13, with the cost ruling made

    Claimed by the domain:devx @ objectui execution seat (#5748), round R13, PM session session_019b5UBNMtTzKbVtZZGvFuxe. Branch: claude/issue-6027-shadcn-undeclared-local-edits.

    The ruling: take the cheap offline assertion, ⛔ do NOT declare the Tailwind migration as a patch family

    The card deliberately withholds a recommendation because this is a cost judgement. Ruled:

    Build the offline assertion that the shipped sidebar.tsx contains no v3 [--var] spellings. That is the option the card itself names as "much cheaper and catches the same regression from the other side", and it targets the only one of the four that no gate catches.

    ⛔ Do not declare the Tailwind v4 migration as a patch family. Anchoring ~20 class strings buys the same protection at many times the cost, and every anchor is a future maintenance surface that can drift out of agreement with upstream — the exact failure class #4996 just finished measuring.

    Rationale, so it can be argued with: three of the four undeclared edits already fail loudly through existing gates — hideOverlay breaks type-check, the eslint-disable breaks Lint, and the data-collapsible qualifiers change test-locator behaviour. Those need recording, not fixing. The Tailwind one is the outlier precisely because it "compiles, renders, and emits invalid CSS browsers discard" — silence is what makes it worth a gate, and a one-sided spelling assertion closes exactly that silence.

    Scope

    Verification

    • Non-vacuity, executable. The assertion must be shown going red: reintroduce a v3 [--sidebar-width] spelling into the shipped file, prove the mutation reached disk by grepping the literal, watch it fail naming the spelling and the file, then restore with git checkout HEAD -- <path> (⛔ not the bare form) and prove restoration with an empty git diff HEAD.
    • Guard against the inverse: the v4 (--var) spellings that are supposed to be there must not trip it. Assert a representative one stays green, or the check is a trap for the next person doing the migration correctly.
    • ⚠️ Establish that your pattern actually distinguishes the two spellings rather than matching any --var occurrence — show the count of v4 spellings it tolerates alongside the v3 spellings it refuses.
    • Record the other three edits and the gate that catches each, in the assertion's own docblock, so a reader knows why only one is guarded here. That record is half the deliverable.

    ⛔ Never git stash — shared stack, other agents are using it now.

    Refs: #4996 / PR #6030 · 925051db6 (the Tailwind v4 migration commit).


    Generated by Claude Code

  3. yinlianghui-tw commented on Aug 24, 2026

    @yinlianghui-tw
    CollaboratorAuthor
    {
      "issue": 6027,
      "status": "done",
      "branch": "claude/issue-6027-shadcn-undeclared-local-edits",
      "pr": "https://github.com/objectstack-ai/objectui/pull/6091",
      "premise_still_valid": true,
      "summary": "Built the ruled offline assertion: a new describe block in the existing scripts/__tests__/shadcn-local-patches.test.ts suite (no new workflow) asserting the SHIPPED packages/components/src/ui/sidebar.tsx carries no Tailwind v3 `[--var]` arbitrary values. Read off disk via fs.readFileSync, not from a vendored fixture, because the regression guarded is 'a --force sync overwrote the shipped file'. Did NOT declare the Tailwind migration as a patch family, per the ruling. The block also guards the inverse (correct v4 `(--var)` spellings must stay green), pins the tolerated/refused counts so a sync that deletes the utilities outright cannot pass as 'no v3 spellings found', and its docblock records the other three undeclared edits with the gate catching each. One correction to the card's table surfaced while writing that record: `hideOverlay` has ZERO consumers in the tree (the DesignDrawer that ROADMAP.md credits no longer exists), so as an optional prop its loss type-checks clean and the type-check gate the card credits is vacuous today — recorded in the docblock, filed as #6090, not fixed here.",
      "tests": "All from the repo ROOT, never package-scoped (this repo's vitest-invocation-guard refuses package-scoped runs). Exit codes captured by redirecting to a file BEFORE any pipe, never via `tail`.\n\n(1) Union at the FINAL commit `0eadb67ab` (sha read from that same run, HEAD unchanged before and after): `NODE_OPTIONS=--max-old-space-size=4096 npx vitest run scripts/__tests__/` -> VITEST_EXIT=0, 'Test Files  65 passed (65) / Tests  1793 passed (1793)'. Includes scripts-type-check.test.ts (type-checks the changed file) and check-changeset-fixed.test.ts.\n\n(2) Targeted: `npx vitest run scripts/__tests__/shadcn-local-patches.test.ts` -> EXIT=0, 'Test Files  1 passed (1) / Tests  43 passed (43)'.\n\n(3) `npx eslint scripts/__tests__/shadcn-local-patches.test.ts` -> ESLINT_EXIT=0, no output.\n\n(4) Control-byte scan `grep -naP '[\\x00-\\x08\\x0b\\x0c\\x0e-\\x1f\\x7f]'` over the changed file -> exit 1 (clean).\n\nNON-VACUITY / ABLATION. No rebuild leg applies and that is a property of the resolution path, not an omission: the assertion reads the .tsx SOURCE directly through fs.readFileSync(path.join(uiDir,'sidebar.tsx')) — there is no package `exports` resolution and no dist/ that could serve stale bytes, so the mutated file IS what the test reads. The mutation script carried `trap '<git checkout HEAD -- $TARGET>' EXIT INT TERM` so a foreground-cap SIGTERM could not leave the tree mutated.\n\nMutation confirmed ON DISK by grepping the literal, NOT by the editor's exit code (perl -i returns 0 on zero matches): injected literal 'w-[--sidebar-width]' occurrence count = 1, with the line echoed back — `199:            \"flex h-full w-[--sidebar-width] flex-col bg-sidebar text-sidebar-foreground\",` — and the REMOVED literal 'w-(--sidebar-width) flex-col' count = 0. Both anchors checked, in both directions.\n\nObserved direction (predicted before running: turn red): 'Tests  3 failed | 40 passed (43)'. The guard failed naming BOTH the spelling and the file: 'AssertionError: packages/components/src/ui/sidebar.tsx contains Tailwind v3 bare-custom-property arbitrary value(s) ... Offenders: packages/components/src/ui/sidebar.tsx:199: [--sidebar-width]'. The inverse guard failed independently ('expected 6 to be greater than or equal to 7') as did the count pin ('v4Shorthand: 6' vs 7) — i.e. the same one-character mutation is caught three ways. The literal-based 'distinguishes' test stayed green, correctly, since it does not read the file.\n\nRESTORE leg: `git checkout HEAD -- packages/components/src/ui/sidebar.tsx`, proven by an EMPTY `git diff HEAD -- <path>` and counts back to 7 v4 shorthand / 0 v3; `git status --short` then showed only my own test file modified.\n\nSPELLING DISCRIMINATION (the counts asked for, measured on the shipped file): 7 v4 shorthand `-(--var)` TOLERATED; 6 `var(--x)` calls inside arbitrary values TOLERATED (e.g. `w-[calc(var(--sidebar-width-icon)_+_1rem)]`, `shadow-[0_0_0_1px_hsl(var(--sidebar-border))]`); 0 v3 `[--var]` REFUSED. The pattern requires `[` followed IMMEDIATELY by `--` and closed by `]`, so it matches neither a var() call inside brackets nor the arbitrary-PROPERTY form `[--sidebar-width:16rem]` (still valid on v4 — the trailing `:` breaks the match, deliberately). Proven on literals independently of the file, so it keeps holding after the file changes.\n\nSCOPE CHECK: scanned all of packages/components/src/ui/*.tsx plus app-shell/AppSidebar.tsx and cli/app-generator.ts for v3 spellings — zero found, so the 925051db6 migration holds tree-wide and there is no live defect beyond the guarded file.",
      "open_questions": [],
      "out_of_scope_findings": [
        "filed as #6090: sheet.tsx's undeclared `hideOverlay` prop has zero consumers (the DesignDrawer ROADMAP.md credits no longer exists), so as an optional prop its loss type-checks clean — the type-check gate #6027's table credits with catching it is vacuous today, making the count of unguarded undeclared edits two rather than one. Filed unassigned, `finding` label, no `pm:queue`; searched for duplicates first (none)."
      ]
    }

    Deviations / notes for PM:


    Generated by Claude Code


    Generated by Claude Code

  4. yinlianghui-tw commented on Aug 24, 2026

    @yinlianghui-tw
    CollaboratorAuthor

    os-dev-report

    (Supersedes the previous comment: the HTML-comment os-dev-report marker did not survive GitHub's body sanitizer, so this copy leads with the literal text instead. Content is otherwise the same report.)

    {
      "issue": 6027,
      "status": "done",
      "branch": "claude/issue-6027-shadcn-undeclared-local-edits",
      "pr": "https://github.com/objectstack-ai/objectui/pull/6091",
      "premise_still_valid": true,
      "summary": "Built the ruled offline assertion: a new describe block in the existing scripts/__tests__/shadcn-local-patches.test.ts suite (no new workflow) asserting the SHIPPED packages/components/src/ui/sidebar.tsx carries no Tailwind v3 [--var] arbitrary values. Read off disk via fs.readFileSync, not from a vendored fixture, because the regression guarded is 'a --force sync overwrote the shipped file'. Did NOT declare the Tailwind migration as a patch family, per the ruling. The block also guards the inverse (correct v4 (--var) spellings must stay green), pins the tolerated/refused counts so a sync that deletes the utilities outright cannot pass as 'no v3 spellings found', and its docblock records the other three undeclared edits with the gate catching each. One correction to the card's table surfaced while writing that record: hideOverlay has ZERO consumers in the tree (the DesignDrawer that ROADMAP.md credits no longer exists), so as an optional prop its loss type-checks clean and the type-check gate the card credits is vacuous today — recorded in the docblock, filed as #6090, not fixed here.",
      "tests": "All from the repo ROOT, never package-scoped (this repo's vitest-invocation-guard refuses package-scoped runs). Exit codes captured by redirecting to a file BEFORE any pipe, never via tail.\n\n(1) Union at the FINAL commit 0eadb67ab (sha read from that same run; HEAD identical before and after): NODE_OPTIONS=--max-old-space-size=4096 npx vitest run scripts/__tests__/ -> VITEST_EXIT=0, 'Test Files  65 passed (65) / Tests  1793 passed (1793)'. Includes scripts-type-check.test.ts (type-checks the changed file) and check-changeset-fixed.test.ts.\n\n(2) Targeted: npx vitest run scripts/__tests__/shadcn-local-patches.test.ts -> EXIT=0, 'Test Files  1 passed (1) / Tests  43 passed (43)'.\n\n(3) npx eslint scripts/__tests__/shadcn-local-patches.test.ts -> ESLINT_EXIT=0, no output.\n\n(4) Control-byte scan grep -naP over the changed file -> exit 1 (clean).\n\nNON-VACUITY / ABLATION. No rebuild leg applies, and that is a property of the resolution path rather than an omission: the assertion reads the .tsx SOURCE directly through fs.readFileSync(path.join(uiDir,'sidebar.tsx')) — no package exports resolution, no dist/ that could serve stale bytes, so the mutated file IS what the test reads. The mutation script carried a trap on EXIT INT TERM restoring the file, so a foreground-cap SIGTERM could not leave the tree mutated.\n\nMutation confirmed ON DISK by grepping the literal, NOT by the editor's exit code (perl -i returns 0 on zero matches): injected literal 'w-[--sidebar-width]' occurrence count = 1, with the line echoed back — line 199, \"flex h-full w-[--sidebar-width] flex-col bg-sidebar text-sidebar-foreground\" — and the REMOVED literal 'w-(--sidebar-width) flex-col' count = 0. Both anchors checked, in both directions.\n\nObserved direction (predicted before running: turn red): 'Tests  3 failed | 40 passed (43)'. The guard failed naming BOTH the spelling and the file: 'AssertionError: packages/components/src/ui/sidebar.tsx contains Tailwind v3 bare-custom-property arbitrary value(s) ... Offenders: packages/components/src/ui/sidebar.tsx:199: [--sidebar-width]'. The inverse guard failed independently ('expected 6 to be greater than or equal to 7') as did the count pin ('v4Shorthand: 6' vs 7) — the same one-character mutation is caught three ways. The literal-based 'distinguishes' test stayed green, correctly, since it does not read the file.\n\nRESTORE leg: git checkout HEAD -- packages/components/src/ui/sidebar.tsx, proven by an EMPTY git diff HEAD for that path and counts back to 7 v4 shorthand / 0 v3; git status --short then showed only my own test file modified.\n\nSPELLING DISCRIMINATION (the counts asked for, measured on the shipped file): 7 v4 shorthand -(--var) TOLERATED; 6 var(--x) calls inside arbitrary values TOLERATED (e.g. w-[calc(var(--sidebar-width-icon)_+_1rem)], shadow-[0_0_0_1px_hsl(var(--sidebar-border))]); 0 v3 [--var] REFUSED. The pattern requires [ followed IMMEDIATELY by -- and closed by ], so it matches neither a var() call inside brackets nor the arbitrary-PROPERTY form [--sidebar-width:16rem] (still valid on v4 — the trailing colon breaks the match, deliberately). Proven on literals independently of the file, so it keeps holding after the file changes.\n\nSCOPE CHECK: scanned all of packages/components/src/ui/*.tsx plus app-shell/AppSidebar.tsx and cli/app-generator.ts for v3 spellings — zero found, so the 925051db6 migration holds tree-wide and there is no live defect beyond the guarded file.",
      "open_questions": [],
      "out_of_scope_findings": [
        "filed as #6090: sheet.tsx's undeclared hideOverlay prop has zero consumers (the DesignDrawer ROADMAP.md credits no longer exists), so as an optional prop its loss type-checks clean — the type-check gate #6027's table credits with catching it is vacuous today, making the count of unguarded undeclared edits two rather than one. Filed unassigned, finding label, no pm:queue; searched for duplicates first (none)."
      ]
    }

    Deviations / notes for PM:


    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

Labels

domain:devxobjectui devx stream: fix lands on .github/, scripts/ or release pipeline — devx lane cross-repopm:dispatchedtooling

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions