# Migration — types, history, sweep depth

Consumer migrations: the durable method + the shape of past breaks. The framework MIGRATION
GUIDE (`.claude/docs/MIGRATION GUIDE.md` in the framework repo — the space in the filename is
intentional) is the per-version source of truth; its required shape is
[`contracts/migration-guide-format.md`](contracts/migration-guide-format.md). Loaded by
`app-migration` (all migration types). Versions and examples below are a snapshot, not a registry.

(Claims re-verified against the kit's live source 2026-08-26; re-verify on a MINOR cut.)

## Types

- **version-upgrade** — bump `@adia-ai/*` X→Y (lockstep; all packages move together).
  MINOR/MAJOR carry breaking items; a PATCH span is **additive**, the type below.
- **additive** — a lockstep PATCH span (e.g. 0.7.1 → 0.7.2): drop-in **for the API only,
  never the app** — PATCH cuts routinely ship opt-in layers, workaround-obsoleting fixes,
  and changelog-only work. Classify the span as additive, say so, and hand off to
  `find-unused` (SKILL.md §Drop-in owns this rule) — never report "no code change" and stop.
- **port-to-adia** — an existing app (raw HTML, or legacy `@agent-ui-kit`) → adia-ui: a tag
  rename map (`aui-button`→`button-ui`, `<button>`→`<button-ui>`) + token namespace swap
  (`--n-*`→`--a-*`).
- **mode-change** — SPA↔SSR: re-own routing (framework router vs `<router-ui>`), registration
  (top-level vs client-hook), and state (signals vs cookies). `host-wiring` carries both paths.

## The 5-step sweep — depth

1. **Read the guide** for the version span: index bullets → per-cut sections. Each breaking
   item ships an old form, a new form, and a greppable pattern (the contract above). A missing
   section, or an item without a pattern/verify command, is a producer defect — pause, report
   upstream; don't improvise a breaking surface.
2. **Audit** — `git grep -nE '<pattern>'` per item; cluster by component; report file +
   occurrence counts and show the list before changing anything. Consumer code has edge cases
   the canonical regexes don't anticipate (a `variant="danger"` on a non-button custom element
   that shares the prefix).
3. **Sweep** — mechanical per approved cluster:

   ```bash
   git grep -lz 'button-ui[^>]*variant="danger"' \
     | while IFS= read -r -d '' f; do
         perl -i -pe 's/(<button-ui[^>]*?)variant="danger"/$1color="danger"/g' "$f"
       done
   ```

   Judgment items (below) are flagged, never swept.
4. **Verify** — `adia-lint` clean of `LEGACY-SHELL`/`NATIVE-PRIMITIVE`; the app's own build +
   the browser gate ([`verification.md`](../skills/surface-qa/references/verification.md)); then the leftover-drift pass below.
5. **Report** — per-axis counts, manual-review list, gate results, next actions.

### Sweep anti-patterns (each shipped a real regression)

- **A swept file with local deviations from the guide's before-shape** (a customized wrapper, a
  local fork of a catalog example) is never pattern-swept blind: show its diff and let the
  author merge — the regex was derived from the canonical shape, not theirs.
- **One component per sweep.** `<(toast|alert|tag)-ui[^>]*variant="error"` looks efficient, but
  perl/sed alternation captures don't substitute the matched alternative cleanly — loop
  `for tag in toast alert tag` instead.
- **HTML-attribute regexes don't cover JS property sites.** `<chat-input-ui busy>` and
  `el.busy = true` are two audits — sweep `\.busy\s*=` separately, scoped to app code.
- **`git grep -lz | while read -d '' f; do perl -i … "$f"; done`, never `find … -exec perl`
  or `git grep -l | xargs perl -i`.** git grep excludes `node_modules/`, `dist/`, `.git/`; a raw
  find doesn't. The `xargs` form has two live bugs (gh#1233): zero matches still runs perl once
  with no file argument, which then hangs reading stdin instead of no-oping; and a replacement
  string containing `@` (e.g. `@adia-ai/...`) gets parsed as perl array interpolation when
  inlined into the `-pe` source, silently substituting empty. The NUL-delimited `while` loop
  no-ops on zero matches and never inlines untrusted strings into the perl source.

## Real breaking-change history (before → after)

- **v0.0.20 (10 items):** `<button-ui variant="danger">` → `variant="solid" color="danger"`
  (canonical form; the guide's mechanical sweep emits just `color="danger"`, leaning on `solid`
  being the default variant); stage Booleans (`completed`/`active`) → `status="completed|active"`
  enum (timeline/stepper/pipeline); `<table-toolbar-ui>` opt-out Booleans **inverted**
  (`searchable="false"` → `no-search`; default flipped); `<chat-input-ui busy>` → `loading`;
  `variant="error"` alias removed → `danger`; event prefixes dropped (`chat-submit`→`submit`,
  `legend-toggle`→`toggle`, `slide-change`→`change`); `<field-ui error>` moved to the child
  input; `<agent-trace-ui open>` → `collapsed` (**semantic flip — default-visible now**); kebab
  prop keys → camelCase (JS only). Safari floor → 18.
- **v0.0.29 — three-tier extraction:** `patterns/` moved `@adia-ai/web-components/patterns/*` →
  `@adia-ai/web-modules/{shell,chat,editor,runtime}/*`. Import-path rewrite + add the
  `web-modules` dep.
