# `gates-catalog.md`, pre-flight gate roster + failure → recovery map

`<plugin-root>` below is `$CLAUDE_PLUGIN_ROOT` in Claude Code; the plugin's installed directory
in Codex.

> Load for a verify-only run or on any gate failure during a cut. Maps every
> release-flow gate × what it checks × typical failure × recovery. Gates are
> grouped by **failure category**, how the operator routes when one goes red, > not by alphabetical namespace. Cited scripts live in the target monorepo's
> `package.json` (`check:*` / `verify:*` / `smoke:*` / `test:*`).

Row layout per gate: **What** · **Typical failure** · **Recovery**.

---

## §Category 1, Release identity (hard-fail any cut)

### `npm run check:lockstep`

- **What:** all 10 lockstep `@adia-ai/*` packages declare the same `version` (the class-B `adia-plugins` package is excluded, `lockstep: false`, `scripts/package-paths.mjs`); internal `@adia-ai/*` dep ranges match policy (`^X.Y.0` during PATCH cycles, bumped at MINOR).
- **Typical failure:** one package forgot to bump; a peer edited an internal range mid-PATCH; a `^0.0.x` range slipped in.
- **Recovery:** version drift → `` `<plugin-root>/skills/package-release/scripts/bump.mjs` ``; range drift → `npm run check:lockstep:fix` auto-aligns, then re-run.
- **Why `^0.0.x` is forbidden:** npm pre-1.0 semver only widens the caret when major+minor aren't both zero, `^0.0.6` resolves to `>=0.0.6 <0.0.7`, locked to exactly 0.0.6. An internal dep pinned that way silently installs a *stale* sibling on every fresh `npm i` (this shipped a real ~4-day-latent bug before the lockstep policy). The `^X.Y.0` floor (Y≥1) widens correctly across patches; trust the gate, don't reason about caret semantics by hand. Moot at 1.0.0.

### `node scripts/release/check-release.mjs --all-pending` (F-N1, the release trip-wire)

