@adia-ai/adia-ui-forge 0.8.58 → 0.8.60

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -154,7 +154,13 @@ for any constant or decision lives in git and PR descriptions
154
154
  7. **Registry ↔ catalog parity.** A component in the runtime registry but
155
155
  missing from catalog schemas silently drops from generated compositions —
156
156
  `npm run check:registry-catalog-coherence` guards it; run it after catalog
157
- changes.
157
+ changes. `packages/gen-ui/a2ui/registry.js` is Class R (ADR-0069,
158
+ gh#3055): never hand-edit it — a new component enters through its yaml
159
+ `component:`/`tag:` fields, an alias or native-element mapping through
160
+ `packages/gen-ui/a2ui/registry.exceptions.json`, then
161
+ `node scripts/build/a2ui-registry.mjs` (derived-resync regenerates it on
162
+ main; `check:a2ui-registry` is the advisory freshness gate,
163
+ `check:a2ui-registry:validate` the blocking validity gate).
158
164
  8. **Multi-turn emits A2UI `updateComponents` messages, not new compositions.**
159
165
  The chunk-refiner mutates the binding plan via four ops (`rebindSlot` /
160
166
  `appendToSlot` / `removeFromSlot` / `replacePage`); state chains via
@@ -187,6 +193,30 @@ for any constant or decision lives in git and PR descriptions
187
193
  SoT); ADR-0097 rules a fourth, `traits`, but that part is decided-not-
