@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.
@@ -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,8 +101,28 @@ 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
 
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
+
105
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.
106
127
 
107
128
  ```bash
@@ -140,6 +161,8 @@ npm run check:harness-manifests-fresh # 32 Hermes/Pi plugin.yaml + __in
140
161
  npm run verify:patterns-index # 33 pattern-index.md (mcp + adia-ui-factory) vs corpus source
141
162
  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
163
  node scripts/release/check-yaml-impl-coverage.mjs --strict # 35 yaml schema fields vs implementation coverage (gh#2829)
164
+ 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)
165
+ 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
166
  ```
144
167
 
145
168
  **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 +180,50 @@ Any red → route via [`gates-catalog.md`](gates-catalog.md); fix at the source,
157
180
 
158
181
  ## §Step 4 — (Variant B, or ANY variant with uncommitted `[Unreleased]` content) Promote, bump, lockfile
159
182
 
183
+ **4a-pre. Assemble `changes/<pr>.md` fragments (REQ-W11-06, gh#2931), BEFORE promotion.** This
184
+ repo's PRs land a `changes/<pr>.md` fragment instead of hand-editing a CHANGELOG directly —
185
+ `check-changelog-pr-gate.mjs`'s own PR-time gate now REQUIRES a fragment for a roster-package
186
+ change and refuses the direct edit outright (gh#3123; the root `CHANGELOG.md` was already
187
+ fragment-only in practice before that ticket). Those fragments accumulate
188
+ unreleased until something folds them into the right CHANGELOG's `[Unreleased]` section — that's
189
+ this step, and it must run before 4a promotes `[Unreleased]` to a versioned heading, or a
190
+ fragment folded in afterward would land under the WRONG (already-promoted) heading.
191
+
192
+ ```bash
193
+ node scripts/release/assemble-changelog-fragments.mjs # writes, deletes consumed fragments
194
+ node scripts/release/assemble-changelog-fragments.mjs --verify # must print PASS afterward
195
+ ```
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
+
202
+ Routing rule (`scripts/release/assemble-changelog-fragments.mjs`'s own header, full detail
203
+ there): a fragment's first line is `- <kind>: <sentence>` (kind in fix|feature|chore|docs,
204
+ unchanged from `changelog_fragments.py`'s schema) or, this repo's own addition, `- <kind>
205
+ (<package>): <sentence>` naming a `PACKAGE_ROSTER` (`package-paths.mjs`) entry. The
206
+ parenthetical-package form routes to that package's own `packages/<dir>/CHANGELOG.md` — the
207
+ norm for package-scoped fragments as of gh#3123, not a hypothetical; the plain form routes to
208
+ the repo-root `CHANGELOG.md`, whose own header scopes
209
+ it to exactly that shape of change ("tooling, CI, build scripts, cross-package work, docs").
210
+ `kind` maps to a Keep-a-Changelog subsection: `feature`→Added, `fix`→Fixed, `chore`→Changed,
211
+ `docs`→Docs — created under `## [Unreleased]` in that canonical order if the subsection doesn't
212
+ already exist, otherwise appended to the existing one.
213
+
214
+ **Deliberately NOT a pre-flight roster gate (§3.1).** Pending fragments are a NORMAL state
215
+ between PRs, not a defect — a `--verify`-shaped freshness gate added to the pre-cut roster (which
216
+ runs before this step, in Step 3) would fail on every cut that has any recent chore/fix/feature
217
+ PR queued, which is the common case. The `--verify` invocation above is a post-assembly
218
+ self-check (proves the assemble actually consumed everything it found), not a standing gate;
219
+ `gate-roster.mjs`'s count is unchanged by this ticket.
220
+
160
221
  **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
222
 
162
223
  **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
224
 
225
+ `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.
226
+
164
227
  **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
228
 
166
229
  **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 +292,7 @@ stub sections exist leaves it nothing to append to, and the gap resurfaces
229
292
  as F-N1 warns at the push boundary, costing a tag move:
230
293
 
231
294
  ```bash
295
+ node scripts/release/assemble-changelog-fragments.mjs # 4a-pre (idempotent — safe to re-run; no-op if already assembled)
232
296
  node "<plugin-root>/skills/package-release/scripts/insert-stub.mjs" \
233
297
  --version X.Y.Z --date YYYY-MM-DD --previous-version X.Y.Z-1 \
234
298
  --substantive "<one-line>" --xref "<anchor>" --packages <missing-stubs> # 4a-stub — FIRST, only the missing ones (hard-errors on existing sections)
@@ -286,11 +350,20 @@ Drift here means a regen output was left out of the allowlist — stage it and r
286
350
  fails the cut if any tracked file is still modified-in-the-worktree —
287
351
  proof the allowlist covered everything bump.mjs / cut-hygiene touched this
288
352
  cut, not just what §Step 5.5's three named freshness gates happen to check.
289
- This is the fourth time the allowlist itself went stale (gh#1198, gh#1899,
290
- gh#1954, gh#2473 — most recently the factory README's generation-MCP pin
291
- prose), so the guard is generic rather than another named file. A manual
292
- cut should run the equivalent check by hand: `git status --porcelain`
293
- after staging must be empty of `M`/`D` lines.
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.
294
367
 
295
368
  ## §Step 5.7 — Release PR: push the branch, merge, re-baseline
296
369
 
@@ -413,6 +486,18 @@ npm view @adia-ai/web-components dist-tags.latest # must equal X.Y.Z
413
486
 
414
487
  Zero workflows fired after a tag push → [`recovery-paths.md`](recovery-paths.md) §Scenario 7.
415
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
+
416
501
  ## §Step 10 — GH releases + site deploy dispatch
417
502
 
418
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
- const A2UI_MCP_PIN = {
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)' },
@@ -88,6 +96,23 @@ const PINNED_REFS = {
88
96
  new RegExp(`("@adia-ai/[a-z0-9-]+":\\s*")${from.replace(/\./g, '\\.')}(?![\\d.])`, 'g'),
89
97
  replace: (to) => `$1${to}`,
90
98
  }],