- **What:** for every unpushed tag, verifies CHANGELOG coverage of the diff between this tag and the package's previous tag.
- **Cosmetic failure:** "diff `packages/<pkg>/components/` touched but CHANGELOG `[X.Y.Z]` doesn't mention 'components'": the entry names the component (`table-ui`) but not the literal path keyword. Regex miss, change IS documented.
- **Real failure:** a touched directory has NO matching CHANGELOG entry at all, the cycle missed documenting a change.
- **Recovery:** cosmetic → add the path keyword to a relevant entry (`table.yaml` → `components/table/table.yaml`); real → author the missing entry. Either way: land it as a new commit through the PR flow (the release commit is already merged, `--amend` is not possible post-invariant-3), delete + re-create the tags at the new post-merge SHA, re-run. Full discipline: [`changelog-discipline.md`](changelog-discipline.md) §F-N1 enrichment.
- The umbrella tag `vX.Y.Z` classifies **`info`** (since 2026-07-19, it used to score `error`, which false-stopped release-pack's Step 7 on every cut); only per-package findings need action.

(The former `check:changelog-coverage` gate was deleted 2026-07-19, its drifted 9-package matcher could report "clean" where the authoritative gate fails. Pre-tag coverage is `check-release.mjs --pending-version X.Y.Z [--fix]`, Step 4f.)

---

## §Category 2, Generated-file coherence (hard-fail any cut)

### `node scripts/build/components.mjs --verify`

- **What:** every component yaml has an up-to-date `.a2ui.json` sidecar; `.d.ts` codegen current. (Generated files are also hook-guarded against hand edits.)
- **Recovery:** `node scripts/build/components.mjs` (no `--verify`) regenerates; stage the sidecars.

### `npm run verify:traits`

- **What:** `traits/_catalog.json` reflects every `defineTrait()` call; 100% coverage.
- **Recovery:** `npm run build:traits`; stage.

### `npm run verify:a2ui-schema`

- **What:** the A2UI JSON Schema is in sync with the `@adia-ai/a2ui-runtime` TS types.
- **Recovery:** `node scripts/build/a2ui-schema-types.mjs`; stage.

### `npm run verify:tsconfig-strictness`

- **What:** per-package `tsconfig.json` doesn't silently relax flags set strict in `tsconfig.base.json` (ADR-0029). A relaxation needs a `// TS-MIG-NNN` tracking comment.
- **Recovery:** remove the override, or add the tracking comment.

---

## §Category 3, Component / primitive structural drift

### `npm run check:demo-shells`

- **What:** every component demo `.html` imports all primitives named in its yaml `composes:` list.
- **Typical failure:** a yaml gained a `composes:` entry but the demo shell didn't get the matching `<script>` import.
- **Recovery:** add `<script type="module" src="../<tag>/<tag>.js">` to the demo shell. **Release-blocking: HIGH**, a commit failing this gate ships broken demo pages in its tarball. If the fix already landed later but entangled with `[Unreleased]` work → [`recovery-paths.md`](recovery-paths.md) §Scenario 4.

Smaller siblings in this category (same recovery shape, fix the declaration or regenerate the registry):

| Gate | What |
| --- | --- |
| `check:composes` | every yaml `composes:` entry resolves to a real primitive |
| `check:dts-sibling-presence` | every component `.js` has a sibling `.d.ts` |
| `check:registry-catalog-coherence` | runtime registry ↔ A2UI catalog parity (both directions) |
| `check:required-icons` | every referenced `<icon-ui>` name is registered |
| `check:standalone-html-phosphor` | standalone demos using Phosphor icons include the loader |
| `check:substitutable-set-drift` | substitutable-element registry in sync |

---

## §Category 4, CSS spec compatibility

### `npm run check:lightningcss-build`

- **What:** every CSS file under `packages/web-{components,modules}/` minifies cleanly under LightningCSS + current Vite default targets.
- **Recovery:** fix the offending CSS. Most common sub-case ↓

### `npm run check:scope-bare-descendants`

- **What:** no `@scope { > X {} }` bare-combinator descendants (LightningCSS rejects as "Invalid empty selector").
- **Recovery:** `node scripts/build/codemod-scope-bare-descendants.mjs` rewrites `> X` → `& > X`.

Also: `check:no-self-import-css` (no transitively self-importing barrel) · `check:rolldown-glob` (`import.meta.glob` works under Rolldown).

---

## §Category 5, Browser safety / module hygiene

### `npm run check:browser-safe`

- **What:** no top-level `import 'node:*'` in browser-reachable modules (`packages/gen-ui/a2ui/{compose,retrieval,validator}`, `web-components/`, `web-modules/`, `apps/*/app/*.contents.js`).
- **Recovery:** dual-mode pattern (`IS_NODE` + dynamic `await import(/* @vite-ignore */ 'node:*')` + `import.meta.glob` browser branch); canonical impl: `packages/gen-ui/a2ui/retrieval/component-catalog.js`.

Siblings: `check:absolute-imports` (no leading-`/` imports, rewrite relative) · `check:template-interp` (template `${expr}` contract) · `check:yaml-events` (yaml `events:` match runtime `dispatchEvent` calls) · `check:yaml-impl-coverage` (every yaml prop implemented in the JS class).

---

## §Category 6, Visual / structural integrity

| Gate | What |
| --- | --- |
| `check:card-structure` / `check:drawer-structure` | card/drawer body wraps in `<section>` (strict variants of the `audit:*` twins) |
| `check:with-css-pairing` | every shell with paired `.js` + `.css` has a `/with-css` companion + exports-map entry |
| `verify:no-legacy-shell-shapes` | no legacy `<main>` / `[data-content-*]` shapes under `<admin-shell>` (ADR-0032) |
| `audit:native-primitive-leak` | native `<button>`/`<input>`/… where a `*-ui` equivalent exists; criticals must be replaced or annotated `data-native-ok="<reason>"` |
| `audit:shell-composition` | admin-shell compositions missing canonical parts (statusbar, `[data-spacer]`/`[data-actions]`, …); escape hatch `data-shell-opt-out="<reason>"` |

The two `audit:*` probes are soft-fail pre-cut (exit-1 only on criticals); findings get folded into release notes if non-zero. Recovery for both is manual, slot/attr semantics need a human eyeball, never auto-fix.

---

## §Category 7, Corpus / bundle freshness

### `npm run verify:corpus`

- **What:** every chunk in `packages/gen-ui/a2ui/corpus/chunks/*.json` is reachable and well-formed.
- **Recovery:** corpus remediation routes to the A2UI-pipeline skill, not this one.

### `npm run check:catalog-tiers`

- **What:** `packages/gen-ui/a2ui/catalog/tier-index.json` derives cleanly from the committed catalog + the hand-authored `tiers/l*-*.json` sources (L0–L4 membership, composability law). It ships inside `@adia-ai/a2ui`, so a stale index is a publish defect. ADR-0069's gate split moved its PR-blocking freshness onto the `derived-resync` (push:main) job; the pre-cut roster re-asserts it because gate `check:chunks-fresh` cannot, the harvester hashes `tier-index.json` as a harvest *source*, so a stale-but-harvested index keeps that gate green (gh#1494).
- **Recovery:** `npm run build:catalog-tiers`; if that regen stales `check:chunks-fresh`, `npm run harvest:chunks` (the conditional pair Step 4d.5b runs post-bump, a byte-identical tier regen owes no re-harvest). Stage `tier-index.json` + any corpus outputs.

### `npm run check:chunks-fresh`

- **What:** `chunks/_index.json` matches on-disk chunks and source fragments.
- **Recovery:** `npm run harvest:chunks`; stage the corpus outputs.

### `npm run check:embeddings-fresh`

- **What:** `chunk-embeddings.json` at-or-newer-than `chunks/_index.json`.
- **Recovery:** `npm run build:embeddings:chunks` (needs `OPENAI_API_KEY`; ~6s). Stage `chunk-embeddings.json` + note the regen in the a2ui-corpus CHANGELOG `[vX.Y.Z]`.
- **2026-08-12, isolated agent worktrees can't run this recovery step.** An agent dispatched with `isolation: worktree` (e.g. `build-lead` resolving a merge conflict) structurally can't read the dotenv secrets file, a permission guard blocks copying it into the worktree, correctly, since that's a content-revealing operation on a secret file. Hit twice in one sweep: two separate PRs (#1113, #1117) each needed this recovery step mid-conflict-resolution and both had to hand off to a human running the command directly in their own terminal / via `!`. This step is a standing human-in-the-loop point, not something to keep dispatching an agent for.

### `npm run check:css-bundles-fresh` / `npm run check:js-bundles-fresh`

- **What:** the `dist/` CDN bundles (`web-components.min.{css,js}`, `everything.min.js`, per-shell `*.min.js`, `icons-manifest.js`) match source. Stale bundles mean jsdelivr/unpkg serve yesterday's build. **Blocking in CI** (2026-06-08).
- **Recovery:** `npm run build -w @adia-ai/llm` **FIRST** (its `index.js` is a gitignored tsc artifact `build:bundle-js` resolves, a fresh worktree can't bundle without it), then `npm run build:bundles` (or `build:bundle-css` / `build:bundle-js` individually). Stage both `dist/` trees.
- **Semantic content checks, mechanized 2026-07-19** (formerly manual spot-checks; the v0.6.29–31 cycles shipped a shells-only `everything.min.js` that hard-crashed CDN consumers): `bundle-js.mjs` now enforces, on every build AND `--verify` run, that (1) `everything.js` still imports `@adia-ai/web-components`, (2) `everything.min.js` carries ≥100 distinct `-ui` tags, and (3) `icons-manifest.js` exists populated in BOTH dist trees. A fresh-but-semantically-wrong bundle fails the gate itself, no separate manual step remains.

### `npm run audit:chunk-reconcile`

- **What:** duplicate-reconciliation report for the chunk corpus (fingerprint collisions, near-duplicates, structural twins). Advisory, not blocking.

---

## §Category 8, Tests + types + evals

### `npm run test:unit`

- **What:** the vitest suite (~1000+ tests).
- **Stale-test failure:** the test asserts a behavior a peer deliberately changed (CHANGELOG-documented) without updating the assertion. Tell it apart by reading assertion vs CHANGELOG vs source: if the code matches the CHANGELOG's described behavior, the test is stale, update the assertion (see [`recovery-paths.md`](recovery-paths.md) §Scenario 5). Real regression → fix the regression.
- **Parallel-contention flake:** heavy web-modules composite suites can fail under full parallelism on a loaded machine (`signals: drain loop exceeded 100 iterations`). Load-dependent, not a code defect. Sequential is the source of truth, and **the roster gate now runs it directly** (`npm run test:unit:serial` = `vitest run --no-file-parallelism`, gate 4 since 2026-07-19), a parallel `test:unit` flake outside the roster still isn't a blocker, and don't raise the drain guard.
- **Same-host contention timeout (gh#2195):** gate 4 running solo (Phase 1, see the execution-model note above) isolates it from every OTHER pre-flight gate, but not from unrelated concurrent load on the same shared, multi-tenant host, another agent's build, a peer's own `npm run check`. gh#2195's diagnosis found this surfaces as a genuine 45s per-test timeout on a DIFFERENT single file each run, with no repeat culprit (the contention signature, not an order-dependent leak), and found no separate "release pre-flight runner" with its own core count exists to recalibrate `worker-cap.mjs` against; it's the same operator/agent host either way. `release-pack.mjs`'s `runSoloGateWithRetry()` now retries gate 4 exactly once, scoped to only the file(s) it named as failed, when the failure output has a parseable `FAIL <file>` list, a file that fails once and passes clean on that immediate re-run was contention, not a regression; a file that fails twice is treated as a real one and the pre-flight still aborts. No other gate, and no OTHER invocation of `test:unit:serial` (CI, local dev, `npm run check`), gets this retry.
- **Exit 143 (SIGTERM):** the vitest process was KILLED (resource pressure / a stray terminator), not a test failure, re-run the gate directly (`npx vitest run --no-file-parallelism`) before diagnosing anything; a clean re-run means transient, proceed (v0.8.34 handoff hit this).
- **First-time-at-cut failures are REAL (v0.8.34):** this serial suite runs tests PR CI never does, `exit-gate.corpus` (dialect-catalog conformance over real harvested chunks) surfaces yaml-schema drift (e.g. an array prop missing `items.type`, the command/combobox `DynamicStringList` mis-map) only HERE, potentially weeks after the yaml edit merged green. Treat such a failure as a genuine latent defect to root-cause at the yaml SoT, never as release-blocking noise. (Structural fix, promoting the corpus batch into PR CI, tracked as a follow-up.)

### `npm run eval:diff -- --engine zettel`

- **What:** end-to-end gen-UI eval. Floors (preserve-not-regress):
  - **Zettel**: cov ≥ 87%, avg ≥ 85, MRR ≥ 0.94
  - **Free-form**: cov ≥ 90%, avg ≥ 83, F1 ≥ 55
  - **Monolithic**: cov = 100%, avg ≥ 95
  - **Dogfood**: 20/20, avg ≥ 95
- **Recovery:** retrieval/corpus regression, routes to the A2UI-pipeline skill.

Siblings: `typecheck` (`tsc --noEmit`) · `smoke:engines` (gen-UI engines + retrieval probes) · `smoke:register-engine` (11/11) · `test:a2ui` (22/22, +1 skipped OK).

---

## §Category 9, Misc release safety

### `node scripts/release/check-cut-hygiene.mjs --version X.Y.Z`

- **What:** roster gate 18 (wired 2026-07-19; release-pack fills `--version`, the roster's one `versionArg` entry). The objective half of the v0.7.13-retro hygiene checker: published README CDN pins / "Current version" claims must not sit below the cut's minor (`doc-currency`, error-level); merged release branches + extra worktrees print as `limbo` warns (advisory without `--strict`). The judgment half of §4e, content *currency* of what the READMEs say, stays yours.
- **Recovery:** update the stale README pin/claim, stage it into the release commit. A `limbo` warn routes to branch/worktree cleanup, never blocks the cut.

### `npm run dogfood:status`

- **What:** aggregator over the component dogfood audits; classifies findings P0–P3 and regenerates the tracker at `qa/findings/dogfood-tracker.md`. Exit-1 when P0+P1 > 0; P2/P3 are advisory.
- **Pre-cut policy: must pass before tag.** Any P0/P1 means a paid-down bug class was re-introduced; open the tracker, apply the canonical fix template at the cited file:line, re-run. ~5s.
- **Layout-aware since gh#1359.** Two of the aggregated audits (`empty-instantiation`, `padded-route-gutter`) render against a throwaway vite dev server and need the pnpm/bootstrapped layout to trust rendered icons (gh#340). Under an npm-shaped `node_modules` (or the explicit `--static-only` flag) the run auto-degrades, skips those two legs, prints a loud non-fatal notice, and still runs every other (static, layout-agnostic) audit. A green static-only result is **not** full coverage: run `node scripts/dev/bootstrap-worktree.mjs && npm run dogfood:status` once under the pnpm layout too before a cut ships. Only a missing `node_modules` entirely still hard-exits.

### `npm run verify:pack`

- **What:** `npm pack --dry-run` succeeds per package, catches malformed `files`/`exports`. Note: the npm `files:` array supports negation (`!components/**/*.html`); verify tarball contents with `--dry-run` after touching it.

Siblings: `check:links` (intra-repo markdown links resolve) · `check:cdn-pins` (docs' CDN `@0.X` pins match the current minor, catches a stale `@0.6` after a `0.7` cut) · `verify:contrast` / `verify:palette` (WCAG pairs / OKLCH ramps).

---

## §Standard subsets

**Minimum 6 (verify-only default)**, fastest set that catches release-blockers:

```bash
node scripts/build/components.mjs --verify
npm run check:lockstep
npm run verify:traits
npm run test:unit
npm run typecheck
npm run check:demo-shells
```

Add `verify:corpus` + `check:embeddings-fresh` if chunks were touched; `check:lightningcss-build` if CSS was touched; F-N1 if unpushed release tags exist.

**Full pre-cut sweep**, the 30-gate roster in [`cut-procedure.md`](cut-procedure.md) §Step 3, sourced from `` `<plugin-root>/skills/package-release/scripts/gate-roster.mjs` `` (the ONE list; `release-pack.mjs` imports and runs it in full, a subset run is impossible without editing that file). ~90s wall time.

**Omnibus**, `npm run check` invokes everything. Heavy; use when re-baselining a stale checkout.

**Suffix variants:** most gates have `:strict` (warns fail), `:fix` (auto-apply, e.g. `check:lockstep:fix`), `:json` / `:quiet` (output form) variants.

---

## §Gate stewardship

- **This catalog must stay in sync with `package.json` `scripts`.** When a cycle meets a release-flow gate not listed here, add its row in the same change.
- **New CI gates ship advisory first** (`continue-on-error: true`), promoted to blocking after 5+ green runs. Two freshness-gate traps: mtime checks are permanently red on fresh `actions/checkout` (clone-time mtimes, need `fetch-depth: 0` + git-restore-mtime); a dir-watching gate needs its own workflow whose `pull_request: paths:` covers the watched dirs.
- **New/extended `scripts/release/check-*.mjs` slots run against `main` immediately**; close every finding same-cycle. Before adding a slot, grep the existing ones: the gap is often a regex limit in a current slot, not a missing slot.