188
194
  yet-shipped (gh#2513) — see `primitive-authoring/references/
189
195
  yaml-contract.md` §Synthesized universal props for the full contract.
196
+ 14. **Provider "extended thinking" is a strategy-level opt-in, not a
197
+ global default** (gh#3516, LLD-0033). PR #3511 (gh#3477) made
198
+ `{ thinking, thinkingBudget }` reachable through
199
+ `AdiaUILLMBridge.complete()`/`stream()`, default off; two call sites
200
+ opt in for real, each behind its own fixed-constant budget rather than
201
+ a caller-threaded option: `generate-thinking.js`'s own generate call
202
+ (`THINKING_BUDGET_MONOLITHIC_THINKING`) and `free-form-composer/
203
+ index.js`'s ingredient-picker call, both its primary pick and its own
204
+ paraphrase-retry (`THINKING_BUDGET_FREE_FORM`), both starting at the
205
+ bridge's own `DEFAULT_THINKING_BUDGET` (10000). `auto` inherits
206
+ through the free-form picker call the moment it escalates that far -
207
+ `monolithic-thinking` itself is never reachable via `auto`'s own
208
+ escalation ladder. Every other LLM call site (`generate-pro.js`'s four
209
+ branches, zettel's locator/modifier/synthesizer, the shared
210
+ `validate-and-repair.js` repair loop) stays thinking-off deliberately,
211
+ a narrow first pass pending real eval numbers - do not assume a new
212
+ engine inherits thinking by proximity to one that has it.
213
+ `StubLLMAdapter` accepts and records `thinking`/`thinkingBudget` on
214
+ its own `calls` log for strategy-level test assertions.
215
+ `eval-diff.mjs` gained `--mode` (`instant`/`pro`/`thinking`), scoped
216
+ to `--engine mcp` only, so `--engine mcp --mode thinking` exercises
217
+ the opted-in path independently of the router's own default. See
218
+ `docs/ops/lld/lld-0033-strategy-thinking-opt-in.md` for the full
219
+ decision record and the conductor's D1-D5 rulings.
190
220
 
191
221
  ## Test + run commands (all verified in root package.json)
192
222
 
@@ -6,7 +6,7 @@ description: >-
6
6
  component's states, composed children, aria behavior, or error/empty/
7
7
  loading handling changes and it already has (or should grow) a
8
8
  `component.md`, or when asked to "add component.md for X" / "write the
9
- screen-reader spec for X" / "why did check:component-md-fresh fail". NOT
9
+ screen-reader spec for X" / "why is check:component-md-fresh warning". NOT
10
10
  the yaml prop/slot/event/token contract itself (primitive-authoring owns
11
11
  that — this skill only owns the two authored yaml fields,
12
12
  `screenReader`/`behavioral`, plus the optional `intent` field); NOT gen-ui
@@ -39,14 +39,16 @@ regenerated from them. This is deliberate, not incidental:
39
39
  gaps — screen-reader and behavioral judgment — get the SAME treatment:
40
40
  authored once, in yaml, transcluded everywhere else (component.md today;
41
41
  gen-ui corpus derivation once a2ui-maintenance wires it in).
42
- - **The freshness gate is PR-blocking, not staleness-only.** Because the
43
- authored content lives in a yaml field, `component.md` is 100%
44
- mechanically regenerable — `check:component-md-fresh` can do a real byte-
45
- diff, the same shape as `check:reference-docs-fresh`, except PR-blocking
46
- (operator ruling 2026-08-31) rather than advisory-only. A hand-edit
47
- directly in `component.md` will be silently clobbered by the next
48
- `npm run docs:component-md` and will fail the gate as "not fresh" — this
49
- is the guard rail, not a bug.
42
+ - **`component.md` is Class R, derived on main, not authored in the PR
43
+ (gh#3172, ADR-0069).** Because the authored content lives in a yaml
44
+ field, `component.md` is 100% mechanically regenerable — a PR commits
45
+ only the `screenReader`/`behavioral` yaml edit; `check:component-md-fresh`
46
+ runs advisory-only inside `check:pr-ready` (WARN, never fails the run)
47
+ and the `push: main` `derived-resync` job regenerates `component.md`
48
+ itself once the PR merges. A hand-edit directly in `component.md` will
49
+ still be silently clobbered by the next `npm run docs:component-md` or
50
+ by `derived-resync` on main — that's the guard rail, not a bug, even
51
+ though nothing blocks the PR on it.
50
52
 
51
53
  ## Authoring a component's two sections
52
54
 
@@ -79,7 +81,7 @@ regenerated from them. This is deliberate, not incidental:
79
81
  ```bash
80
82
  node scripts/build/components.mjs --validate # schema-valid yaml
81
83
  npm run docs:component-md # regenerate component.md
82
- npm run check:component-md-fresh # PR-blocking gate
84
+ npm run check:component-md-fresh # advisory gate (check:pr-ready); derived-resync owns main
83
85
  ```
84
86
 
85
87
  6. If this is the component's FIRST component.md (yaml previously had
@@ -110,7 +112,9 @@ defeats the point of a rollout plan).
110
112
  - `scripts/schemas/component.yaml.schema.json` — `intent`/`screenReader`/
111
113
  `behavioral` field definitions (all optional; a component with a `.yaml`
112
114
  but neither authored field simply has no `component.md` yet).
113
- - `scripts/verify/check-component-md-fresh.mjs` — the PR-blocking gate:
114
- byte-freshness (component.md matches a fresh render) AND same-PR
115
- coverage (a component.md-bearing component's source/yaml change must
116
- touch component.md in the same diff).
115
+ - `scripts/verify/check-component-md-fresh.mjs` — the freshness gate:
116
+ byte-freshness (component.md matches a fresh render). Advisory-only in
117
+ `check:pr-ready` (gh#3172, ADR-0069) a PR commits the yaml edit alone
118
+ and `derived-resync` regenerates `component.md` on `push: main`; the
119
+ same-PR coverage check this gate used to run was removed outright
120
+ (LLD-0020 §1c), not demoted.
@@ -2,8 +2,8 @@
2
2
  name: demo-audit
3
3
  description: >-
4
4
  Seven-mode QA sweep of the monorepo's demo/app surfaces: visual probe,
5
- app-shell QA, attr-quote typos, native-primitive leak, admin-shell
6
- composition, card anatomy, plus an aggregated token/contrast/lifecycle
5
+ app-shell QA, attr-quote typos, native-primitive leak, shell (admin/chat/
6
+ editor) composition, card anatomy, plus an aggregated token/contrast/lifecycle
7
7
  drift battery (`npm run dogfood:status`). Use for "run a dogfood sweep",
8
8
  "find broken demos", "audit native primitive leaks". NOT for gen-UI
9
9
  gallery scoring (gen-ui-review) or authoring primitives (primitive-authoring).
@@ -33,7 +33,7 @@ below — load it before running or triaging.
33
33
  | 2 | App-shell QA — after `apps/` structural sweeps; before a release | [app-shell-pitfalls](references/app-shell-pitfalls.md) |
34
34
  | 3 | HTML attr-quote typo sweep — nested `"` broke an attribute boundary | [html-attr-sweep](references/html-attr-sweep.md) |
35
35
  | 4 | Native-primitive leak — `<button>` where `<button-ui>` exists | [native-leak-annotations](references/native-leak-annotations.md) |
36
- | 5 | Admin-shell composition — incomplete `<admin-shell>` anatomy | [admin-shell-anatomy](references/admin-shell-anatomy.md) |
36
+ | 5 | Shell composition — incomplete `<admin-shell>` / `<chat-shell>` / `<editor-shell>` anatomy | [admin-shell-anatomy](references/admin-shell-anatomy.md), [chat-shell-anatomy](references/chat-shell-anatomy.md), [editor-shell-anatomy](references/editor-shell-anatomy.md) |
37
37
  | 6 | Card structure + anatomy docs coverage | [card-anatomy-sweep](references/card-anatomy-sweep.md) |
38
38
  | 7 | Token/contrast/lifecycle/yaml drift battery — independent of modes 1–6 | [mode7-status-battery](references/mode7-status-battery.md) |
39
39
 
@@ -1,3 +1,3 @@
1
1
  interface:
2
2
  display_name: "Demo Audit"
3
- short_description: "Seven-mode QA sweep of the monorepo's demo/app surfaces: visual probe, app-shell QA, attr-quote typos, native-primitive leak, admin-shell composition, card anatomy, plus an aggregated token/contrast/lifecycle drift battery (`npm run dogfood:status`)."
3
+ short_description: "Seven-mode QA sweep of the monorepo's demo/app surfaces: visual probe, app-shell QA, attr-quote typos, native-primitive leak, shell (admin/chat/ editor) composition, card anatomy, plus an aggregated token/contrast/lifecycle drift battery (`npm run dogfood:status`)."
@@ -4,7 +4,10 @@ Script: `node scripts/dev/audit-app-shells.mjs` (repo-local). Walks every
4
4
  `apps/<name>/…/<page>.html` shell headlessly and checks console errors,
5
5
  custom-element registration, collapsed heights, icon-ui presence, demo-root
6
6
  flex, and network 4xx/5xx. Flags: `--only=NAME` · `--fail-fast` ·
7
- `--compare-prod` (diff registered tags against the prod deploy).
7
+ `--compare-prod` (diff registered tags against the prod deploy) ·
8
+ `--playgrounds` (gh#3197 — also sweeps `playgrounds/<name>/app/<name>.html`,
9
+ same shell shape; opt-in because that root carries other pre-existing,
10
+ unaudited findings — combine with `--only=NAME` to scope to one playground).
8
11
 
9
12
  Prerequisites: `npm run dev` running (vite `:5173`); `npm run proxy` only when
10
13
  probing chat / gen-ui pages. If vite is mid dep-reoptimization the first sweep
@@ -42,6 +45,8 @@ with no console error, regardless of real content height.
42
45
  | `button-ui` (with `icon=`) | `icon-ui` |
43
46
  | `badge-ui` (with `icon=`) | `icon-ui` |
44
47
  | `menu-item-ui` | `icon-ui`, `text-ui` |
48
+ | `admin-roster-ui` | `upload-ui`, `menu-item-ui` |
49
+ | `table-toolbar-ui` | `search-ui`, `select-ui`, `menu-ui`, `menu-item-ui` |
45
50
 
46
51
  ## Registration diagnosis rules
47
52
 
@@ -0,0 +1,98 @@
1
+ # Mode 5b — Chat-shell composition: the canonical parts
2
+
3
+ Script: `scripts/dev/audit-shell-composition.mjs` (repo-local), npm gates
4
+ `audit:shell-composition{,:strict,:all}`. Static AST walk over
5
+ `apps/**/*.html`, `playgrounds/**/*.html`, `catalog/page-shells/**/*.html` —
6
+ no browser needed; pre-commit fast. Shares one script and one output
7
+ contract with the `<admin-shell>` census
8
+ ([admin-shell-anatomy](admin-shell-anatomy.md)) — the script's own `CHAT_PARTS`
9
+ array is the mechanical census; this file is the human review standard.
10
+ Canonical source: `packages/web-modules/chat/chat-shell/chat-shell.yaml`
11
+ (the behavioral contract) and `playgrounds/chat/app/chat.contents.html`
12
+ (the canonical rendered reference, cited by `apps/genui/PATTERNS.md`'s
13
+ chrome-decision table as chat-shell's canonical demo).
14
+
15
+ `gh#2909`/`apps/genui/PATTERNS.md:329-337` established that `apps/genui`
16
+ (gen-ui, factory-chat) is a real `<chat-shell>` consumer that a
17
+ `<admin-shell>`-only mode 5 sweep never scanned — this anatomy closes that
18
+ blind spot.
19
+
20
+ ## The canonical parts
21
+
22
+ 1. `<chat-shell provider="…" model="…" proxy-url="…">` outer.
23
+ 2. `<chat-thread>` direct child — the message scroll surface. Required:
24
+ without it there is nowhere for the host's rendering pipeline to append
25
+ messages.
26
+ 3. `<chat-thread> > <chat-empty>` as (typically first) child — the
27
+ empty-state placeholder shown via the `[empty]` reflected attribute
28
+ before any message exists.
29
+ 4. `<chat-composer>` direct child — the input region.
30
+ 5. `<chat-composer> > <chat-input-ui>` (or `<input-ui>`) inner child — the
31
+ actual input; a composer with no primitive input inside has nothing to
32
+ submit.
33
+ 6. `<chat-header>` direct child — optional top chrome bar (name, status,
34
+ actions). When present, expected to carry `[slot="name"]` and
35
+ `[slot="status"]` (typically a `<chat-status>`) — a header with neither
36
+ is bare chrome with no identifying content.
37
+ 7. `<chat-sidebar slot="leading"|"trailing">` — optional conversation-history
38
+ or inspector rail.
39
+ 8. No generic layout primitive (`<col-ui>`, `<row-ui>`, `<stack-ui>`) as a
40
+ **direct child** of `<chat-shell>` — the shell's CSS lays out children by
41
+ tag selector (`chat-thread`, `chat-composer`, etc.); a generic wrapper
42
+ defeats that and the shell's `:has(chat-thread[streaming])` cross-cut
43
+ styling.
44
+
45
+ ## Severity mapping
46
+
47
+ - **critical** — `<chat-shell>` present but missing `<chat-thread>` or
48
+ `<chat-composer>` (parts 2, 4). The shell can't render a usable
49
+ conversation surface without both.
50
+ - **warning** — `<chat-composer>` has no inner `<chat-input-ui>`/`<input-ui>`
51
+ (part 5); a `<chat-header>` present but missing both `[slot="name"]` and
52
+ `[slot="status"]` content (part 6); a generic layout primitive
53
+ (`col-ui`/`row-ui`/`stack-ui`) authored as a direct child (part 8).
54
+ - **info** — `<chat-thread>` missing its `<chat-empty>` first child (part 3;
55
+ a thread pre-seeded with real messages legitimately skips this);
56
+ `<chat-header>` absent entirely (part 6 is optional chrome);
57
+ `<chat-sidebar>` absent (part 7, forward-looking per the yaml — chat is
58
+ typically single-pane).
59
+
60
+ ## What the script flags (mechanical subset)
61
+
62
+ | Symptom | Diagnosis |
63
+ |---|---|
64
+ | `<chat-shell>` with no `<chat-thread>` | critical — part 2 |
65
+ | `<chat-shell>` with no `<chat-composer>` | critical — part 4 |
66
+ | `<chat-composer>` with no `<chat-input-ui>`/`<input-ui>` child | warning — part 5 |
67
+ | `<chat-header>` present, no `[slot="name"]` and no `[slot="status"]`/`<chat-status>` | warning — part 6 |
68
+ | `<chat-shell>` direct child is `col-ui`/`row-ui`/`stack-ui` | warning — part 8 (legacy-generic-layout leak) |
69
+ | `<chat-thread>` with no `<chat-empty>` child | info — part 3 |
70
+
71
+ ## Opt-out contract
72
+
73
+ Same annotation mechanism as admin-shell: `<chat-shell
74
+ data-shell-opt-out="reason">` downgrades every finding on that shell to
75
+ info and prints the reason for reviewers.
76
+
77
+ ## Triage
78
+
79
+ - Canonical product surface (`apps/genui` chat/factory-chat pages,
80
+ `playgrounds/chat/*`) → fix mandatory.
81
+ - A narrow single-feature playground isolating one chat behavior → fix
82
+ optional; annotate the opt-out.
83
+ - Never point this audit at `packages/web-modules/chat/**/*.examples.html`
84
+ or `packages/web-components/components/*/*.html` (single-primitive
85
+ spotlights) — the script's `isShowcaseDemo`/showcase-path exclusion
86
+ already keeps those out of scope, same as admin-shell.
87
+ - `:strict` is the CI/publish posture; keep warn-only while iterating
88
+ locally.
89
+
90
+ ## Legacy shapes — never re-authored
91
+
92
+ `chat-shell.yaml`'s own description lists the ADR-0024-retired legacy
93
+ data-attribute shapes (`<section data-chat-messages>`, `<chat-input-ui
94
+ data-chat-input>`, `<empty-state-ui data-chat-empty>`, `<header
95
+ data-chat-name>`) as silently unrecognized, not merely deprecated. This
96
+ audit does not re-detect them (a separate concern from anatomy
97
+ completeness) — `verify:no-legacy-shell-shapes` in `npm run check` already
98
+ covers that ground.
@@ -0,0 +1,109 @@
1
+ # Mode 5c — Editor-shell composition: the canonical parts
2
+
3
+ Script: `scripts/dev/audit-shell-composition.mjs` (repo-local), npm gates
4
+ `audit:shell-composition{,:strict,:all}`. Static AST walk over
5
+ `apps/**/*.html`, `playgrounds/**/*.html`, `catalog/page-shells/**/*.html` —
6
+ no browser needed; pre-commit fast. Shares one script and one output
7
+ contract with the `<admin-shell>` census
8
+ ([admin-shell-anatomy](admin-shell-anatomy.md)) — the script's own
9
+ `EDITOR_PARTS` array is the mechanical census; this file is the human
10
+ review standard. Canonical source:
11
+ `packages/web-modules/editor/editor-shell/editor-shell.yaml` (the
12
+ behavioral contract) and `apps/construct-canvas/app/construct-canvas.contents.html`
13
+ (the canonical rendered reference, cited by `apps/genui/PATTERNS.md`'s
14
+ chrome-decision table as editor-shell's canonical demo, alongside
15
+ `apps/genui`'s own a2ui-editor consumer).
16
+
17
+ `gh#2909`/`apps/genui/PATTERNS.md:329-337` established that `apps/genui`
18
+ (a2ui-editor) is a real `<editor-shell>` consumer that a
19
+ `<admin-shell>`-only mode 5 sweep never scanned — this anatomy closes that
20
+ blind spot.
21
+
22
+ ## The canonical parts
23
+
24
+ 1. `<editor-shell>` outer.
25
+ 2. `<editor-canvas>` direct child — the central work surface. Required:
26
+ without it there is nowhere for artboards/document body/canvas content
27
+ to land, the editor equivalent of admin-shell's `<admin-content>`.
28
+ 3. `<editor-toolbar>` direct child — the app-scope top chrome bar (document
29
+ title, run/save/undo/redo, focus-mode toggle). Recommended; an editor
30
+ with no toolbar has no document-wide action surface.
31
+ 4. `<editor-canvas> > <editor-canvas-empty>` as (typically first) child —
32
+ the empty-state placeholder shown via the parent's `[empty]` reflected
33
+ attribute before any content exists.
34
+ 5. `<editor-canvas> > <editor-canvas-toolbar>` — optional canvas-scope
35
+ chrome (view-mode tabs, breadcrumbs) sticky to the canvas top edge;
36
+ distinct from part 3's app-scope toolbar.
37
+ 6. `<editor-statusbar>` direct child — bottom chrome bar (save/sync state,
38
+ zoom, cursor position). Recommended; loses the canonical status-strip
39
+ without it.
40
+ 7. `<editor-sidebar slot="leading"|"trailing">` — optional navigator or
41
+ inspector rail.
42
+ 8. When an `<editor-sidebar>` is present, it must wrap `<pane-ui
43
+ resizable>` (or at minimum `<pane-ui>`) — editor-sidebar is the one
44
+ bespoke shell child that **delegates** rather than duplicates a
45
+ primitive's resize behavior (per `shell-patterns.md`'s "FIRST bespoke
46
+ shell child that delegates" note); an editor-sidebar with no inner
47
+ `<pane-ui>` reimplements drag by hand instead of reusing the primitive.
48
+ 9. No bare `<header>` / `<footer>` native elements as direct children of
49
+ `<editor-shell>` — these are the ADR-0024-retired legacy chrome shapes
50
+ that `<editor-toolbar>` / `<editor-statusbar>` replaced.
51
+
52
+ ## Severity mapping
53
+
54
+ - **critical** — `<editor-shell>` present but missing `<editor-canvas>`
55
+ (part 2). The shell can't render usable content without it.
56
+ - **warning** — no `<editor-toolbar>` (part 3); no `<editor-statusbar>`
57
+ (part 6); an `<editor-sidebar>` present with no inner `<pane-ui>` (part
58
+ 8); a bare native `<header>`/`<footer>` direct child (part 9, the
59
+ retired-legacy-shape leak).
60
+ - **info** — `<editor-canvas>` missing its `<editor-canvas-empty>` first
61
+ child (part 4; a canvas pre-seeded with real content legitimately skips
62
+ this); no `<editor-canvas-toolbar>` (part 5, optional canvas chrome); no
63
+ `<editor-sidebar>` at all (part 7 — `construct-canvas`'s own comment
64
+ notes "no leading pane today", a legitimately sidebar-less composition).
65
+
66
+ ## What the script flags (mechanical subset)
67
+
68
+ | Symptom | Diagnosis |
69
+ |---|---|
70
+ | `<editor-shell>` with no `<editor-canvas>` | critical — part 2 |
71
+ | `<editor-shell>` with no `<editor-toolbar>` | warning — part 3 |
72
+ | `<editor-shell>` with no `<editor-statusbar>` | warning — part 6 |
73
+ | `<editor-sidebar>` present, no inner `<pane-ui>` | warning — part 8 (delegation contract violated) |
74
+ | `<editor-shell>` direct child is native `<header>`/`<footer>` | warning — part 9 (retired-legacy-shape leak) |
75
+ | `<editor-canvas>` with no `<editor-canvas-empty>` child | info — part 4 |
76
+
77
+ ## Opt-out contract
78
+
79
+ Same annotation mechanism as admin-shell: `<editor-shell
80
+ data-shell-opt-out="reason">` downgrades every finding on that shell to
81
+ info and prints the reason for reviewers.
82
+
83
+ ## Triage
84
+
85
+ - Canonical product surface (`apps/construct-canvas`, `apps/genui`'s
86
+ a2ui-editor) → fix mandatory.
87
+ - A narrow single-feature playground isolating one editor behavior → fix
88
+ optional; annotate the opt-out.
89
+ - Never point this audit at `packages/web-modules/editor/**/*.examples.html`
90
+ or `packages/web-components/components/*/*.html` (single-primitive
91
+ spotlights) — the script's `isShowcaseDemo`/showcase-path exclusion
92
+ already keeps those out of scope, same as admin-shell.
93
+ - **Don't nest `<editor-shell>` inside `<admin-shell>`** as page chrome —
94
+ `editor-shell.examples.html` documents them as sibling surfaces, not
95
+ nested; this audit doesn't mechanically flag the nesting mistake (a
96
+ cross-shell structural rule, not a within-shell anatomy gap), but a
97
+ reviewer seeing both tags in one file should treat it as a design smell.
98
+ - `:strict` is the CI/publish posture; keep warn-only while iterating
99
+ locally.
100
+
101
+ ## Legacy shapes — never re-authored
102
+
103
+ `editor-shell.yaml`'s own description lists the ADR-0024-retired legacy
104
+ data-attribute shapes (`<header>`, `<div data-editor-body>`, `<pane-ui
105
+ data-left|data-right>`, `<div data-canvas>`, `<footer>`, `<span
106
+ data-spacer>`) as silently unrecognized, not merely deprecated. Part 9
107
+ above catches the two structural container tags (`<header>`/`<footer>`)
108
+ mechanically; the finer-grained data-attribute forms are already covered
109
+ by `verify:no-legacy-shell-shapes` in `npm run check`.
@@ -37,6 +37,16 @@ import { chromium } from 'playwright';
37
37
  import { mkdir, writeFile, readFile } from 'node:fs/promises';
38
38
  import { existsSync } from 'node:fs';
39
39
  import { join } from 'node:path';
40
+ // gh#3616: the shared overflow predicate, vendored (not import()able:
41
+ // this script ships inside a published plugin package with no apps/ tree
42
+ // in its files allowlist, see this repo's scripts/build/
43
+ // overflow-detect-vendor.mjs for the derive-and-vendor build that keeps
44
+ // this copy provably identical to apps/genui/app/_shared/overflow-detect.js).
45
+ // Imported as an ordinary function and passed BY REFERENCE into
46
+ // elementHandle.evaluate() below, which stringifies it into the browser's
47
+ // page context, same mechanism scripts/qa/gen-review-decompose.mjs's own
48
+ // (non-vendored, import()-able) copy uses.
49
+ import { OVERFLOW_TAGS, detectOverflow } from './overflow-detect.generated.mjs';
40
50
 
41
51
  // The script lives inside the plugin but runs against the monorepo. All
42
52
  // monorepo paths are resolved from the working directory (the monorepo root),
@@ -259,7 +269,7 @@ for (const group of gallery.groups) {
259
269
  for (const engineKey of Object.keys(prompt.engines ?? {})) {
260
270
  const engineData = prompt.engines[engineKey];
261
271
  if (!engineData || engineData.dryRun) continue;
262
- allPrompts.push({ group: group.slug, prompt: prompt.slug, engineKey, engineData });
272
+ allPrompts.push({ group: group.slug, prompt: prompt.slug, label: prompt.label, engineKey, engineData });
263
273
  }
264
274
  }
265
275
  }
@@ -321,7 +331,7 @@ const results = [];
321
331
  let renderFailureCount = 0;
322
332
  let idx = 0;
323
333
 
324
- for (const { group, prompt: promptSlug, engineKey, engineData } of allPrompts) {
334
+ for (const { group, prompt: promptSlug, label, engineKey, engineData } of allPrompts) {
325
335
  idx++;
326
336
  const slug = `${group}-${promptSlug}-${engineKey}`;
327
337
  process.stdout.write(` [${idx}/${allPrompts.length}] ${slug.padEnd(50)} `);
@@ -331,7 +341,10 @@ for (const { group, prompt: promptSlug, engineKey, engineData } of allPrompts) {
331
341
  // Each has a .gallery-canvas-wrap containing a canvas-ui.
332
342
  // We identify by matching the group anchor + prompt h3 text.
333
343
 
334
- const promptLabel = promptSlug.replace(/-/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
344
+ // gh#3249: use the gallery's real prompt.label (what the h3 actually renders), not a
345
+ // title-cased guess derived from the slug -- the two diverge whenever the authored label
346
+ // isn't the slug's mechanical title-case ("search-filters" vs "Search with Filters").
347
+ const promptLabel = label;
335
348
 
336
349
  // Scroll to the prompt section and wait for canvas settle
337
350
  await page.evaluate(({ groupSlug, label }) => {
@@ -407,41 +420,21 @@ for (const { group, prompt: promptSlug, engineKey, engineData } of allPrompts) {
407
420
  // canvas. Runs only when the canvas rendered (renderFailure = false).
408
421
  // Results land in decomposed.json as `overflowElements` — a non-empty array
409
422
  // is treated as P1 in Phase 4 regardless of the Phase 3 structural score.
410
-
411
- const OVERFLOW_TAGS = new Set([
412
- 'text-ui', 'stat-ui', 'badge-ui', 'field-ui', 'button-ui',
413
- 'label', 'span', 'p', 'h1', 'h2', 'h3', 'h4',
414
- ]);
423
+ //
424
+ // gh#3616: the predicate itself (both the TEXT-truncation and LAYOUT-
425
+ // overflow checks, plus every documented exclusion: hscroll boundary,
426
+ // <svg> subtree) now lives in overflow-detect.generated.mjs (vendored
427
+ // from apps/genui/app/_shared/overflow-detect.js), passed straight into
428
+ // elementHandle.evaluate() below rather than re-implemented here, so
429
+ // this plugin sweep and the in-repo QA script/live artifact pane provably
430
+ // run the same code. `canvasWrapEl` is the exact same `.gallery-canvas-
431
+ // wrap` element the screenshot/render-failure check above already
432
+ // resolved, so there's no need to re-locate it by groupSlug/label.
415
433
 
416
434
  let overflowElements = [];
417
435
 
418
436
  if (!renderFailure) {
419
- overflowElements = await page.evaluate(({ groupSlug, label, overflowTags }) => {
420
- const section = document.getElementById(`group-${groupSlug}`);
421
- if (!section) return [];
422
- const h3s = [...section.querySelectorAll('.gallery-prompt-heading')];
423
- const h3 = h3s.find(h => h.textContent.trim().toLowerCase() === label.toLowerCase());
424
- const wrap = h3?.closest('.gallery-prompt')?.querySelector('.gallery-canvas-wrap');
425
- if (!wrap) return [];
426
-
427
- const found = [];
428
- function walk(el) {
429
- const tag = el.tagName?.toLowerCase() ?? '';
430
- if (overflowTags.includes(tag)) {
431
- const style = getComputedStyle(el);
432
- const hasHidden = style.overflow === 'hidden' || style.overflowX === 'hidden';
433
- if (hasHidden && el.scrollWidth > el.clientWidth + 2) {
434
- found.push({ tag, clippedWidth: true });
435
- }
436
- if (hasHidden && el.scrollHeight > el.clientHeight + 2) {
437
- found.push({ tag, clippedHeight: true });
438
- }
439
- }
440
- for (const child of el.children) walk(child);
441
- }
442
- walk(wrap);
443
- return found;
444
- }, { groupSlug: group, label: promptLabel, overflowTags: [...OVERFLOW_TAGS] });
437
+ overflowElements = await canvasWrapEl.evaluate(detectOverflow, OVERFLOW_TAGS);
445
438
  }
446
439
 
447
440
  // ── Primitive lookup + sanitize ──────────────────────────────────────────
@@ -0,0 +1,101 @@
1
+ // GENERATED, do not hand-edit. Source: apps/genui/app/_shared/overflow-detect.js
2
+ // Rebuild: node scripts/build/overflow-detect-vendor.mjs
3
+ // Freshness gate: node scripts/build/overflow-detect-vendor.mjs --verify (npm run check:overflow-detect-vendor-fresh)
4
+ //
5
+ // gh#3616: vendored so the gen-ui-review plugin's own decompose script (a
6
+ // published package with no apps/ tree in its files allowlist) runs the
7
+ // EXACT SAME overflow/clip predicate as the in-repo QA script and the live
8
+ // artifact pane, instead of a hand-kept inline copy that can silently drift
9
+ // (the gap gh#3616 closed, see that ticket for the pre-fix divergence).
10
+
11
+ /**
12
+ * Tags the text-truncation check inspects for their own `overflow:hidden`
13
+ * clipping a2ui-root/canvas-ui render. Every other tag is still walked for
14
+ * the layout-overflow check below, which applies to any tag.
15
+ */
16
+ export const OVERFLOW_TAGS = [
17
+ 'text-ui', 'stat-ui', 'badge-ui', 'field-ui', 'button-ui',
18
+ 'label', 'span', 'p', 'h1', 'h2', 'h3', 'h4',
19
+ ];
20
+
21
+ /**
22
+ * Walks a rendered container and reports overflow/clip findings.
23
+ *
24
+ * Two independent clipping mechanisms (gh#3248):
25
+ * - TEXT truncation: an element's own `overflow:hidden` clips its own
26
+ * scrollWidth/scrollHeight (e.g. a Text node with no line-clamp room).
27
+ * Checked only for `overflowTags` elements.
28
+ * - LAYOUT overflow: an element's whole box lays out past the
29
+ * container's edge, whether or not anything in its ancestor chain has
30
+ * `overflow:hidden` (e.g. two sibling `[stretch]` buttons in a Row
31
+ * each claiming full row width, one landing off-canvas). Checked for
32
+ * every visible tag, horizontal only. Vertical overflow is never
33
+ * flagged since the container is expected to scroll vertically (the
34
+ * gallery's own `.gallery-canvas-wrap` is deliberately
35
+ * `overflow-y: auto`), and a container never scrolls sideways so a
36
+ * right-edge slice is always wrong.
37
+ *
38
+ * Both checks skip content inside a genuine horizontal-scroll boundary
39
+ * (table-ui's own scroll wrapper, swiper-ui, tabs-ui's overflow-x strip):
40
+ * content past the visible edge there is intentional, scrolled-to
41
+ * content, not a layout bug. Also skipped: content inside an `<svg>`
42
+ * subtree (icon-ui's glyph markup, chart-ui's rendered marks), since SVG
43
+ * coordinate-space geometry doesn't map onto DOM layout-box geometry the
44
+ * same way, confirmed via a live false positive on chart-ui's own
45
+ * sparkline. `<foreignObject>` re-enters normal HTML flow content inside
46
+ * that `<svg>` subtree (SVG spec), so the svg-exclusion resets for its
47
+ * own children rather than staying latched: a real HTML element nested
48
+ * there gets checked again.
49
+ *
50
+ * `container` itself is excluded from the horizontal-scroll-boundary test:
51
+ * per the CSS Overflow spec, pairing `overflow-y: auto` with the default
52
+ * `overflow-x: visible` computes overflow-x to `auto` too. Treating that
53
+ * as a real scroll boundary would mark every one of the container's own
54
+ * children as inside-hscroll and silently disable the layout-overflow
55
+ * check for the entire canvas, the opposite of what it exists to catch.
56
+ *
57
+ * @param {Element} container the rendered surface's outer boundary
58
+ * (gen-review-decompose.mjs's `.gallery-canvas-wrap`; the live artifact
59
+ * pane's own equivalent container).
60
+ * @param {string[]} overflowTags see OVERFLOW_TAGS; always pass this
61
+ * explicitly (see the module-doc note on why there's no default value).
62
+ * @returns {Array<{tag: string, clippedWidth?: true, clippedHeight?: true, pushedPastContainer?: true}>}
63
+ */
64
+ export function detectOverflow(container, overflowTags) {
65
+ const found = [];
66
+ const containerRect = container.getBoundingClientRect();
67
+ const TOLERANCE = 2; // matches the scrollWidth/clientWidth +2 slack below
68
+
69
+ function walk(el, insideHScroll, isContainer, insideSvg) {
70
+ const tag = el.tagName?.toLowerCase() ?? '';
71
+ const style = getComputedStyle(el);
72
+
73
+ if (overflowTags.includes(tag)) {
74
+ const hasHidden = style.overflow === 'hidden' || style.overflowX === 'hidden';
75
+ if (hasHidden && el.scrollWidth > el.clientWidth + TOLERANCE) {
76
+ found.push({ tag, clippedWidth: true });
77
+ }
78
+ if (hasHidden && el.scrollHeight > el.clientHeight + TOLERANCE) {
79
+ found.push({ tag, clippedHeight: true });
80
+ }
81
+ }
82
+
83
+ if (!insideHScroll && !insideSvg) {
84
+ const rect = el.getBoundingClientRect();
85
+ if (rect.width > 0 && rect.height > 0) {
86
+ if (rect.right > containerRect.right + TOLERANCE || rect.left < containerRect.left - TOLERANCE) {
87
+ found.push({ tag, pushedPastContainer: true });
88
+ }
89
+ }
90
+ }
91
+
92
+ const isHScrollRoot = !isContainer
93
+ && (style.overflowX === 'auto' || style.overflowX === 'scroll')
94
+ && el.scrollWidth > el.clientWidth + TOLERANCE;
95
+ const nowInsideSvg = tag === 'foreignobject' ? false : (insideSvg || tag === 'svg');
96
+ for (const child of el.children) walk(child, insideHScroll || isHScrollRoot, false, nowInsideSvg);
97
+ }
98
+
99
+ walk(container, false, true, false);
100
+ return found;
101
+ }