basquetWi + New ticket
pluto PLUTO-719

UI Button lint misses className overrides when <Button and className are on different lines

Done normal cpcoder-pluto-cc

FOUND while pushing PLUTO-40 Stage B (7a009bd), not looked for. .gitpush-pre.sh:233 gates <Button className> overriding size/color: if BUTTON_OPEN.search(line) and 'className=' in line and FORBIDDEN_BTN_CLASS.search(line) All three conditions are tested against ONE line of the diff. So the rule only fires when <Button, className= and the offending utility all land on the SAME added line. A multi-line JSX element -- the normal formatting once a Button has more than two props -- puts className on its own line and the rule cannot see it. DEMONSTRATED, both directions, same file same push: my single-line 'size="sm" className="h-11 sm:h-8"' was CAUGHT and failed the push. The two pre-existing buttons at practica-detail.tsx:335/347 carry className="h-11 sm:h-6 px-3 sm:px-2 text-xs" -- four forbidden utilities, exactly what the rule targets -- and pass, because they are formatted across several lines. They have been in the tree unflagged. So the lint currently enforces a FORMATTING convention, not a design-system rule: it penalises compact JSX and exempts verbose JSX. That is worse than not having it, because a green hook reads as 'no overrides in this diff'. It also means the sanctioned workaround for the rule is to reformat, which is why I added a size variant instead (sm-field) rather than splitting my line -- but the next person under time pressure will just hit Enter. FIX: accumulate the JSX element rather than matching per-line -- track from <Button until the matching > and test the whole element text. Diff-only scanning makes that fiddly (an element can straddle the hunk boundary), so the cheap correct version is to scan the FULL FILE of any .tsx touched by the diff, and report only elements whose lines intersect added lines. SWEEP NEEDED: nobody knows how many existing overrides this has been missing. The count is the first deliverable -- a full-file scan across src/**/*.tsx will say whether this is 2 buttons or 200, and that number decides whether the fix ships with a cleanup or with a grandfather list. Do NOT 'fix' by deleting the rule. The rule is right (a one-off className override is how a design system stops being one); it is the matching that is wrong. Same class as pluto-fn-replace-asserts-must-check-the-body: an assertion that inspects a proxy for the thing instead of the thing.

Sub-tickets

No sub-tickets.
+ Add sub-ticket

Activity

  • wi-cli-venus created · 7w ago
  • wi-cli-venus note · 7w ago
    SWEEP DONE — the number the WI asked for, measured across src/**/*.tsx (excluding src/components/ui/, which the lint exempts by design). 36 <Button> elements carry a className with a forbidden utility (h-*/bg-*/border-*/rounded-*/px-*/py-*/text-xs/text-sm), across 21 files. - caught by the current single-line rule: 2 - MISSED because className sits on a different line: 34 So the lint sees 6% of what it targets. It is not 'mostly working with an edge case' — the edge case is the common case, because any Button with more than about two props gets formatted across lines by default. Distribution (top files): teacher-report 6, export-buttons 3, administrar/docente-membership-actions 3, login-client 2, roster-history-log 2, support-sop-ref 2, audit-log 2, practica-detail 2, impersonation-banner 2, then a tail of 1s across 9 more files. WHAT THIS CHANGES ABOUT THE FIX: 34 is too many to clean up inside the same push that fixes the matcher, and blocking every future push on a 34-item backlog would strand the next unrelated change. Ship it in two steps — (1) fix the matcher to scan whole elements and land it with an explicit grandfather list of the 34 known sites, so the hook goes green on the existing tree but rejects any NEW override; (2) burn the list down separately, per file, on its own WI. Step 1 is the one with value: it stops the bleed. Step 2 is cosmetics with real regression risk (each of these 34 is a live rendering decision someone made, and several are almost certainly load-bearing — export-buttons and practica-detail's are mobile-height fixes from PLUTO-40 Stage A). Method note for whoever picks this up: I measured with a brace-depth scan from <Button to its closing >, which is approximate on elements containing nested JSX in props. Treat 36 as accurate to within a couple, not exact. The 34/2 split is the load-bearing figure and is not sensitive to that imprecision. Confirms the WI's framing: the rule is right, the matching is wrong, and a green hook has been reading as 'no overrides in this diff' while missing 94% of them.
  • wi-cli-venus note · 7w ago
    Matcher fix + counted baseline shipped 91302c1. Body extracted to scripts/ui-standard-lint.py (was an inline heredoc in .gitpush-pre.sh, untestable). Element-level parse of whole touched files; rules 1/4 stay per-line. Baseline is path::normalized-className => count, 29 keys / 36 occurrences (6 keys repeat, which is exactly the presence-key evasion audit-pluto-ca named). Fails closed on unparseable JSX. 14 tests in src/lib/ui-standard-lint.test.ts, run by pnpm test so every push exercises the parser; multiline-catch, duplicate-rejection and fail-closed each mutation-checked. NOT closed: the 34 multi-line sites are still in the tree, carried at baseline. Burning them down needs size variants first (several are load-bearing PLUTO-40 Stage A h-11 sm:h-N mobile fixes) - that is the remaining leg of this WI or a follow-up, pm's call.
  • wi-cli-venus completed · 7w ago
    Shipped 91302c1 v2.23.34, PTD green (live 2.23.34, dpl_D6GofQHQk5JaMNNXqdUzoRGL1ZMZ). Lint body extracted to scripts/ui-standard-lint.py and rewritten to parse whole touched files element-by-element, so multi-line JSX is no longer invisible; rules 1/4 stay per-line (those open tags are single-line by construction). Existing sites carried as a COUNTED baseline (path::normalized-className => count), audit-pluto-ca's call over my proposed presence key, which would have let a second byte-identical override ride the first one's entry - live in the tree, 29 keys covering 36 occurrences. Fails closed on unparseable JSX. 14 tests in src/lib/ui-standard-lint.test.ts driving the real script, run by pnpm test; multiline-catch, duplicate-rejection and fail-closed each mutation-checked; end-to-end negative proven on the real hook against an already-baselined file. Coverage: parser, baseline arithmetic and hook firing are proven. NOT exercised - another lane pushing through the new gate; no runtime code changed, so the PTD is build health, not behaviour. The 34 sites themselves remain at baseline and are now PLUTO-720 per pm's call, not held open here.
bug
7w ago by wi-cli-venus
7w ago
2026-08-22 10:57