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

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
@@ -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`.
@@ -23,6 +23,7 @@ Two entry variants, converging at Step 5:
23
23
  | 1 | Re-baseline (branch check + status + log + fetch) | No |
24
24
  | 2 | Classify uncommitted files; stash strays | Stash only |
25
25
  | 3 | Pre-flight gates (+ harvest preamble if source content changed) | No |
26
+ | 4a-pre | Assemble `changes/<pr>.md` fragments into the right `[Unreleased]` (REQ-W11-06) | Yes |
26
27
  | 4 | (Variant B) Promote `[Unreleased]`; bump; lockfile | Yes |
27
28
  | 4f | Pre-tag coverage `--fix` — authoritative F-N1 matcher, pre-PR | CHANGELOGs |
28
29
  | 5 | Stage the release allowlist; commit on `release/vX.Y.Z` | Yes |
@@ -100,6 +101,8 @@ When only source *hashes* move and chunk content does not, `check:embeddings-fre
100
101
 
101
102
  **Regen output supersedes working-tree state.** These outputs land in the release commit unconditionally, even when the same paths are also dirty from a peer — the fresh regen is authoritative; divergent uncommitted work rebases on top afterwards.
102
103
 
104
+ **Staging a read-only report without cutting** (gh#3063): `node scripts/release/preflight-dry-run.mjs --version X.Y.Z` runs this same roster (`gate-roster.mjs` SoT) inside a throwaway `npm ci` clone and prints per-gate PASS/FAIL plus a tail-of-log on failure — no bump, no tag, no CHANGELOG promotion, nothing lands on the real repo. This mechanizes the ad-hoc procedure the 0.8.59 staging pre-flight hand-drove (gh#2870 comments 5526330460 + addendum); use it whenever a "how healthy is main right now" report is wanted ahead of an actual cut. `--dry-run` lists the roster with no clone/npm ci; `--keep` preserves the throwaway clone for inspection.
105
+
103
106
  ### 3.1 The full roster — every gate runs; a subset = pre-flight failure
104
107
 
105
108
  **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.
@@ -140,6 +143,8 @@ npm run check:harness-manifests-fresh # 32 Hermes/Pi plugin.yaml + __in
140
143
  npm run verify:patterns-index # 33 pattern-index.md (mcp + adia-ui-factory) vs corpus source
141
144
  node scripts/release/check-yaml-events-vs-runtime.mjs --strict --strict-details # 34 yaml events: blocks vs runtime dispatch — no phantom/missing events (gh#2829)
142
145
  node scripts/release/check-yaml-impl-coverage.mjs --strict # 35 yaml schema fields vs implementation coverage (gh#2829)
146
+ npm run check:treeshake # 36 single-import build matrix (esbuild+rollup) — byte budgets + marker-leak + CSS purity + whole-lib delta + docs grep-gate (gh#2912)
147
+ npm run check:lint-efficacy # 37 lint rule bank: seeded catch rate 100% + golden-set 0 error FPs + mutation hardening (gh#2911 — not in `npm run check`, ~100s over the <60s bar)
143
148
  ```
144
149
 
145
150
  **Gate 29 was the ADR-0048 latch; since P5 it is a permanent invariant.** Between P1 and P5 the repo was correct in-repo but deliberately **not publishable** (old-name stubs marked `private: true` that the roster still mapped, plus dependency edges onto workspace packages no cut published), and a cut in that window would have shipped broken packages that npm cannot unpublish. **P5 cleared it by landing the real thing** — the six stubs became publishable shims and `PACKAGE_ROSTER` gained the three remaining new names, at which point all 9 offending edges resolved and the gate went green on its own. No gate logic was changed.
@@ -157,10 +162,45 @@ Any red → route via [`gates-catalog.md`](gates-catalog.md); fix at the source,
157
162
 
158
163
  ## §Step 4 — (Variant B, or ANY variant with uncommitted `[Unreleased]` content) Promote, bump, lockfile
159
164
 
165
+ **4a-pre. Assemble `changes/<pr>.md` fragments (REQ-W11-06, gh#2931), BEFORE promotion.** This
166
+ repo's PRs land a `changes/<pr>.md` fragment instead of hand-editing a CHANGELOG directly —
167
+ `check-changelog-pr-gate.mjs`'s own PR-time gate now REQUIRES a fragment for a roster-package
168
+ change and refuses the direct edit outright (gh#3123; the root `CHANGELOG.md` was already
169
+ fragment-only in practice before that ticket). Those fragments accumulate
170
+ unreleased until something folds them into the right CHANGELOG's `[Unreleased]` section — that's
171
+ this step, and it must run before 4a promotes `[Unreleased]` to a versioned heading, or a
172
+ fragment folded in afterward would land under the WRONG (already-promoted) heading.
173
+
174
+ ```bash
175
+ node scripts/release/assemble-changelog-fragments.mjs # writes, deletes consumed fragments
176
+ node scripts/release/assemble-changelog-fragments.mjs --verify # must print PASS afterward
177
+ ```
178
+
179
+ Routing rule (`scripts/release/assemble-changelog-fragments.mjs`'s own header, full detail
180
+ there): a fragment's first line is `- <kind>: <sentence>` (kind in fix|feature|chore|docs,
181
+ unchanged from `changelog_fragments.py`'s schema) or, this repo's own addition, `- <kind>
182
+ (<package>): <sentence>` naming a `PACKAGE_ROSTER` (`package-paths.mjs`) entry. The
183
+ parenthetical-package form routes to that package's own `packages/<dir>/CHANGELOG.md` — the
184
+ norm for package-scoped fragments as of gh#3123, not a hypothetical; the plain form routes to
185
+ the repo-root `CHANGELOG.md`, whose own header scopes
186
+ it to exactly that shape of change ("tooling, CI, build scripts, cross-package work, docs").
187
+ `kind` maps to a Keep-a-Changelog subsection: `feature`→Added, `fix`→Fixed, `chore`→Changed,
188
+ `docs`→Docs — created under `## [Unreleased]` in that canonical order if the subsection doesn't
189
+ already exist, otherwise appended to the existing one.
190
+
191
+ **Deliberately NOT a pre-flight roster gate (§3.1).** Pending fragments are a NORMAL state
192
+ between PRs, not a defect — a `--verify`-shaped freshness gate added to the pre-cut roster (which
193
+ runs before this step, in Step 3) would fail on every cut that has any recent chore/fix/feature
194
+ PR queued, which is the common case. The `--verify` invocation above is a post-assembly
195
+ self-check (proves the assemble actually consumed everything it found), not a standing gate;
196
+ `gate-roster.mjs`'s count is unchanged by this ticket.
197
+
160
198
  **Run 4a whenever a hand-authored `## [Unreleased]` section is still sitting uncommitted, not only on a strict Variant B.** `release-pack.mjs --mode cut` (a peer's pre-staged content, not yet promoted) needs it exactly as much as `--mode from-scratch` does — the v0.8.4 near-miss was `--mode cut` skipping this step entirely because the doc (and the script) only associated promotion with "from scratch". Both modes now run it and both hard-fail before the bump if any roster package still carries non-empty `[Unreleased]` content afterward.
161
199
 
162
200
  **4a. Promote** `## [Unreleased]` → `## [vX.Y.Z] — YYYY-MM-DD` per package (`` `<plugin-root>/skills/package-release/scripts/promote-unreleased.mjs` ``); author fresh blocks for changed-but-unlogged packages; stub the pure ride-alongs (`` `<plugin-root>/skills/package-release/scripts/insert-stub.mjs` ``). Classification recipe + shapes: [`changelog-discipline.md`](changelog-discipline.md).
163
201
 
202
+ `release-pack.mjs` (both `--mode cut` and `--mode from-scratch`) now rejects any `--substantive-packages`/`--stub-packages` name whose entry in `scripts/package-paths.mjs`'s `PACKAGE_ROSTER` is unknown or `lockstep: false`, at parse time, before Step 1 runs (gh#2894) — the 0.8.58 cut passed `adia-plugins` (lockstep:false) in `--substantive-packages` and let `promote-unreleased.mjs` rewrite its `[Unreleased]` header to a version that package never ships, caught only at Step 5.6 after the full pre-flight had already run.
203
+
164
204
  **4b. Bump.** PATCH vs MINOR: **MINOR is reserved for API-surface breaks only** (removed/renamed prop, attribute, slot, event, token, or tag). Visible behavior changes, re-scalings, and opt-in features stay PATCH; a CHANGELOG bullet saying "(MINOR behavior change)" is prose, not a semver directive. Unqualified "bump version" = PATCH; don't round-trip to ask. `node "<plugin-root>/skills/package-release/scripts/bump.mjs" --from X.Y.Z-1 --to X.Y.Z`. On a MINOR cut, also bump the internal `@adia-ai/*` `^ranges` separately (bump.mjs touches `"version"` fields only) — and a MINOR cut owes a MIGRATION GUIDE section ([`migration-guide-authoring.md`](migration-guide-authoring.md)).
165
205
 
166
206
  **4c. Lockfile.** `npm install --package-lock-only --no-audit --no-fund` — must land in the release commit. The publish workflows open with `npm ci`, which hard-fails on a version/lockfile mismatch: a bump without the regenerated lockfile passes locally and breaks **every** publish at clean-install.
@@ -229,6 +269,7 @@ stub sections exist leaves it nothing to append to, and the gap resurfaces
229
269
  as F-N1 warns at the push boundary, costing a tag move:
230
270
 
231
271
  ```bash
272
+ node scripts/release/assemble-changelog-fragments.mjs # 4a-pre (idempotent — safe to re-run; no-op if already assembled)
232
273
  node "<plugin-root>/skills/package-release/scripts/insert-stub.mjs" \
233
274
  --version X.Y.Z --date YYYY-MM-DD --previous-version X.Y.Z-1 \
234
275
  --substantive "<one-line>" --xref "<anchor>" --packages <missing-stubs> # 4a-stub — FIRST, only the missing ones (hard-errors on existing sections)
@@ -88,6 +88,23 @@ const PINNED_REFS = {
88
88
  new RegExp(`("@adia-ai/[a-z0-9-]+":\\s*")${from.replace(/\./g, '\\.')}(?![\\d.])`, 'g'),
89
89
  replace: (to) => `$1${to}`,
90
90
  }],
91
+ // icons-cdn.js's hand-typed PACKAGE_VERSION literal (gh#3228's jsDelivr
92
+ // manifest-fallback URL pin) must move with web-components' OWN version
93
+ // every cut — unlike the file's other literal (PINNED_VERSION, which
94
+ // tracks @phosphor-icons/core's independent release cadence and is
95
+ // updated by hand only when Phosphor cuts), this one pins the very
96
+ // package it lives in. Left unmoved by bump.mjs through the 0.8.58 cut,
97
+ // it would leave MANIFEST_CDN_FALLBACK_URL naming a stale version with
98
+ // check:lockstep staying green — no coherence source watched it until
99
+ // lockstep-checks.mjs's invariant 9 (gh#3240) and this pin closed the
100
+ // loop together.
101
+ 'packages/web-components': [{
102
+ file: 'core/icons-cdn.js',
103
+ label: 'icons-cdn.js PACKAGE_VERSION literal (jsDelivr manifest fallback pin)',
104
+ pattern: (from) =>
105
+ new RegExp(`(const PACKAGE_VERSION = ')${from.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(?![\\d.])(')`),
106
+ replace: (to) => `$1${to}$2`,
107
+ }],
91
108
  };
