From 55dfbfb33279fa0b466d6be8359dd8b95f525b0b Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 22 Aug 2026 01:39:40 +0000 Subject: [PATCH 1/3] fix(console): the SDUI authoring preview stops teaching Tailwind in page source MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `apps/console/src`'s three ADR-0080 browser preview harnesses authored Tailwind classNames inside real page `source` strings. Page `source` is RUNTIME metadata: the console's Tailwind is compiled at BUILD time by scanning the console's own `src` (`@source '../src/**'` in `index.css`) with no safelist, so a utility class authored in real page metadata produces no CSS and no error anywhere — the ADR-0065 "works only by coincidence" failure, recorded as ADR-0080's 2026-06-30 amendment. These harnesses are the one place in the repo where the rule is violated AND STILL LOOKS RIGHT, because each harness file is itself inside the scanned `src`. Split by what each harness claims to be: - `sdui-tiers-preview.tsx` labels itself "Browser preview for the two AI-authoring tiers", so it is the one making an authoring claim. Its two sources are rewritten to each tier's real primitive — the html tier with ``'s structured props plus JSON `style` objects, the react tier with inline `style` objects, colours as `hsl(var(--token))`. It now demonstrates what authors are told to write, exercises the token path in a browser, and follows light/dark instead of pinning a fixed slate/indigo palette. - `sdui-jsx-preview.tsx` and `sdui-workbench-preview.tsx` are renderer-plumbing previews. They keep their Tailwind and declare the exception in their headers, naming the build-scan coincidence and pointing at the authoring example. `__tests__/sdui-preview-page-source-styling.test.ts` pins both halves against the shipped rule itself (`validatePageSourceStyling` / `page-source-className-tailwind` from `@objectstack/lint`, the same finding `os validate` gives an author) rather than a local re-implementation: zero findings for the authoring harness, expected findings WITH the header note for the other two, and no third path for a new harness. Part of #5470 Co-authored-by: Claude --- .../sdui-preview-page-source-tailwind-5470.md | 26 +++ apps/console/package.json | 1 + .../sdui-preview-page-source-styling.test.ts | 185 ++++++++++++++++++ apps/console/src/sdui-jsx-preview.tsx | 24 ++- apps/console/src/sdui-tiers-preview.tsx | 109 ++++++++--- apps/console/src/sdui-workbench-preview.tsx | 23 ++- pnpm-lock.yaml | 3 + 7 files changed, 336 insertions(+), 35 deletions(-) create mode 100644 .changeset/sdui-preview-page-source-tailwind-5470.md create mode 100644 apps/console/src/__tests__/sdui-preview-page-source-styling.test.ts diff --git a/.changeset/sdui-preview-page-source-tailwind-5470.md b/.changeset/sdui-preview-page-source-tailwind-5470.md new file mode 100644 index 0000000000..f14b09da71 --- /dev/null +++ b/.changeset/sdui-preview-page-source-tailwind-5470.md @@ -0,0 +1,26 @@ +--- +--- + +Publishes nothing — declared with an empty frontmatter rather than left undeclared. + +The changed files are `apps/console`'s three ADR-0080 browser preview harnesses +(`src/sdui-*-preview.tsx`) plus a test. They are dev-server-only: `apps/console`'s +vite config declares no `build.rollupOptions.input`, so the build's only entry is +`index.html` (resolved config, measured: `input` unset) — the three +`sdui-*-preview.html` entries and the modules behind them never enter `dist/`. +`@object-ui/console`'s `files` omits `src`, and its `exports` map has exactly one +entry (`.` → `./plugin.js`, built from `tsconfig.plugin.json` including +`plugin.ts` alone, which imports nothing from `src/`). Nothing here reaches a +consumer. + +Behind the change: page `source` is runtime metadata, and the console's Tailwind +is compiled at build time by scanning the console's own `src` with no safelist, so +a utility class authored in real page metadata produces no CSS and no error +(ADR-0065; ADR-0080's 2026-06-30 amendment). `sdui-tiers-preview.tsx` — the +harness making an explicit authoring claim — now styles with each tier's real +primitive (the html tier with ``'s structured props plus JSON `style` +objects, the react tier with inline `style` objects, colours as +`hsl(var(--token))`), so it demonstrates what authors are told to write and now +follows the theme in light and dark. The two renderer-plumbing harnesses keep +their Tailwind and declare the exception in their headers; a test pins both +halves against the shipped `page-source-className-tailwind` rule. diff --git a/apps/console/package.json b/apps/console/package.json index 52cabaa644..0d050ae94a 100644 --- a/apps/console/package.json +++ b/apps/console/package.json @@ -91,6 +91,7 @@ "@object-ui/test-support": "workspace:*", "@object-ui/types": "workspace:*", "@objectstack/client": "^17.0.0", + "@objectstack/lint": "^17.0.0", "@objectstack/spec": "^17.0.0", "@tailwindcss/postcss": "^4.3.3", "@tailwindcss/typography": "^0.5.20", diff --git a/apps/console/src/__tests__/sdui-preview-page-source-styling.test.ts b/apps/console/src/__tests__/sdui-preview-page-source-styling.test.ts new file mode 100644 index 0000000000..36ee4c12b9 --- /dev/null +++ b/apps/console/src/__tests__/sdui-preview-page-source-styling.test.ts @@ -0,0 +1,185 @@ +/** + * The three ADR-0080 browser preview harnesses, held to the page-source styling + * rule BY THAT RULE — `validatePageSourceStyling` from `@objectstack/lint`, the + * same `page-source-className-tailwind` an author gets from `os validate`. Not a + * local re-implementation: a copy of a rule cannot disagree with itself. + * + * WHY THIS FILE EXISTS (objectui#5470). A page's `source` is RUNTIME metadata. + * The console's Tailwind is compiled at BUILD time by scanning the console's own + * `src` (`@source '../src/**'` in `apps/console/src/index.css`) with no + * safelist, so a utility class authored in real page metadata produces no CSS + * and no error anywhere — the ADR-0065 "works only by coincidence" failure, + * recorded as ADR-0080's 2026-06-30 amendment. These three harnesses are the one + * place in the repo where the rule is violated AND STILL LOOKS RIGHT, because + * each harness file is itself inside the scanned `src`. Lift one of their source + * strings into a real page and every class evaporates silently. + * + * So the harnesses are split, and both halves are pinned here: + * - `sdui-tiers-preview.tsx` makes an explicit AUTHORING claim ("Browser + * preview for the two AI-authoring tiers"), so it was rewritten to each + * tier's real primitive and must stay at ZERO findings. + * - the other two are renderer-PLUMBING previews that keep their Tailwind and + * declare the exception in their headers. They are pinned as EXPECTED + * findings carrying that note — so the note cannot be deleted while the + * classNames stand, a cleanup cannot land unnoticed, and a NEW harness + * cannot quietly inherit the exception. + */ +import { describe, it, expect } from 'vitest'; +import { readFileSync, readdirSync } from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { validatePageSourceStyling, PAGE_SOURCE_CLASSNAME } from '@objectstack/lint'; +import { parseJsx } from '@object-ui/sdui-parser'; + +const srcDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); + +/** The header line every harness that keeps its Tailwind must carry. */ +const EXCEPTION_ANCHOR = ' * ADR-0080 EXCEPTION — Tailwind in page source'; + +interface HarnessPage { + kind: string; + name: string; + source: string; +} + +/** + * Pull the page objects out of a harness file: every `kind: ''` inside an + * object literal that also carries a `source`, with the source resolved from + * the template literal it names (or the ES-shorthand `source` const). + * + * Deliberately fails loudly rather than returning nothing: an extractor that + * silently finds zero pages is indistinguishable from a clean file, which is + * the exact way a count-based guard rots (objectui#5470 — the card's own 79 was + * a whole-file `grep -c`, i.e. LINES, harness JSX included; the rule's own count + * over the source strings is 95). + */ +function pagesOf(text: string): HarnessPage[] { + const pages: HarnessPage[] = []; + for (const m of text.matchAll(/\bkind:\s*'([a-z]+)'/g)) { + // widen from the `kind:` match to the enclosing object literal + const open = text.lastIndexOf('{', m.index); + if (open < 0) continue; + let depth = 0; + let close = open; + for (; close < text.length; close++) { + if (text[close] === '{') depth++; + else if (text[close] === '}' && --depth === 0) break; + } + const objText = text.slice(open, close + 1); + const bound = objText.match(/\bsource:\s*([A-Za-z_$][\w$]*)/); + const ident = bound ? bound[1] : /\bsource\s*[,}]/.test(objText) ? 'source' : null; + if (!ident) continue; + const decl = new RegExp(String.raw`^const\s+${ident}\s*=\s*\``, 'm').exec(text); + if (!decl) continue; + const start = decl.index + decl[0].length; + let i = start; + while (i < text.length) { + if (text[i] === '\\') { i += 2; continue; } + if (text[i] === '`') break; + i++; + } + const name = objText.match(/\bname:\s*'([^']+)'/)?.[1] ?? ident; + pages.push({ kind: m[1], name, source: text.slice(start, i) }); + } + return pages; +} + +function findingsFor(pages: HarnessPage[]) { + return validatePageSourceStyling({ pages: pages as unknown as Record[] }); +} + +const read = (file: string) => readFileSync(path.join(srcDir, file), 'utf8'); + +describe('ADR-0080 preview harnesses — page-source styling', () => { + // ---- the instrument, before anything is asserted with it ---------------- + it('the rule fires on an authored className (control)', () => { + const dirty = findingsFor([ + { kind: 'html', name: 'control', source: '
x
' }, + ]); + expect(dirty.map((f) => f.rule)).toEqual([PAGE_SOURCE_CLASSNAME]); + expect(dirty[0].message).toContain('1 `className` attribute'); + + // …and is silent on the same markup styled the prescribed way, so a green + // result below means "clean", not "rule inert". + const clean = findingsFor([ + { kind: 'html', name: 'control', source: '
x
' }, + ]); + expect(clean).toEqual([]); + }); + + // ---- (a) the authoring example: zero findings --------------------------- + it('sdui-tiers-preview.tsx authors no Tailwind in either page source', () => { + const pages = pagesOf(read('sdui-tiers-preview.tsx')); + expect(pages.map((p) => `${p.name}:${p.kind}`)).toEqual([ + 'release_notes:html', + 'pipeline_react:react', + ]); + expect(findingsFor(pages)).toEqual([]); + }); + + it("the html-tier source's JSON style objects materialize (not deferred expressions)", () => { + const [html] = pagesOf(read('sdui-tiers-preview.tsx')); + const parsed = parseJsx(html.source); + expect(parsed.diagnostics.filter((d) => d.severity === 'error')).toEqual([]); + + // A braced attribute is materialized by JSON.parse and kept as `{ $expr }` + // otherwise, so JS-object syntax (`style={{ padding: 4 }}`) would parse + // without complaint and render NOTHING. Every style here must be a real + // object, and there must be some. + const styles: unknown[] = []; + const walk = (node: unknown): void => { + if (!node || typeof node !== 'object') return; + if (Array.isArray(node)) return node.forEach(walk); + const rec = node as Record; + if ('style' in rec) styles.push(rec.style); + if (Array.isArray(rec.children)) rec.children.forEach(walk); + }; + walk(parsed.tree); + expect(styles.length).toBeGreaterThan(8); + for (const s of styles) { + expect(s).toBeTypeOf('object'); + expect(s).not.toHaveProperty('$expr'); + } + }); + + // ---- (b) the plumbing previews: exception kept, and declared ------------ + it.each([ + ['sdui-jsx-preview.tsx', 48], + ['sdui-workbench-preview.tsx', 21], + ])('%s keeps its Tailwind AND declares the exception', (file, expected) => { + const text = read(file); + const pages = pagesOf(text); + expect(pages).toHaveLength(1); + + const findings = findingsFor(pages); + expect(findings.map((f) => f.rule)).toEqual([PAGE_SOURCE_CLASSNAME]); + // The rule counts `className=` inside the source string; pinning the number + // means a class added or removed here is a deliberate, reviewed edit. + expect(findings[0].message).toContain(`${expected} \`className\` attributes`); + + // The note is what makes the exception legible to the next reader. If the + // classNames ever go, this assertion is the reminder to drop the note too. + expect(text.split('\n')).toContain(EXCEPTION_ANCHOR); + }); + + // ---- no silent third path ---------------------------------------------- + it('every preview harness in src/ is one of the two declared shapes', () => { + const harnesses = readdirSync(srcDir).filter((f) => /-preview\.tsx$/.test(f)); + expect(harnesses.length).toBeGreaterThanOrEqual(3); + + for (const file of harnesses) { + const text = read(file); + const pages = pagesOf(text); + if (pages.length === 0) continue; // not a source-tier harness + const dirty = findingsFor(pages).length > 0; + const declared = text.split('\n').includes(EXCEPTION_ANCHOR); + expect( + dirty === declared, + dirty + ? `${file} authors Tailwind in page source without the "${EXCEPTION_ANCHOR.trim()}" header note. ` + + 'Style with the tier primitive (see sdui-tiers-preview.tsx), or declare the exception.' + : `${file} carries the exception note but authors no Tailwind in page source — drop the note.`, + ).toBe(true); + } + }); +}); diff --git a/apps/console/src/sdui-jsx-preview.tsx b/apps/console/src/sdui-jsx-preview.tsx index 7f04c18ec1..d4fc0fc613 100644 --- a/apps/console/src/sdui-jsx-preview.tsx +++ b/apps/console/src/sdui-jsx-preview.tsx @@ -1,6 +1,26 @@ /* 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. */ + * PageRenderer (compile -> SchemaRenderer). + * + * ADR-0080 EXCEPTION — Tailwind in page source + * The page source below authors Tailwind classNames. The rule forbids that in + * real page metadata: `source` is RUNTIME metadata, the console's Tailwind is + * compiled at BUILD time by scanning the console's own `src` (`@source + * '../src/**'` in index.css) with no safelist, so a utility class authored in a + * real page produces no CSS and no error anywhere. `os validate` reports it as + * `page-source-className-tailwind` (ADR-0065; ADR-0080's 2026-06-30 amendment; + * `content/docs/guide/react-pages.md` §Styling). + * + * It renders fully styled HERE only because this harness file is itself inside + * the scanned `src` — precisely the "works only by coincidence" failure mode + * ADR-0065 names. This file is a renderer-PLUMBING preview (does the jsx tier + * compile and mount the registered blocks), not an authoring example: DO NOT + * copy its source string into a page. The authoring example is + * `sdui-tiers-preview.tsx`, whose sources carry no className at all and style + * with each tier's real primitive. + * + * `__tests__/sdui-preview-page-source-styling.test.ts` pins this exception: + * the note cannot be dropped while the classNames stand, and a NEW harness + * cannot inherit the exception unnoticed. */ import './index.css'; import '@object-ui/components'; import React from 'react'; diff --git a/apps/console/src/sdui-tiers-preview.tsx b/apps/console/src/sdui-tiers-preview.tsx index 96126ce911..c6339ee624 100644 --- a/apps/console/src/sdui-tiers-preview.tsx +++ b/apps/console/src/sdui-tiers-preview.tsx @@ -1,12 +1,25 @@ /* Browser preview for the two AI-authoring tiers, each rendered through the * REAL PageRenderer (not a private harness): * - kind:'html' — constrained JSX with the full native HTML tag set (h1/p/ - * a/ul/li/img/strong/blockquote), parsed, never executed. + * a/ul/li/img/strong/blockquote) plus the blocks' own structured props + * (), parsed, never executed. * - kind:'react' — real React (useState/map/onClick) + an injected data block, * executed by @object-ui/react-runtime. Gated by CAP_REACT_PAGES, enabled * here to exercise the trusted tier. - * Tailwind v4 scans this file's text for class candidates, so the runtime - * source strings below are fully styled. */ + * + * STYLING — the two page sources below carry NO Tailwind classes, and must not + * acquire any. A page's `source` is runtime metadata: the console's Tailwind is + * compiled at build time by scanning the console's own `src` (`@source + * '../src/**'` in index.css) with no safelist, so a utility class authored in + * real page metadata produces no CSS and no error anywhere. (ADR-0065; ADR-0080's + * 2026-06-30 amendment; `content/docs/guide/react-pages.md` §Styling; `os + * validate` reports it as `page-source-className-tailwind`.) This file makes an + * explicit authoring claim, so its sources style the way authors are told to: + * the html tier with the blocks' structured props plus a JSON `style` object, + * the react tier with inline `style` objects — colours from the theme as + * `hsl(var(--token))`, so both tiers follow light/dark and whatever theme the + * deployment installs. `__tests__/sdui-preview-page-source-styling.test.ts` + * holds this file to zero authored classNames. */ import './index.css'; import '@object-ui/components'; import React from 'react'; @@ -17,60 +30,92 @@ import { enableCapability, CAP_REACT_PAGES } from '@object-ui/core'; // Trusted tier on (a host would do this; never authored metadata). enableCapability(CAP_REACT_PAGES); +// Layout comes from 's structured props (the renderer emits the classes +// from its OWN source, which the build does scan); everything else is a JSON +// `style` object — quoted keys and quoted values, because the html tier +// materializes a braced attribute with JSON.parse and keeps anything else as a +// deferred expression. const htmlSource = ` -
-

Release Notes

-

A kind:'html' page — plain native HTML tags, Tailwind classes, parsed (never executed).

-
-

What shipped

-
    -
  • Full HTML tag set in the html tier.
  • -
  • A trusted react tier behind a flag.
  • -
  • Author writes markup; the platform renders it.
  • -
-
-

“The best custom page is one the AI can write and the platform can render safely.”

-
- banner +
+ + +

Release Notes

+

A kind:'html' page — native HTML tags and the blocks' structured props, parsed (never executed), styled from theme tokens.

+
+
+ +

What shipped

+
    +
  • Full HTML tag set in the html tier.
  • +
  • A trusted react tier behind a flag.
  • +
  • Author writes markup; the platform renders it.
  • +
+
+
+

“The best custom page is one the AI can write and the platform can render safely.”

+
+ banner +
`; const htmlPage = { type: 'home', kind: 'html', name: 'release_notes', label: 'Release Notes', source: htmlSource }; +// The react tier's styling primitive: inline `style` objects, colours as +// `hsl(var(--token))`. Real JS, so shared style objects are just consts. const reactSource = ` function Page() { const [sortKey, setSortKey] = React.useState('amount'); const [count, setCount] = React.useState(0); const rows = [...data].sort((a, b) => (sortKey === 'amount' ? b.amount - a.amount : a.name.localeCompare(b.name))); const total = data.reduce((s, r) => s + r.amount, 0); + const cell = { padding: 'var(--space-2) 0', borderBottom: '1px solid hsl(var(--border))' }; + const head = { padding: 'var(--space-2) 0', fontWeight: 500, color: 'hsl(var(--muted-foreground))' }; return ( -
-
-

Pipeline (real React)

+
+
+

Pipeline (real React)

-

useState · map · reduce · sort · onClick — executed by the runtime.

-
+

useState · map · reduce · sort · onClick — executed by the runtime.

+
{['amount', 'name'].map((k) => ( ))}
- - +
AccountAmount
+ {rows.map((r) => ( - - - + + + ))}
AccountAmount
{r.name}{'$' + r.amount.toLocaleString()}
{r.name}{'$' + r.amount.toLocaleString()}
-
Total: {'$' + total.toLocaleString()}
+
Total: {'$' + total.toLocaleString()}
); }`; @@ -92,9 +137,9 @@ const reactPage = { createRoot(document.getElementById('root')!).render( -
+
-
+
, diff --git a/apps/console/src/sdui-workbench-preview.tsx b/apps/console/src/sdui-workbench-preview.tsx index 346a031d9d..f4d5de9c45 100644 --- a/apps/console/src/sdui-workbench-preview.tsx +++ b/apps/console/src/sdui-workbench-preview.tsx @@ -2,7 +2,28 @@ * ( + ) with React state — a master/detail workbench, * the exact pattern of examples/app-showcase crm-workbench.page.ts. Rendered * through the real PageRenderer; a tiny in-memory adapter stands in for the - * backend so the real plugins mount and the interaction is genuinely exercised. */ + * backend so the real plugins mount and the interaction is genuinely exercised. + * + * ADR-0080 EXCEPTION — Tailwind in page source + * The page source below authors Tailwind classNames. The rule forbids that in + * real page metadata: `source` is RUNTIME metadata, the console's Tailwind is + * compiled at BUILD time by scanning the console's own `src` (`@source + * '../src/**'` in index.css) with no safelist, so a utility class authored in a + * real page produces no CSS and no error anywhere. `os validate` reports it as + * `page-source-className-tailwind` (ADR-0065; ADR-0080's 2026-06-30 amendment; + * `content/docs/guide/react-pages.md` §Styling). + * + * It renders fully styled HERE only because this harness file is itself inside + * the scanned `src` — precisely the "works only by coincidence" failure mode + * ADR-0065 names. This file is a renderer-PLUMBING preview (do the real + * / blocks mount and interact inside an executed react + * page), not an authoring example: DO NOT copy its source string into a page. + * The authoring example is `sdui-tiers-preview.tsx`, whose sources carry no + * className at all and style with each tier's real primitive. + * + * `__tests__/sdui-preview-page-source-styling.test.ts` pins this exception: + * the note cannot be dropped while the classNames stand, and a NEW harness + * cannot inherit the exception unnoticed. */ import './index.css'; import '@object-ui/components'; import '@object-ui/plugin-grid'; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 37d9cdc7f2..aeaad9f176 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -275,6 +275,9 @@ importers: '@objectstack/client': specifier: ^17.0.0 version: 17.1.0(ai@7.0.65(zod@4.4.3)) + '@objectstack/lint': + specifier: ^17.0.0 + version: 17.1.0(ai@7.0.65(zod@4.4.3)) '@objectstack/spec': specifier: ^17.0.0 version: 17.1.0(ai@7.0.65(zod@4.4.3)) From d472dd920ad69df6f9684b98d2809e970f6482c9 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 22 Aug 2026 01:54:55 +0000 Subject: [PATCH 2/3] test(console): read the harness files through Vite's raw glob, not node:fs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The console app's tsconfig is browser-only (`lib: ES2020, DOM`, `types` without `node`), so a `node:fs` import passes under Vitest and fails `tsc` in the console's own build — the trap `insecure-origin-crypto.placement.test.ts` already records. `import.meta.glob('../*-preview.tsx', { query: '?raw' })` reads the same bytes and is a better enumeration besides: Vite expands it against the real directory at transform time, so a NEW preview harness is judged without anyone remembering to list it. Co-authored-by: Claude --- .../sdui-preview-page-source-styling.test.ts | 46 +++++++++++++++---- 1 file changed, 36 insertions(+), 10 deletions(-) diff --git a/apps/console/src/__tests__/sdui-preview-page-source-styling.test.ts b/apps/console/src/__tests__/sdui-preview-page-source-styling.test.ts index 36ee4c12b9..94a6cf2dc9 100644 --- a/apps/console/src/__tests__/sdui-preview-page-source-styling.test.ts +++ b/apps/console/src/__tests__/sdui-preview-page-source-styling.test.ts @@ -25,13 +25,23 @@ * cannot quietly inherit the exception. */ import { describe, it, expect } from 'vitest'; -import { readFileSync, readdirSync } from 'node:fs'; -import path from 'node:path'; -import { fileURLToPath } from 'node:url'; import { validatePageSourceStyling, PAGE_SOURCE_CLASSNAME } from '@objectstack/lint'; import { parseJsx } from '@object-ui/sdui-parser'; -const srcDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); +/** + * The harness files as TEXT, enumerated by Vite rather than `node:fs`: this + * app's tsconfig is browser-only (`lib: ES2020, DOM`, `types` without `node`), + * so a `node:fs` import passes under Vitest and fails the console's `tsc` — + * the trap `insecure-origin-crypto.placement.test.ts` records. The glob is also + * the enumeration the last test needs: Vite expands it against the real + * directory at transform time, so a NEW harness appears here without anyone + * remembering to add it. + */ +const harnesses = import.meta.glob('../*-preview.tsx', { + query: '?raw', + import: 'default', + eager: true, +}) as Record; /** The header line every harness that keeps its Tailwind must carry. */ const EXCEPTION_ANCHOR = ' * ADR-0080 EXCEPTION — Tailwind in page source'; @@ -88,7 +98,16 @@ function findingsFor(pages: HarnessPage[]) { return validatePageSourceStyling({ pages: pages as unknown as Record[] }); } -const read = (file: string) => readFileSync(path.join(srcDir, file), 'utf8'); +function read(file: string): string { + const text = harnesses[`../${file}`]; + // A missing key means the file was renamed or the glob stopped matching — + // fail loudly rather than assert over an empty string, which reads exactly + // like a clean file. + if (typeof text !== 'string') { + throw new Error(`${file} is not in the preview-harness glob (found: ${Object.keys(harnesses).join(', ')})`); + } + return text; +} describe('ADR-0080 preview harnesses — page-source styling', () => { // ---- the instrument, before anything is asserted with it ---------------- @@ -164,10 +183,17 @@ describe('ADR-0080 preview harnesses — page-source styling', () => { // ---- no silent third path ---------------------------------------------- it('every preview harness in src/ is one of the two declared shapes', () => { - const harnesses = readdirSync(srcDir).filter((f) => /-preview\.tsx$/.test(f)); - expect(harnesses.length).toBeGreaterThanOrEqual(3); - - for (const file of harnesses) { + const files = Object.keys(harnesses).map((k) => k.replace('../', '')); + expect(files.length).toBeGreaterThanOrEqual(3); + expect(files).toEqual( + expect.arrayContaining([ + 'sdui-jsx-preview.tsx', + 'sdui-tiers-preview.tsx', + 'sdui-workbench-preview.tsx', + ]), + ); + + for (const file of files) { const text = read(file); const pages = pagesOf(text); if (pages.length === 0) continue; // not a source-tier harness @@ -177,7 +203,7 @@ describe('ADR-0080 preview harnesses — page-source styling', () => { dirty === declared, dirty ? `${file} authors Tailwind in page source without the "${EXCEPTION_ANCHOR.trim()}" header note. ` + - 'Style with the tier primitive (see sdui-tiers-preview.tsx), or declare the exception.' + 'Style with the tier primitive (content/docs/guide/react-pages.md §Styling), or declare the exception.' : `${file} carries the exception note but authors no Tailwind in page source — drop the note.`, ).toBe(true); } From 70fc8b9a66a279ef4dfecf98d19fcda544f3c3d9 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 22 Aug 2026 02:05:25 +0000 Subject: [PATCH 3/3] fix(console): the html-tier flex columns stretch, so the
is not zero-width MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ``'s `align` defaults to `start` (`items-start`), which makes a child with no intrinsic width collapse. Measured in a real browser: the rewritten html source's `
` resolved to `hidden` — Playwright reported `123 x locator resolved to hidden
` — while its computed border was correct all along, so nothing static could have caught it. `align="stretch"` is the canonical structured prop for it and keeps the fix on this tier's own primitive. Re-measured: `alignItems: stretch`, and the `
`, `
` and `` all report 688px inside a 688px column, matching what the pre-change page rendered. Co-authored-by: Claude --- apps/console/src/sdui-tiers-preview.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/console/src/sdui-tiers-preview.tsx b/apps/console/src/sdui-tiers-preview.tsx index c6339ee624..23e1ae591c 100644 --- a/apps/console/src/sdui-tiers-preview.tsx +++ b/apps/console/src/sdui-tiers-preview.tsx @@ -37,13 +37,13 @@ enableCapability(CAP_REACT_PAGES); // deferred expression. const htmlSource = `
- - + +

Release Notes

A kind:'html' page — native HTML tags and the blocks' structured props, parsed (never executed), styled from theme tokens.


- +

What shipped

  • Full HTML tag set in the html tier.