@adia-ai/adia-ui-forge 0.8.59 → 0.8.61
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.
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/CHANGELOG.md +68 -0
- package/package.json +1 -1
- package/plugin.yaml +1 -1
- package/references/contracts/a2ui-mcp-surface.md +1 -1
- package/scripts/lint-rules.generated.mjs +2418 -359
- package/skills/a2ui-maintenance/references/eval-diagnostics.md +2 -2
- package/skills/a2ui-maintenance/references/pipeline-overview.md +26 -0
- package/skills/a2ui-maintenance/references/strategy-engines.md +12 -0
- package/skills/demo-audit/references/admin-shell-anatomy.md +18 -12
- package/skills/gen-ui-review/scripts/gen-review-decompose.mjs +27 -34
- package/skills/gen-ui-review/scripts/overflow-detect.generated.mjs +101 -0
- package/skills/package-release/references/cut-procedure.md +49 -5
- package/skills/package-release/references/recovery-paths.md +20 -0
- package/skills/package-release/scripts/bump.mjs +36 -6
- package/skills/package-release/scripts/dispatch-publish.mjs +8 -0
- package/skills/package-release/scripts/gate-roster.mjs +4 -4
- package/skills/package-release/scripts/release-pack.mjs +485 -68
- package/skills/package-release/scripts/stale-copy-warning.mjs +215 -0
- package/skills/package-release/scripts/tag-lockstep.mjs +8 -0
- package/skills/primitive-authoring/references/INDEX.md +1 -0
- package/skills/primitive-authoring/references/examples-structure.md +52 -0
- package/skills/primitive-authoring/references/shell-patterns.md +12 -11
- package/skills/primitive-authoring/references/token-contract.md +2 -2
- package/skills/primitive-authoring/references/yaml-contract.md +106 -9
|
@@ -46,7 +46,7 @@ fluctuate).
|
|
|
46
46
|
## Phase 2 — fixes per bucket
|
|
47
47
|
|
|
48
48
|
- **A. Holdout alignment** — map each intent's `expected_chunk` to the actual
|
|
49
|
-
top-1; update `packages/gen-ui/engine/corpus/
|
|
49
|
+
top-1; update `packages/gen-ui/engine/evals/corpus/holdout-compose-from-chunks.jsonl`.
|
|
50
50
|
- **E. Measurement traps** — PascalCase→kebab (`AgentTrace` → `agent-trace`,
|
|
51
51
|
not `agenttrace`); substring false positives (`pane` vs `panel`,
|
|
52
52
|
`textarea-ui` contains `text-ui` — word boundaries); case sensitivity (`/i`).
|
|
@@ -82,7 +82,7 @@ the SKILL.md floors hold.
|
|
|
82
82
|
The two `check:*-eval-regression` scripts own the floor numbers — read the source
|
|
83
83
|
before quoting a number elsewhere; SKILL.md only mirrors them and can drift (it
|
|
84
84
|
once silently regressed to `cov≥40` before the mechanical gate existed). Zettel's
|
|
85
|
-
floors are a committed file, `evals/health/zettel-floor.json` (gh#1391)
|
|
85
|
+
floors are a committed file, `packages/gen-ui/engine/evals/health/zettel-floor.json` (gh#1391);
|
|
86
86
|
`scripts/release/check-zettel-eval-regression.mjs` loads it at runtime and refuses
|
|
87
87
|
to run without it, so re-baselining is a JSON diff, not a source edit. Free-form's
|
|
88
88
|
floors are still `ALERT_FLOOR`/`HARD_FLOOR` constants in
|
|
@@ -193,6 +193,32 @@ for any constant or decision lives in git and PR descriptions
|
|
|
193
193
|
SoT); ADR-0097 rules a fourth, `traits`, but that part is decided-not-
|
|
194
194
|
yet-shipped (gh#2513) — see `primitive-authoring/references/
|
|
195
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`, lowered to 4000 (gh#3516 slice
|
|
206
|
+
3, conductor ruling 2026-09-08) after an n=5 measurement at 10000
|
|
207
|
+
showed no measurable quality gain with real cost. `auto` inherits
|
|
208
|
+
through the free-form picker call the moment it escalates that far -
|
|
209
|
+
`monolithic-thinking` itself is never reachable via `auto`'s own
|
|
210
|
+
escalation ladder. Every other LLM call site (`generate-pro.js`'s four
|
|
211
|
+
branches, zettel's locator/modifier/synthesizer, the shared
|
|
212
|
+
`validate-and-repair.js` repair loop) stays thinking-off deliberately,
|
|
213
|
+
a narrow first pass pending real eval numbers - do not assume a new
|
|
214
|
+
engine inherits thinking by proximity to one that has it.
|
|
215
|
+
`StubLLMAdapter` accepts and records `thinking`/`thinkingBudget` on
|
|
216
|
+
its own `calls` log for strategy-level test assertions.
|
|
217
|
+
`eval-diff.mjs` gained `--mode` (`instant`/`pro`/`thinking`), scoped
|
|
218
|
+
to `--engine mcp` only, so `--engine mcp --mode thinking` exercises
|
|
219
|
+
the opted-in path independently of the router's own default. See
|
|
220
|
+
`docs/ops/lld/lld-0033-strategy-thinking-opt-in.md` for the full
|
|
221
|
+
decision record and the conductor's D1-D5 rulings.
|
|
196
222
|
|
|
197
223
|
## Test + run commands (all verified in root package.json)
|
|
198
224
|
|
|
@@ -86,6 +86,18 @@ shows the results); `--strict` enforcement is deferred to TKT-0010
|
|
|
86
86
|
(27% of the corpus currently fails, mostly a schema-generation gap
|
|
87
87
|
around `data-*`/`span` attributes rather than corpus-content defects).
|
|
88
88
|
|
|
89
|
+
## Provider thinking opt-in kill switch (gh#3516, LLD-3516 slice 1)
|
|
90
|
+
|
|
91
|
+
`monolithic-thinking` and `free-form-composer` opt into provider thinking at
|
|
92
|
+
a fixed strategy-level budget (`THINKING_BUDGET_MONOLITHIC_THINKING`,
|
|
93
|
+
`THINKING_BUDGET_FREE_FORM`, both 4000 as of gh#3516 slice 3) via the shared
|
|
94
|
+
`compose/shared/thinking-opt-in.js` helper. `ADIA_THINKING_OPT_IN=0` drops
|
|
95
|
+
the `thinking`/`thinkingBudget` fields at both call sites entirely (not
|
|
96
|
+
`thinking: false`, the adapter never sees the keys), giving the
|
|
97
|
+
thinking-off baseline a runtime lever instead of a code revert. Unset (or
|
|
98
|
+
`"1"`) keeps current behavior. This is an operations knob, not a caller
|
|
99
|
+
option: no consumer threads it through `generateUI()`.
|
|
100
|
+
|
|
89
101
|
## Pitfalls
|
|
90
102
|
|
|
91
103
|
- **`STRONG_MATCH_THRESHOLD` was raised 22 → 40 post-incident.** Lowering it
|
|
@@ -17,7 +17,7 @@ the live admin-dashboard example.
|
|
|
17
17
|
`<select-ui avatar="…" value="…" variant="ghost">` context switcher —
|
|
18
18
|
NOT `<menu-ui>` (legacy pattern) — ★ commonly mis-implemented
|
|
19
19
|
4. Sidebar nav wrap around `<nav-ui>` — `<section>` (per the examples),
|
|
20
|
-
`<section-ui>` (card-style chrome), or `<
|
|
20
|
+
`<section-ui>` (card-style chrome), or `<page-scroll>` all accepted;
|
|
21
21
|
a bare `<nav-ui>` direct child overflows long lists
|
|
22
22
|
5. Sidebar `<admin-statusbar slot="footer">` with `<select-ui avatar="…">`
|
|
23
23
|
(user menu) — ★ commonly missing
|
|
@@ -28,17 +28,23 @@ the live admin-dashboard example.
|
|
|
28
28
|
★ spacer + actions commonly missing
|
|
29
29
|
8. `[data-actions]` contains `<popover-ui>` + `<theme-panel slot="content">`
|
|
30
30
|
(there is no `<theme-picker-ui>`)
|
|
31
|
-
9. `<
|
|
32
|
-
`<router-ui>` (or `<
|
|
33
|
-
**[
|
|
34
|
-
rename to `page-scroll` (both modes carried over);
|
|
35
|
-
|
|
31
|
+
9. `<page-scroll>` wrapping optional `<aside data-subnav hidden>` +
|
|
32
|
+
`<router-ui>` (or `<page-ui band>` directly for non-routed)
|
|
33
|
+
**[deleted, ADR-0098 / gh#3745]** `admin-scroll` was a wholesale
|
|
34
|
+
rename to `page-scroll` (both modes carried over); its one-release
|
|
35
|
+
deprecation window has closed and the module is deleted (gh#3745).
|
|
36
|
+
The `admin-page` family (`admin-page`/`admin-page-header`/
|
|
37
|
+
`admin-page-body`) is deleted outright per ADR-0098, no compat alias.
|
|
38
|
+
`page-scroll` wrapping `page-ui[band]` is the sole successor for the
|
|
39
|
+
routed and non-routed case alike.
|
|
36
40
|
10. `<admin-page>` with `<admin-page-header>` + `<admin-page-body>`
|
|
37
|
-
**[
|
|
38
|
-
|
|
39
|
-
is now the one canonical page-chrome
|
|
40
|
-
|
|
41
|
-
|
|
41
|
+
**[deleted, ADR-0098]** The `admin-page` family is deleted outright,
|
|
42
|
+
no compat alias and no open deprecation window, in favor of
|
|
43
|
+
`page-ui[band]`; `page-ui` is now the one canonical page-chrome
|
|
44
|
+
primitive (item 9 above shows the live composition: `page-scroll`
|
|
45
|
+
wrapping `page-ui[band]`). This item is kept only so this anatomy
|
|
46
|
+
still recognizes the retired shape when auditing legacy surfaces
|
|
47
|
+
that predate the migration; never author it in new surfaces.
|
|
42
48
|
11. `<admin-statusbar>` at content footer (version strip) — ★ commonly missing
|
|
43
49
|
12. Second `<admin-sidebar slot="trailing">` (inspector rail, hidden by
|
|
44
50
|
default) — strongly recommended
|
|
@@ -64,7 +70,7 @@ the live admin-dashboard example.
|
|
|
64
70
|
| content topbar missing `[data-spacer]` / `[data-actions]` | warning — part 7 |
|
|
65
71
|
| content missing trailing `<admin-statusbar>` | warning — part 11 |
|
|
66
72
|
| sidebar topbar contains only plain text | warning — part 3 context switcher |
|
|
67
|
-
| `<
|
|
73
|
+
| `<page-scroll>` missing around `<page-ui band>` | critical, part 9 (the enforcing script `audit-shell-composition.mjs` tiers this critical: without the scroll+chrome wrapper the content renders flush with the topbar, no margins). **[deleted, ADR-0098 / gh#3745]** `admin-scroll` and the `admin-page` family are both deleted outright, no compat alias; the successor is `page-scroll` wrapping `page-ui[band]` |
|
|
68
74
|
|
|
69
75
|
## Opt-out contract
|
|
70
76
|
|
|
@@ -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
|
-
|
|
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
|
-
|
|
412
|
-
|
|
413
|
-
|
|
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
|
|
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
|
+
}
|
|
@@ -105,6 +105,24 @@ When only source *hashes* move and chunk content does not, `check:embeddings-fre
|
|
|
105
105
|
|
|
106
106
|
### 3.1 The full roster — every gate runs; a subset = pre-flight failure
|
|
107
107
|
|
|
108
|
+
**Precondition — tsc build for llm, agent, persona (gh#3342).** Gate 4
|
|
109
|
+
(`test:unit:serial`) runs each package's root `*.test.js` files against its
|
|
110
|
+
BUILT output, not its `src/*.ts`: `persona.test.js` imports `./index.js`
|
|
111
|
+
directly, `agent.test.js`'s own docstring says "run against the BUILT
|
|
112
|
+
output ... `npm run build -w @adia-ai/agent` first", and `llm/core` carries
|
|
113
|
+
a dedicated `dist-check.test.js` that asserts the emitted artifacts exist
|
|
114
|
+
and explicitly does not build them itself. All three packages' emitted
|
|
115
|
+
`.js`/`.d.ts` are gitignored, so a fresh cut clone has none of them until
|
|
116
|
+
something builds them — unlike §3.0's regen outputs, this is a plain build
|
|
117
|
+
artifact, not a content-conditional regen, so `release-pack.mjs`'s
|
|
118
|
+
`step3PreFlight()` now runs `npm run build -w @adia-ai/llm -w @adia-ai/agent
|
|
119
|
+
-w @adia-ai/persona` unconditionally, every cut, immediately before gate 4
|
|
120
|
+
(mechanized fix — a manual cut should run the same command first). The
|
|
121
|
+
v0.8.59 cut hit this: §3.0's `npm run build -w @adia-ai/llm` line only
|
|
122
|
+
fires when its source-content trigger list matches, and never named
|
|
123
|
+
agent/persona at all, so a cut with no matching trigger reached gate 4 with
|
|
124
|
+
stale or absent dist and failed on it.
|
|
125
|
+
|
|
108
126
|
**Execution model (gh#2006): three phases, not one serial walk.** `step3PreFlight()` runs gate 4 solo first (see its own note below), then gates 16 → 27 → 28 strictly in order (the eval-health write-then-read dependency — gate 28 reads whichever `evals/mcp/runs/` directory sorts lexically LAST, so nothing else may write there between 27 and 28), concurrently with a bounded pool running every other gate at once (`PREFLIGHT_CONCURRENCY`, default 4 — override for a dedicated/idle host). Every gate still resolves the same command, still fails the whole pre-flight on a red result, and still reports its own number — only the WALL-CLOCK schedule changed, never the roster below or its numbering. `--dry` previews stay the original flat serial walk unchanged.
|
|
109
127
|
|
|
110
128
|
```bash
|
|
@@ -176,6 +194,11 @@ node scripts/release/assemble-changelog-fragments.mjs # writes, deletes c
|
|
|
176
194
|
node scripts/release/assemble-changelog-fragments.mjs --verify # must print PASS afterward
|
|
177
195
|
```
|
|
178
196
|
|
|
197
|
+
Since gh#3638 a fragment may also be named `changes/gh-<issue>.md` (the form a builder can use
|
|
198
|
+
before a PR number exists). Assembly handles both: an issue-named fragment is credited to the PR
|
|
199
|
+
that merged it, read off that merge commit's own `(#<pr>)` subject, and falls back to the issue
|
|
200
|
+
number when no such commit is found. Nothing changes in this step's commands.
|
|
201
|
+
|
|
179
202
|
Routing rule (`scripts/release/assemble-changelog-fragments.mjs`'s own header, full detail
|
|
180
203
|
there): a fragment's first line is `- <kind>: <sentence>` (kind in fix|feature|chore|docs,
|
|
181
204
|
unchanged from `changelog_fragments.py`'s schema) or, this repo's own addition, `- <kind>
|
|
@@ -327,11 +350,20 @@ Drift here means a regen output was left out of the allowlist — stage it and r
|
|
|
327
350
|
fails the cut if any tracked file is still modified-in-the-worktree —
|
|
328
351
|
proof the allowlist covered everything bump.mjs / cut-hygiene touched this
|
|
329
352
|
cut, not just what §Step 5.5's three named freshness gates happen to check.
|
|
330
|
-
|
|
331
|
-
gh#
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
353
|
+
The allowlist itself went stale five times (gh#1198, gh#1899, gh#1954,
|
|
354
|
+
gh#2473, gh#3342 — most recently `icons-cdn.js`'s PACKAGE_VERSION pin), so
|
|
355
|
+
this guard is generic rather than another named file — it stays the
|
|
356
|
+
fallback for anything below. **The PINNED_REFS-covered subset of the
|
|
357
|
+
allowlist can no longer drift this way at all** (gh#3361): `release-pack.mjs`
|
|
358
|
+
now derives those specific entries straight from `bump.mjs`'s own
|
|
359
|
+
`PINNED_REFS`/`REPO_PINNED_REFS` tables (`pinnedRefFiles()`) instead of
|
|
360
|
+
hand-listing the same paths a second time — a new pinned file in `bump.mjs`
|
|
361
|
+
is automatically a new allowlist entry, no second edit needed. Everything
|
|
362
|
+
NOT PINNED_REFS-covered (roster `package.json`/`CHANGELOG.md`, and the Step
|
|
363
|
+
4d.5-4d.8 derived catalog/manifest/dist outputs) is still hand-listed and
|
|
364
|
+
still relies on this guard as the safety net. A manual cut should run the
|
|
365
|
+
equivalent check by hand: `git status --porcelain` after staging must be
|
|
366
|
+
empty of `M`/`D` lines.
|
|
335
367
|
|
|
336
368
|
## §Step 5.7 — Release PR: push the branch, merge, re-baseline
|
|
337
369
|
|
|
@@ -454,6 +486,18 @@ npm view @adia-ai/web-components dist-tags.latest # must equal X.Y.Z
|
|
|
454
486
|
|
|
455
487
|
Zero workflows fired after a tag push → [`recovery-paths.md`](recovery-paths.md) §Scenario 7.
|
|
456
488
|
|
|
489
|
+
**npm's async staged-publish path (gh#3342):** a large tarball can take up to
|
|
490
|
+
~25 minutes to become visible on `npm view` after npm accepts it —
|
|
491
|
+
`release-pack.mjs`'s own Step 9 poll accounts for this (`REGISTRY_POLL_MINUTES`,
|
|
492
|
+
default 30, up from the 10-minute window the v0.8.59 cut exceeded with every
|
|
493
|
+
publish run green). A re-dispatch attempted while a package is in that state
|
|
494
|
+
fails its `npm publish` step with `npm error code E409` ("Cannot publish over
|
|
495
|
+
previously staged version") — that is the staged-not-lost signal, never a real
|
|
496
|
+
failure; release-pack's Step 9 detects it (the failing run's own log) and polls
|
|
497
|
+
longer instead of hard-failing. See [`recovery-paths.md`](recovery-paths.md)
|
|
498
|
+
§Scenario 9 for manual recovery, including resuming at Step 10 only
|
|
499
|
+
(`--from-step10`) without re-running the pre-flight or re-tagging.
|
|
500
|
+
|
|
457
501
|
## §Step 10 — GH releases + site deploy dispatch
|
|
458
502
|
|
|
459
503
|
```bash
|
|
@@ -97,6 +97,26 @@ For a batch, preserve npm-latest ordering (`--after <prev>`). Verify against the
|
|
|
97
97
|
|
|
98
98
|
---
|
|
99
99
|
|
|
100
|
+
## §Scenario 9 — Step 9 registry poll times out with every publish run green (npm E409)
|
|
101
|
+
|
|
102
|
+
**Shape:** `release-pack.mjs --mode handoff` exits 1 at Step 9, but `gh run list --workflow=publish-<pkg>.yml` shows every run `success` and `npm view <scope>/<pkg> version` eventually returns the target version — it just took longer than the poll waited. A large tarball can take up to ~25 minutes to become visible on `npm view` after npm accepts it (asynchronous staged publish); the v0.8.59 cut's poll window was 10 minutes.
|
|
103
|
+
|
|
104
|
+
**A re-dispatch made while a package is in this state fails its own `npm publish` step with `npm error code E409` / "Cannot publish over previously staged version".** That error is not a real failure — npm rejected the duplicate publish precisely because the real one already landed server-side. It is the staged-not-lost signal, never grounds to re-dispatch a third time.
|
|
105
|
+
|
|
106
|
+
**Resolution:**
|
|
107
|
+
|
|
108
|
+
- `release-pack.mjs`'s Step 9 (gh#3342) already extends its own poll to `REGISTRY_POLL_MINUTES` (default 30, override via env) and, for any package still stale after that, checks its latest `publish-<pkg>.yml` run for the E409 signature before deciding — an E409-confirmed package gets one more extended, isolated poll instead of an immediate hard-fail. Nothing to do by hand in that case; let it finish.
|
|
109
|
+
- If Step 9 already exited 1 and you've confirmed by hand (registry + `gh run list`) that every package is actually published, don't re-run the full handoff — it would re-run the ~15min pre-flight (Step 3) and re-tag at HEAD (Step 6, wrong if anything merged since the original tag). Resume from Step 10 only:
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
node "<plugin-root>/skills/package-release/scripts/release-pack.mjs" \
|
|
113
|
+
--mode handoff --version X.Y.Z --date YYYY-MM-DD --previous-version X.Y.Z-1 \
|
|
114
|
+
--gh-notes-file <path> --from-step10
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
`--from-step10` skips Steps 1/3/4/5/6/7/8/9 entirely and runs only Step 10 (GH releases + site deploy) — the exact shape a hand-rolled one-off script (`/tmp/cut-0859-step10.sh`, never checked in) worked around live on the v0.8.59 cut. Before jumping, it hard-verifies the umbrella tag AND every per-package tag (`vX.Y.Z`, `<pkg>-vX.Y.Z`) already exist on origin and resolve to HEAD — without that check, `gh release create` on a tag that was never actually made would silently mint a NEW lightweight tag at whatever HEAD happens to be, attaching the release notes to the wrong commit. A missing or mismatched tag refuses the resume outright, naming which tag and why (either Step 8 never pushed it, or HEAD moved since); re-tag/push for real, or checkout the tagged commit, before retrying.
|
|
118
|
+
- **Never** re-dispatch a package a third time once it shows E409 — a third attempt only 409s again. Wait for the registry; it always converges.
|
|
119
|
+
|
|
100
120
|
## §Decision flowchart
|
|
101
121
|
|
|
102
122
|
```text
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
import fs from 'node:fs';
|
|
23
23
|
import path from 'node:path';
|
|
24
24
|
import process from 'node:process';
|
|
25
|
+
import { fileURLToPath } from 'node:url';
|
|
25
26
|
import { PACKAGE_ROSTER } from './package-paths.mjs';
|
|
26
27
|
|
|
27
28
|
// Single-sourced roster (H3, package-paths.mjs). Each plugin also carries a
|
|
@@ -60,7 +61,14 @@ const SIBLING_MANIFESTS = Object.fromEntries(
|
|
|
60
61
|
// `gen-ui-mcp`) was narrowed back by the shim-deletion follow-up PR, matching
|
|
61
62
|
// lockstep-checks.mjs's MCP_PIN_RE — a retired name in the pin must now fail
|
|
62
63
|
// loudly instead of being bumped along.
|
|
63
|
-
|
|
64
|
+
// gh#3361: exported so release-pack.mjs's Step 5 can derive its
|
|
65
|
+
// PINNED_REFS-covered allowlist entries from this table directly instead of
|
|
66
|
+
// hand-duplicating the file list — the drift class this table itself was
|
|
67
|
+
// born to fix (gh#1198/1899/1954/2473/3342, all "bump.mjs moved a pin,
|
|
68
|
+
// release-pack.mjs's own list didn't know about it") can no longer recur
|
|
69
|
+
// for anything PINNED_REFS already covers: a new entry here is now a new
|
|
70
|
+
// entry there, structurally, not two edits.
|
|
71
|
+
export const A2UI_MCP_PIN = {
|
|
64
72
|
label: 'generation-MCP pin (@adia-ai/mcp)',
|
|
65
73
|
// `@adia-ai/mcp@<from>` (not a prefix of a longer version) → `@<to>`
|
|
66
74
|
// Escape EVERY regex metacharacter in `from`, not just dots: a prerelease or
|
|
@@ -71,7 +79,7 @@ const A2UI_MCP_PIN = {
|
|
|
71
79
|
new RegExp(`(@adia-ai/mcp@)${from.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(?![\\d.])`),
|
|
72
80
|
replace: (to) => `$1${to}`,
|
|
73
81
|
};
|
|
74
|
-
const PINNED_REFS = {
|
|
82
|
+
export const PINNED_REFS = {
|
|
75
83
|
'packages/plugins/adia-ui-factory': [
|
|
76
84
|
{ ...A2UI_MCP_PIN, file: '.mcp.json' },
|
|
77
85
|
{ ...A2UI_MCP_PIN, file: 'README.md', label: 'generation-MCP pin (README prose)' },
|
|
@@ -113,7 +121,7 @@ const PINNED_REFS = {
|
|
|
113
121
|
// once and the next cut fails the gate — so the bump moves it, like the
|
|
114
122
|
// .mcp.json pin (H2 follow-through, 2026-07-19: found 0.7.13 claimed while
|
|
115
123
|
// 0.8.7 was live — a full minor of README drift shipped to npm).
|
|
116
|
-
const REPO_PINNED_REFS = [
|
|
124
|
+
export const REPO_PINNED_REFS = [
|
|
117
125
|
{
|
|
118
126
|
file: 'README.md',
|
|
119
127
|
label: '"Current version" claim',
|
|
@@ -407,6 +415,28 @@ function selftest() {
|
|
|
407
415
|
console.log('selftest OK');
|
|
408
416
|
}
|
|
409
417
|
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
else main()
|
|
418
|
+
// gh#3361: this module is now imported for its PINNED_REFS/REPO_PINNED_REFS
|
|
419
|
+
// exports (release-pack.mjs's Step 5), not only run as a CLI — a bare
|
|
420
|
+
// `topArgv[0] === 'selftest'` / `else main()` at module scope would fire
|
|
421
|
+
// `main()` (which calls `parseArgs`, which prints "error: --from/--to
|
|
422
|
+
// required" and `process.exit(2)`s) the moment ANYTHING imports this file,
|
|
423
|
+
// killing the importer's own process. Realpath-safe (a naive
|
|
424
|
+
// `argv[1] === fileURLToPath(import.meta.url)` reads false through a
|
|
425
|
+
// symlink, same pitfall scripts/lib/is-entry-point.mjs documents) so this
|
|
426
|
+
// stays correct if bump.mjs is ever ELM'd behind one; inlined rather than
|
|
427
|
+
// importing that top-level helper, keeping this skill's own scripts/
|
|
428
|
+
// self-sufficient.
|
|
429
|
+
function isEntryPoint() {
|
|
430
|
+
if (!process.argv[1]) return false;
|
|
431
|
+
try {
|
|
432
|
+
return fs.realpathSync(fileURLToPath(import.meta.url)) === fs.realpathSync(path.resolve(process.argv[1]));
|
|
433
|
+
} catch {
|
|
434
|
+
return false;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
if (isEntryPoint()) {
|
|
439
|
+
const topArgv = process.argv.slice(2);
|
|
440
|
+
if (topArgv[0] === 'selftest') selftest();
|
|
441
|
+
else main();
|
|
442
|
+
}
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
import { execFileSync } from 'node:child_process';
|
|
21
21
|
import process from 'node:process';
|
|
22
22
|
import { assertMonorepoRoot } from './assert-monorepo-root.mjs';
|
|
23
|
+
import { warnIfStaleCopy } from './stale-copy-warning.mjs';
|
|
23
24
|
import { PACKAGE_ROSTER } from './package-paths.mjs';
|
|
24
25
|
|
|
25
26
|
// Instance data — fork-configurable. The npm scope the lockstep roster publishes under.
|
|
@@ -288,6 +289,13 @@ async function main() {
|
|
|
288
289
|
const args = parseArgs(process.argv.slice(2));
|
|
289
290
|
// Fail-fast guard: refuse to run npm/gh against a non-monorepo directory.
|
|
290
291
|
assertMonorepoRoot(process.cwd());
|
|
292
|
+
// Stale-plugin-cache self-check (ticket #3943 item 2), same primitive
|
|
293
|
+
// release-pack.mjs's own top-level check now calls.
|
|
294
|
+
warnIfStaleCopy(
|
|
295
|
+
import.meta.url,
|
|
296
|
+
process.cwd(),
|
|
297
|
+
'packages/plugins/adia-ui-forge/skills/package-release/scripts/dispatch-publish.mjs',
|
|
298
|
+
);
|
|
291
299
|
if (args.after) checkAfter(args.after, args.scope);
|
|
292
300
|
|
|
293
301
|
// Recovery mode — verify each publish workflow SUCCEEDED for its pushed tag
|
|
@@ -103,15 +103,15 @@ export const GATE_ROSTER = [
|
|
|
103
103
|
// on the handoff re-run, since the promoted heading was already on main).
|
|
104
104
|
{ n: 26, cmd: 'node scripts/release/check-dx-sweep-freshness.mjs', targetVersionArg: true, what: 'DX sweep fresh + not regression-RED' },
|
|
105
105
|
// gh#1135 (WS-4 SPEC REQ-06): a fresh free-form eval run feeds the health
|
|
106
|
-
// record gate 28 writes
|
|
106
|
+
// record gate 28 writes: without it, "latest" under packages/gen-ui/engine/evals/mcp/runs/ could
|
|
107
107
|
// be a stale leftover from an earlier ad-hoc run, not this cut's own state.
|
|
108
|
-
{ n: 27, cmd: 'npm run eval:diff -- --engine free-form', what: 'fresh free-form eval run (evals/health input)' },
|
|
109
|
-
// REQ-06: commits evals/health/<version>.json
|
|
108
|
+
{ n: 27, cmd: 'npm run eval:diff -- --engine free-form', what: 'fresh free-form eval run (packages/gen-ui/engine/evals/health input)' },
|
|
109
|
+
// REQ-06: commits packages/gen-ui/engine/evals/health/<version>.json; the version this record
|
|
110
110
|
// names is the CUT's target version from the start, never the
|
|
111
111
|
// pre/post-bump `hygieneVersion` gate 24 needs (a claim-validation
|
|
112
112
|
// concern this is not) — hence `targetVersionArg` rather than
|
|
113
113
|
// `versionArg`, a distinct flag release-pack.mjs's step3PreFlight reads.
|
|
114
|
-
{ n: 28, cmd: 'node scripts/release/write-eval-health.mjs', what: 'evals/health/<version>.json committed, AC-01/AC-02 run for real', targetVersionArg: true },
|
|
114
|
+
{ n: 28, cmd: 'node scripts/release/write-eval-health.mjs', what: 'packages/gen-ui/engine/evals/health/<version>.json committed, AC-01/AC-02 run for real', targetVersionArg: true },
|
|
115
115
|
// ADR-0048 (gh#1192) estate-split latch. Between P1 (mint @adia-ai/gen-ui)
|
|
116
116
|
// and P5 (shims + roster) the tree is correct in-repo but deliberately
|
|
117
117
|
// NOT publishable: the old package dirs are `private: true` stubs the
|