# 6.x → 7.0.0, entry by entry

**Four breaking entries, and only one of them can reach you at runtime.** Three are compile errors
the moment you upgrade; the fourth is a type you may never have named. None ships a codemod.

| # | Surface | Costs you an edit if |
|---|---|---|
| 1 | `ScrapeTarget.pageErrors` | you implement `ScrapeDriver`/`ScrapeTarget` yourself |
| 2 | `PwaRenderMode` | you import that type name |
| 3 | `PwaOfflineStrategy` | you import that type name |
| 4 | `PrerenderReport.skipped` | you read `x build --target static --json`, or the report in code |

### 1. `ScrapeTarget` gains a required `pageErrors: PageErrorRing`

**Only a third-party driver author pays this**, and nothing in an ordinary app implements
`ScrapeTarget`. If you build one — the shape `packages/scraping/README.md`'s driver-author example
builds — construct the ring and, if your transport can observe uncaught page exceptions, push to it.

```diff
+ import { createRing, type PageErrorRing } from '@ultimat3/scraping';

  const target: ScrapeTarget = {
    // …
+   pageErrors: createRing(200),
  };
```

A driver that **cannot** observe them builds the ring and never pushes — which is exactly what the
offline targets do. That is the whole migration.

**Required rather than optional, deliberately.** An optional ring lets a driver stay *silent* about
errors it can see, which is the gap this closed: nothing in `@ultimat3/scraping` subscribed to
`pageerror` at all, so an island that **threw** was invisible. A throw calls no console method, so
`console()` answered `[]` and a page whose script had died read as clean.

New on `ScrapePage`, and additive — no edit needed to consume them: `pageErrors()` and
`pageErrorsDropped()`. The dropped count makes the list a **floor**, not a total.

### 2 and 3. `PwaRenderMode` and `PwaOfflineStrategy` are deleted from `@ultimat3/pwa`

Two type-only renames. No member changed — only the name the type is declared under.

| Was | Is | Members, unchanged |
|---|---|---|
| `PwaRenderMode` | `RenderMode` | `'static' \| 'isr' \| 'ssr' \| 'stream'` |
| `PwaOfflineStrategy` | `OfflineStrategy` | `'precache' \| 'runtime' \| 'network-only'` |

```diff
- import type { PwaRenderMode, PwaOfflineStrategy } from '@ultimat3/pwa';
+ import type { RenderMode, OfflineStrategy } from '@ultimat3/pwa';
```

`@ultimat3/pwa` re-exports both under the canonical name, so the import path does not have to move —
`@ultimat3/core` is where they are declared and is equally correct.

**Why the alias existed and why it could not stay.** Tier 4 may not import tier 4, so `@ultimat3/pwa`
wrote its own copy of a set `@ultimat3/render` already had. That copy is what kept `spa` mapped to
`cache-first` after `spa` was deleted in 6.0.0 — the one strategy that gives an `app/` route a
**shared** cache entry, i.e. one signed-in member's HTML served to the next. The vocabulary is now
declared once at tier 0, and `bun run scripts/render-modes.ts --json` refuses a second declaration
anywhere in `packages/*/src`.

### 4. `PrerenderReport.skipped` carries the reason, not just the path

`readonly string[]` → `readonly SkippedRoute[]`, where a `SkippedRoute` is
`{ route, surface, render, reason, why }`. `PrerenderedPage` also gains `route`, the declared path a
concrete URL came from.

```diff
- for (const path of report.skipped) console.log(`skipped ${path}`);
+ for (const skipped of report.skipped) console.log(`skipped ${skipped.route}: ${skipped.why}`);
```

`x build --target static --json` now returns `emitted` and `skipped`, and the human path prints the
same rows.

**Why it changed.** `.x/static/` held a partial site and said nothing about the difference: `app/`
routes exist only through the server, so a tool pointed at the directory filed *"the island did not
mount"* against a route that was never emitted. A list of paths cannot distinguish "not emitted
because it needs a server" from "not emitted because it is broken", and those are opposite facts.