99
+ // icons-cdn.js's hand-typed PACKAGE_VERSION literal (gh#3228's jsDelivr
100
+ // manifest-fallback URL pin) must move with web-components' OWN version
101
+ // every cut — unlike the file's other literal (PINNED_VERSION, which
102
+ // tracks @phosphor-icons/core's independent release cadence and is
103
+ // updated by hand only when Phosphor cuts), this one pins the very
104
+ // package it lives in. Left unmoved by bump.mjs through the 0.8.58 cut,
105
+ // it would leave MANIFEST_CDN_FALLBACK_URL naming a stale version with
106
+ // check:lockstep staying green — no coherence source watched it until
107
+ // lockstep-checks.mjs's invariant 9 (gh#3240) and this pin closed the
108
+ // loop together.
109
+ 'packages/web-components': [{
110
+ file: 'core/icons-cdn.js',
111
+ label: 'icons-cdn.js PACKAGE_VERSION literal (jsDelivr manifest fallback pin)',
112
+ pattern: (from) =>
113
+ new RegExp(`(const PACKAGE_VERSION = ')${from.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(?![\\d.])(')`),
114
+ replace: (to) => `$1${to}$2`,
115
+ }],
91
116
  };
92
117
 
93
118
  // Repo-level version references (path relative to REPO ROOT, not a package) —