92
109
 
93
110
  // Repo-level version references (path relative to REPO ROOT, not a package) —
@@ -274,10 +291,14 @@ function main() {
274
291
  // script never calls fs.writeFileSync by pointing at a path whose directory
275
292
  // doesn't exist; a latent write bug would throw ENOENT instead of passing.
276
293
  function selftest() {
294
+ // A fixture pkg name deliberately NOT in PINNED_REFS/SIBLING_MANIFESTS —
295
+ // this proof exercises bumpAll's core version-field transform in
296
+ // isolation, so it must not collide with (and pull in the file reads for)
297
+ // any real package's pin config.
277
298
  const fake = [{
278
- pkg: 'packages/web-components',
299
+ pkg: 'packages/fixture-pkg',
279
300
  path: '/nonexistent-fixture-dir/package.json',
280
- txt: '{\n "name": "@adia-ai/web-components",\n "version": "0.8.4"\n}\n',
301
+ txt: '{\n "name": "@adia-ai/fixture-pkg",\n "version": "0.8.4"\n}\n',
281
302
  version: '0.8.4',
282
303
  }];
283
304
  if (!validateAllAtFrom(fake, '0.8.4')) {
@@ -341,6 +362,24 @@ function selftest() {
341
362
  console.error('selftest FAIL: peer-pin pattern prefix-matched a longer version'); process.exit(1);
342
363
  }
343
364
 
365
+ // icons-cdn.js PACKAGE_VERSION literal (gh#3240, invariant 9) — must move
366
+ // with web-components' own version, and must not touch the file's OTHER
367
+ // literal-looking pin (PINNED_VERSION, @phosphor-icons/core's version,
368
+ // updated by hand on its own independent cadence).
369
+ const [iconsCdnCfg] = PINNED_REFS['packages/web-components'];
370
+ const iconsCdnBefore =
371
+ "const PINNED_VERSION = '2.1.1';\n\nconst PACKAGE_VERSION = '0.8.58';\n";
372
+ const iconsCdnAfter = bumpPinnedRef(iconsCdnBefore, iconsCdnCfg, '0.8.58', '0.8.59');
373
+ if (!iconsCdnAfter.includes("PACKAGE_VERSION = '0.8.59'") || iconsCdnAfter.includes("PACKAGE_VERSION = '0.8.58'")) {
374
+ console.error('selftest FAIL: PINNED_REFS did not bump the icons-cdn.js PACKAGE_VERSION literal'); process.exit(1);
375
+ }
376
+ if (!iconsCdnAfter.includes("PINNED_VERSION = '2.1.1'")) {
377
+ console.error('selftest FAIL: icons-cdn.js pin bump touched the unrelated PINNED_VERSION (phosphor) literal'); process.exit(1);
378
+ }
379
+ if (bumpPinnedRef("const PACKAGE_VERSION = '0.8.580';", iconsCdnCfg, '0.8.58', '0.8.59') !== "const PACKAGE_VERSION = '0.8.580';") {
380
+ console.error('selftest FAIL: icons-cdn.js pin pattern prefix-matched a longer version'); process.exit(1);
381
+ }
382
+
344
383
  // validatePinsAtFrom — the mutation-free pre-flight (reviewer finding: the
345
384
  // mid-loop pin check fired only after two files were already written).
346
385
  const goodPin = [{ pkg: 'packages/plugins/adia-ui-factory', cfg: pinCfg, path: '/nonexistent/.mcp.json', txt: pinBefore }];
@@ -163,6 +163,33 @@ export const GATE_ROSTER = [
163
163
  // inserted — same numbering discipline as gate 30/31/32/33.
164
164
  { n: 34, cmd: 'node scripts/release/check-yaml-events-vs-runtime.mjs --strict --strict-details', what: 'yaml events: blocks vs runtime dispatch (no phantom/missing events)' },
165
165
  { n: 35, cmd: 'node scripts/release/check-yaml-impl-coverage.mjs --strict', what: 'yaml schema fields vs implementation coverage' },
166
+ // gh#2912, plan-top-priority-verification.md item 4 (G1): the full
167
+ // 10-component x 2-bundler single-import build matrix — byte budgets +
168
+ // 0-foreign-marker-leak + paired-CSS-purity + whole-lib-delta + docs
169
+ // grep-gate. Too slow/heavy to gate every `npm run check` run (it spins
170
+ // up real esbuild AND rollup builds per cell); a 3-cell fast canary is
171
+ // wired into `npm run check` instead (check:treeshake:canary) as a
172
+ // regression tripwire, with the full matrix reserved for cut time. The
173
+ // matrix surfaced 14 real, pre-existing cross-component marker leaks
174
+ // (tracked as gh#2922, not fixed by this ticket) that are held under a
175
+ // known-red allowlist in check-treeshake.mjs itself — a cell diverging
176
+ // from its allowlisted finding, or any non-allowlisted cell failing,
177
+ // still reds this gate; the script also reports an allowlisted cell that
178
+ // comes back clean as promotable rather than staying silent about it.
179
+ // With the allowlist, this gate is currently all-green. Appended, not
180
+ // inserted — same numbering discipline as gate 30/31/32/33/34/35.
181
+ { n: 36, cmd: 'npm run check:treeshake', what: 'single-import build matrix (esbuild+rollup) — byte budgets + marker-leak + CSS purity + whole-lib delta + docs grep-gate' },
182
+ // gh#2911 (plan-top-priority-verification.md item 3 G1): the lint rule
183
+ // bank's seeded-violation catch-rate + false-positive golden-set sweep +
184
+ // 5-rule mutation hardening. NOT in `npm run check` — the golden-set FP
185
+ // sweep alone measured ~100s (every rule in the bank re-parses every one
186
+ // of ~440 `*.examples.html`/`*.contents.html` files; the generated
187
+ // composition rules each call `tagTree()` independently rather than
188
+ // sharing one parse per file — a real perf gap, filed as gh#2937,
189
+ // not fixed here), over AGENTS.md's <60s bar for the PR-blocking
190
+ // aggregate. Appended, not inserted — same numbering discipline as gate
191
+ // 30/31/32/33/34/35/36.
192
+ { n: 37, cmd: 'npm run check:lint-efficacy', what: 'lint rule bank: seeded catch rate 100% + golden-set 0 error FPs + mutation hardening' },
166
193
  ];
167
194
 
168
195
  // -- CLI ------------------------------------------------------------------
@@ -88,6 +88,26 @@ const DEFAULT_NPM_SCOPE = '@adia-ai';
88
88
  const LOCKSTEP_ROSTER = PACKAGE_ROSTER.filter((p) => p.lockstep !== false);
89
89
  const PACKAGES = LOCKSTEP_ROSTER.map((p) => p.name);
90
90
 
91
+ // Reject a --substantive-packages/--stub-packages name whose roster entry is
92
+ // unknown or lockstep:false, BEFORE Step 1 runs (gh#2894). The 0.8.58 cut
93
+ // passed `adia-plugins` (lockstep:false, gh#1133) in --substantive-packages;
94
+ // promote-unreleased.mjs happily rewrote its CHANGELOG's [Unreleased] header
95
+ // to a version that package doesn't ship, and the cut only caught it at Step
96
+ // 5.6's unstaged-tracked-files guard — AFTER the 34-gate pre-flight had
97
+ // already run. Matches PACKAGE_ROSTER by name form (`adia-plugins`) or dir
98
+ // form minus the `packages/` prefix (`plugins/adia-plugins`).
99
+ function assertLockstepPackages(names) {
100
+ for (const pkg of names) {
101
+ const entry = PACKAGE_ROSTER.find(
102
+ (p) => p.name === pkg || p.dir.replace(/^packages\//, '') === pkg,
103
+ );
104
+ if (!entry || entry.lockstep === false) {
105
+ console.error(`error: --substantive-packages/--stub-packages package "${pkg}" is not lockstep:true in scripts/package-paths.mjs PACKAGE_ROSTER (unknown or class-B) — rejected before Step 1.`);
106
+ process.exit(2);
107
+ }
108
+ }
109
+ }
110
+
91
111
  function parseArgs(argv) {
92
112
  const args = {
93
113
  version: null, date: null, previous: null, mode: null,
@@ -148,6 +168,12 @@ function parseArgs(argv) {
148
168
  console.error(`error: --mode must be cut|from-scratch|handoff (got: ${args.mode})`);
149
169
  process.exit(2);
150
170
  }
171
+ // Roster validation (gh#2894), before any mode-specific check below —
172
+ // applies to BOTH cut and from-scratch (the script's own --help text shows
173
+ // both flags on a `cut` invocation).
174
+ if (args.substantivePackages || args.stubPackages) {
175
+ assertLockstepPackages([...(args.substantivePackages ?? []), ...(args.stubPackages ?? [])]);
176
+ }
151
177
  // Handoff needs the GH-notes body at Step 10 — validate at PARSE time, not
152
178
  // there: on the v0.8.10 cut the missing flag surfaced only after tags and
153
179
  // npm publish were already irreversible (Step 10 is the LAST step). An arg
@@ -1753,6 +1779,37 @@ async function selftest() {
1753
1779
  process.exit(1);
1754
1780
  }
1755
1781
 
1782
+ // gh#2894 — a --substantive-packages/--stub-packages name whose roster
1783
+ // entry is lockstep:false (or unknown) must die at PARSE time, naming
1784
+ // the roster file, BEFORE Step 1 runs. The 0.8.58 cut passed
1785
+ // `adia-plugins` (lockstep:false) in --substantive-packages and only
1786
+ // caught it at Step 5.6, after the 34-gate pre-flight had already run.
1787
+ const rosterRejectShapes = [
1788
+ { flags: '--substantive-packages adia-plugins', name: 'adia-plugins' },
1789
+ { flags: '--stub-packages totally-unknown-package', name: 'totally-unknown-package' },
1790
+ ];
1791
+ for (const { flags, name } of rosterRejectShapes) {
1792
+ let failed = false;
1793
+ let out = '';
1794
+ try {
1795
+ execSync(
1796
+ `node "${scriptPath}" --mode cut --version 9.9.9 --date 2026-01-01 --previous-version 9.9.8 ${flags} --dry`,
1797
+ { cwd: REPO, encoding: 'utf8' },
1798
+ );
1799
+ } catch (e) {
1800
+ failed = true;
1801
+ out = (e.stdout || '') + (e.stderr || '');
1802
+ }
1803
+ if (!failed || !out.includes(name) || !out.includes('package-paths.mjs PACKAGE_ROSTER')) {
1804
+ console.error(`selftest FAIL: --substantive-packages/--stub-packages "${name}" must hard-reject naming scripts/package-paths.mjs PACKAGE_ROSTER`);
1805
+ process.exit(1);
1806
+ }
1807
+ if (out.includes('=== Step 1') || out.includes('gate roster')) {
1808
+ console.error(`selftest FAIL: the roster guard for "${name}" must fire BEFORE Step 1, not after`);
1809
+ process.exit(1);
1810
+ }
1811
+ }
1812
+
1756
1813
  // gh#765 — from-scratch with substantive-but-no-stub packages must die at
1757
1814
  // PARSE time, naming the ride-alongs, BEFORE any pre-flight gate runs
1758
1815
  // (v0.8.29 burned three ~15-min pre-flight re-runs discovering it at