- **v0.4.0 — legacy shell shapes retired (ADR-0024):** `<aside data-sidebar>`→`<admin-sidebar
  slot>`; `<dialog data-command>`→`<admin-command>`; `[data-chat-messages/input/empty]`→
  `<chat-thread>/<chat-composer>/<chat-empty>`; `[data-editor-body]/[data-canvas]`→
  `<editor-canvas>` + `<editor-sidebar>`. JS selectors move with the markup. (`adia-lint`
  `LEGACY-SHELL` flags the remnants.)
- **v0.6.0/0.6.1:** `stat-ui.{js,css}`→`stat.{js,css}` (deep-import only); `<link-ui>` token
  rename `--link-color-*`→`--link-fg-*` (only if you override).

## Judgment items (flag with call sites — the author decides, never a sweep)

- **Semantic flips** — `<agent-trace-ui open>`→`[collapsed]` is an inversion, not a rename:
  `[open]` was default-hidden/opt-in, `[collapsed]` is default-visible/opt-out. Trace meant to
  show? drop the attribute. Meant hidden? write `collapsed`.
- **Boolean opt-out inversions** — `<table-toolbar-ui>`: the legacy Booleans defaulted to
  `true`, so a bare `filterable` was a no-op; only an explicit `searchable="false"` carried
  intent (→ `no-search`). A regex can't tell the two apart.
- **Attribution transfers** — `<field-ui error="…">` moves the message to the child input,
  which may not exist yet in the markup.
- **JS-only key renames** — kebab property keys (`el['submit-label']`→`el.submitLabel`): the
  HTML attribute form is unchanged, so attribute-only consumers need no change. Audit
  programmatic access only.
- **Value-semantics remaps (v0.8.0 scrims)** — the old `--a-{family}-N-scrim` ramp and the new
  `--md-sys-color-{family}-scrim-*` ladder align by ALPHA VALUE, not by name position: `-1-`
  (20%) → `-weak`, `-2-` (30%) → base, `-3-` (40%) → `-strong`, `-4-` (50%) → `-stronger`,
  `-5/-6-` (60/70%) → `-strongest` (60% is now the ceiling). A blind positional rename shifts
  every overlay one step lighter — remap per call site against the alpha the design needed.
- **Silent capability loss (v0.8.0 named themes)** — `[theme="ocean"]` etc. still parse and
  apply harmlessly, but no longer re-color components (only `--a-brand-hue` + radius/density/
  shadow knobs respond). No symbol was removed, so no grep fails — yet an app that RELIED on
  live re-theming is visually broken. Flag every named-theme consumer; the author decides
  between overriding `--md-sys-color-*` roles directly or accepting the default palette.

## MCP aids (the a2ui server)

`search_chunks` — the *updated* catalog example for a changed component · `check_anti_patterns`
— confirm a swept file is clean · `convert_html` — map legacy/foreign markup to current
components (ports). There is no list-breaking-changes tool; the guide is read by hand.

## Leftover drift — what the path-only sweep misses

A vocabulary migration touches the markup but not the CSS selectors that style it, the JS
comments that mention it, or the metadata that indexes it — different files, so a markup-only
commit looks complete. Close with a pre/post grep diff:

```bash
grep -rln 'OLD_NAME' . --include='*.md' --include='*.json' | sort > /tmp/pre.txt
# … sweep …
grep -rln 'OLD_NAME' . --include='*.md' --include='*.json' | sort > /tmp/post.txt
diff /tmp/pre.txt /tmp/post.txt   # anything left post-sweep is a stale ref to investigate
```

Categories that have survived full-path sweeps:

- **Bare-name prose mentions** in narrative docs and inventory tables (README, roadmap-style
  indexes) — grep `\b<old-name>\b` over `*.md` / `*.yaml`, not just paths.
- **Directories named after the renamed thing** (skill dirs, config dirs) — dir vs frontmatter
  mismatch check:

  ```bash
  for skill in $(find . -name 'SKILL.md' -not -path '*/node_modules/*'); do
    dir=$(basename $(dirname "$skill")); name=$(grep -m1 '^name:' "$skill" | sed 's/name: *//')
    [ "$dir" != "$name" ] && echo "MISMATCH: $skill (dir=$dir, name=$name)"
  done
  ```

- **JSON metadata at filename granularity** (highest impact — a stale `source`/`page` field is
  a *silent* harvest miss on the next rebuild; no error is raised):

  ```bash
  for f in 'old-name.html' 'old-name.contents.html' 'old-name.contents.js'; do
    grep -rn "$f" --include='*.json' | grep -v '/dist/' | grep -v '/node_modules/'
  done
  ```

- **Refs to the directory itself, not the tag** — barrel JS exports, CSS `@import`s, HTML link
  rels, sitemap/content paths name the old *dir*; a tag-rename sweep never touches them. Grep
  the old directory name separately.
- **Relative-import depth after `git mv`** — grep each moved file for `../` imports and fix the
  depth; builds don't load them, so the breakage surfaces only in the browser. A doc move needs
  a *different* rewrite per linking file's own depth — precompute per file.
- **Hard-coded source lists in build scripts** — `SOURCES = […]` / include-dir arrays silently
  drop renamed or new siblings while the build runs clean; diff output counts against the
  pre-rename baseline after any rename.