@@ -96,7 +121,7 @@ const PINNED_REFS = {
96
121
  // once and the next cut fails the gate — so the bump moves it, like the
97
122
  // .mcp.json pin (H2 follow-through, 2026-07-19: found 0.7.13 claimed while
98
123
  // 0.8.7 was live — a full minor of README drift shipped to npm).
99
- const REPO_PINNED_REFS = [
124
+ export const REPO_PINNED_REFS = [
100
125
  {
101
126
  file: 'README.md',
102
127
  label: '"Current version" claim',
@@ -274,10 +299,14 @@ function main() {
274
299
  // script never calls fs.writeFileSync by pointing at a path whose directory
275
300
  // doesn't exist; a latent write bug would throw ENOENT instead of passing.
276
301
  function selftest() {
302
+ // A fixture pkg name deliberately NOT in PINNED_REFS/SIBLING_MANIFESTS —
303
+ // this proof exercises bumpAll's core version-field transform in
304
+ // isolation, so it must not collide with (and pull in the file reads for)
305
+ // any real package's pin config.
277
306
  const fake = [{
278
- pkg: 'packages/web-components',
307
+ pkg: 'packages/fixture-pkg',
279
308
  path: '/nonexistent-fixture-dir/package.json',
280
- txt: '{\n "name": "@adia-ai/web-components",\n "version": "0.8.4"\n}\n',
309
+ txt: '{\n "name": "@adia-ai/fixture-pkg",\n "version": "0.8.4"\n}\n',
281
310
  version: '0.8.4',
282
311
  }];
283
312
  if (!validateAllAtFrom(fake, '0.8.4')) {
@@ -341,6 +370,24 @@ function selftest() {
341
370
  console.error('selftest FAIL: peer-pin pattern prefix-matched a longer version'); process.exit(1);
342
371
  }
343
372
 
373
+ // icons-cdn.js PACKAGE_VERSION literal (gh#3240, invariant 9) — must move
374
+ // with web-components' own version, and must not touch the file's OTHER
375
+ // literal-looking pin (PINNED_VERSION, @phosphor-icons/core's version,
376
+ // updated by hand on its own independent cadence).
377
+ const [iconsCdnCfg] = PINNED_REFS['packages/web-components'];
378
+ const iconsCdnBefore =
379
+ "const PINNED_VERSION = '2.1.1';\n\nconst PACKAGE_VERSION = '0.8.58';\n";
380
+ const iconsCdnAfter = bumpPinnedRef(iconsCdnBefore, iconsCdnCfg, '0.8.58', '0.8.59');
381
+ if (!iconsCdnAfter.includes("PACKAGE_VERSION = '0.8.59'") || iconsCdnAfter.includes("PACKAGE_VERSION = '0.8.58'")) {
382
+ console.error('selftest FAIL: PINNED_REFS did not bump the icons-cdn.js PACKAGE_VERSION literal'); process.exit(1);
383
+ }
384
+ if (!iconsCdnAfter.includes("PINNED_VERSION = '2.1.1'")) {
385
+ console.error('selftest FAIL: icons-cdn.js pin bump touched the unrelated PINNED_VERSION (phosphor) literal'); process.exit(1);
386
+ }
387
+ if (bumpPinnedRef("const PACKAGE_VERSION = '0.8.580';", iconsCdnCfg, '0.8.58', '0.8.59') !== "const PACKAGE_VERSION = '0.8.580';") {
388
+ console.error('selftest FAIL: icons-cdn.js pin pattern prefix-matched a longer version'); process.exit(1);
389
+ }
390
+
344
391
  // validatePinsAtFrom — the mutation-free pre-flight (reviewer finding: the
345
392
  // mid-loop pin check fired only after two files were already written).
346
393
  const goodPin = [{ pkg: 'packages/plugins/adia-ui-factory', cfg: pinCfg, path: '/nonexistent/.mcp.json', txt: pinBefore }];
@@ -368,6 +415,28 @@ function selftest() {
368
415
  console.log('selftest OK');
369
416
  }
370
417
 
371
- const topArgv = process.argv.slice(2);
372
- if (topArgv[0] === 'selftest') selftest();
373
- 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
+ }
@@ -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 ------------------------------------------------------------------