diff --git a/.llm/runs/release-0.0.6-features--orchestration/slices/implement-1459.md b/.llm/runs/release-0.0.6-features--orchestration/slices/implement-1459.md new file mode 100644 index 0000000000..f9b94dacc5 --- /dev/null +++ b/.llm/runs/release-0.0.6-features--orchestration/slices/implement-1459.md @@ -0,0 +1,133 @@ +use harness + +# Slice brief — #1459 the deferred refresh coordinator is never hydrated + +**Codex · GPT-5.6 Sol · high** (`complex_implementation`). The plan **passed PLAN-EVAL** (run +`31593309658`, verdict PASS in the body). Implement it; do not re-decide it. + +| Field | Value | +| --- | --- | +| Issue | #1459 (`priority:p1`) | +| PR | **#1558** — already open as a draft at plan phase; commit onto its branch | +| Worktree | `/home/codex/repos/ns006-1459` | +| Branch | `fix/1459-defer-island-hydration` | +| Base | `origin/main@59e435c5d` | + +**Read first, in order:** + +1. `slices/plan-1459.md` — read the **whole** file. It has three sections: the original plan, then + **`# Plan v2`** (the revision after `FAIL_PLAN`), then **`## PLAN-EVAL v2 result`** with + amendments **B1–B4**. **v2 and B1–B4 are binding**; where the original plan disagrees with v2, v2 + wins. +2. `slices/research-1459.md`. + +If they are not on your branch: `git show chore/release-0.0.6-runtime-reopen:`. + +## SKILL + +- `deno-fresh` — Fresh 2.x islands, partials, client navigation, and the Vite plugin. +- `netscript-doctrine` — `packages/fresh` is framework code. +- `netscript-cli` — the scaffold template surface, if D1 touches it. +- `netscript-tools`, `netscript-pr`, `netscript-harness`. + +## The defect — two inseparable halves + +1. **`DeferIsland.tsx` is not an island at all.** No island registration exists anywhere. It is a + *named* export (`DeferIsland.tsx:111`); the scaffold calls `fresh()` with **no options** + (`packages/cli/src/kernel/assets/app/vite.config.ts.template:41`); `createNetScriptVitePlugin` has + **zero** island logic. It is reached only through a server-render path, so it never enters the + client bundle. +2. **`f-client-nav` is false in exactly the failing case.** `DeferIsland.tsx:222` sets + `f-client-nav={!(isPartialRequest && !hasCachedData)}` — that condition **is** `partial-miss`. + PLAN-EVAL verified against Fresh core that `client.ts:256` gates form submission on + `checkClientNavEnabled`, so with it false the `requestSubmit()` produces a **full document + navigation**, not a `/partials/**` request. It also confirmed the client refresh mechanism is + form/anchor interception only — **no background poll, no auto-refresh timer**, so nothing else + rescues a missed region. + +Fixing (1) without (2) leaves the reported symptom unfixed. + +## LOCKED decisions + +- **D1 — register the island via `islandSpecifiers`.** `@fresh/plugin-vite` exposes + `islandSpecifiers?: string[]`, documented as *"Treat these specifiers as island files. This is used + to declare islands from remote packages."* + **B1 (binding):** the scaffold pins **`jsr:@fresh/plugin-vite@^1.1.2`** — verify against **1.1.2** + (`utils.ts:59-63`; mechanism at `src/mod.ts:234-237`, `fConfig.islandSpecifiers.set(spec, name)`), + **not** 1.0.8. Copy-mode and a consumer-owned shim are **rejected**. +- **B2 (binding) — the specifier must be created, not just referenced.** `packages/fresh/deno.json` + has **no** sub-export for the defer island today. You must **(a)** add the sub-export and **(b)** + name that specifier in `vite.config.ts.template`'s `fresh({ islandSpecifiers: [...] })`. Choose the + concrete specifier string and say why. +- **D2 — fix `f-client-nav`** so `partial-miss` enables client nav. Semantics are already verified; + do not re-litigate them. +- **D3 — move the coordinator form *inside* the region's ``.** It currently renders as a + sibling outside it (`DeferPage.tsx:257-275`), so a region swap leaves stale DOM and a page swap + re-renders an inert form. **Fallback if that breaks the fallback-render contract:** stable `key` + + remount. `SlotRef` is not pursued. State which landed and why. +- **D4 — do not change `decideDeferClientAction`'s policy logic** (`policy.ts:177-208`). It is correct + and unit-covered; it is simply never executed. If your fix requires changing it, **stop and report**. +- **D5 — scope.** There is exactly **one** render site (`DeferPage.tsx:263`), and the streaming path + provably never renders it (`runtime/mod.tsx:178`, `!shouldStream && …`). Out of scope: the dead + `debug` prop (`DeferIsland.tsx:54`), and the #1457/#1548 surfaces (both already merged). +- **B4 — migration.** `islandSpecifiers` is a `fresh()` option, so this is a **scaffold-template + change**: already-generated apps do **not** get the fix until regenerated. Carry either a + template-regeneration commit or a tracked follow-up issue, and **say which in the PR**. + +## Required tests + +1. **Client-bundle presence — B3 (binding).** Build the client bundle and assert the defer island is + in it. **You must commit to a fixture location and add it in this PR** — there is no `vite build` + fixture under `packages/fresh/tests/` today. Either a fixture under `packages/fresh/tests/fixtures/` + with a `Deno.test` wrapper, or a scaffolded-app fixture under `packages/cli/e2e/`. This attacks the + exact evidence the issue reports: *"the generated client bundle contains none of `DeferComponent`, + `decideDeferClientAction`, or `partial-miss`."* +2. **`f-client-nav` across all four** `isPartialRequest` × `hasCachedData` combinations, pinning that + `partial-miss` **enables** client nav. This is the guard that would have caught defect (2). +3. **Island marker in server output**, reusing the JSX-tree harness + (`define-page/tests/search-params.test.tsx:90-134`). +4. **Partial-swap behaviour** for whichever D3 technique lands. + +Each must fail if its own defect returns. Where a shape matters more than a value, assert the shape — +the #1548 slice in this lane proved that a behaviour-only suite stays green through a silent +regression. + +## Gates — hardcoded, not conditional + +```bash +deno run --allow-read --allow-run .llm/tools/run-deno-check.ts --root packages/fresh --ext ts,tsx +deno run --allow-read --allow-run .llm/tools/run-deno-lint.ts --root packages/fresh --ext ts,tsx +deno run --allow-read --allow-run .llm/tools/run-deno-fmt.ts --root packages/fresh --ext ts,tsx +deno task --cwd packages/fresh test +deno task doc:lint --root packages/fresh --pretty +``` + +`arch:check` (`deno.json:156`) **does not cover `packages/fresh`** (verified). Run `quality:gate` +**and** an explicit target quality scan over `packages/fresh/src`, and state in the PR that the +package-quality verdict rests on the explicit scan. If you touch the scaffold template, add the +`packages/cli` scoped wrappers and expect `scaffold-static` to exercise it in CI. + +Never a bare `deno test ` — it omits `--allow-env` and exits 1 on `NotCapable`. + +## PR contract — read this carefully + +PR **#1558** exists. Its body already carries **`Refs #1459`, deliberately not a closing keyword**, +because the client-bundle *navigation* criterion is split to **#1557**. **Do not change `Refs` to +`Closes`.** If you believe your work fully resolves #1459 including browser-navigation proof, say so +in your report and let the orchestrator decide — do not decide it in the body. + +Do **not** emit an `acceptance-evidence` block with an empty entry list; the mirror's parser throws +on it (#1561). If there is nothing to map, omit the block and say why. + +Commit per slice, push by explicit refspec, post `[PHASE: IMPL]` with commit hash and **pasted real +gate output**, and move the label from `status:plan-eval` to `status:impl`. + +## Reporting contract + +Report what changed and where; the concrete specifier you chose and the fixture location you +committed to; the exact test names and what each catches; verbatim gate output; and **anything you +could not do, could not verify, or that surprised you**. If a gate goes red, report the red with its +output — do not work around it silently. + +Do **not** flip the PR to ready (that fires the automatic IMPL-EVAL, which is the orchestrator's +trigger) and do **not** merge. diff --git a/.llm/runs/release-0.0.6-features--orchestration/slices/plan-1459.md b/.llm/runs/release-0.0.6-features--orchestration/slices/plan-1459.md new file mode 100644 index 0000000000..5cc3070d61 --- /dev/null +++ b/.llm/runs/release-0.0.6-features--orchestration/slices/plan-1459.md @@ -0,0 +1,258 @@ +# Plan — #1459 `DeferComponent` is not hydrated, so partial-miss regions never refresh + +Lane: 0.0.6 runtime reopen. Control PR #1555. Research: `slices/research-1459.md`. +Branch: `fix/1459-defer-island-hydration`. **Goes to PLAN-EVAL before implementation** via the +`openhands` + `status:plan-eval` label pair. + +## What the research changed + +The issue frames this as "the component is not hydrated". The actual finding is narrower and harder: +**`DeferIsland.tsx` is not an island at all** — there is no island registration for it anywhere +(`DeferIsland.tsx:111` is a *named* export; the scaffold calls `fresh()` with no options at +`vite.config.ts.template:41`; `createNetScriptVitePlugin` has zero island logic across all 438 lines). +Islands in this repo reach the client by **directory convention** or by being **copied into the +consumer app** (`fresh-ui`), and the one JSR-shipped island that works — `QueryIsland` — works only +because the *consumer's own* island file imports it (`query-island.tsx:26-36`). + +And a **second, independent defect** sits in the same component: `DeferIsland.tsx:222` sets +`f-client-nav={!(isPartialRequest && !hasCachedData)}`, whose condition is **exactly** `partial-miss`. +So in precisely the failing case, `f-client-nav` is `false` while `f-partial` is set (`:221`). +Hydration alone may therefore still produce a full document navigation instead of the `/partials/**` +request. These two are **technically inseparable**: fixing hydration without this leaves the issue's +observable symptom unfixed. + +## Scope decision — split, and move one criterion with a written reason + +The issue's acceptance asks for a regression test that *"build[s] the Fresh client bundle, navigate[s] +into a page with a cache-miss deferred layer, assert[s] the partial endpoint is requested, and +assert[s] the named boundary swaps exactly once."* + +This repo has **no browser driver** (Playwright appears only in docs and in the CLI *scaffolding* for +consumer projects) and **no gate that builds or inspects a client bundle**. That criterion therefore +needs **net-new test capability**, which is its own piece of work. + +**Decision: it moves to a follow-up issue with a written reason (#1090 pattern), and this PR does not +tick it.** A p1 runtime blocker — a documented pattern that paints a skeleton which never resolves — +should not wait on net-new browser-test infrastructure. Per the honesty rule, the criterion travels +with a reason rather than being ticked on weaker evidence. + +What this PR **does** prove is stated below and is deliberately not dressed up as the missing test. + +## Locked decisions + +- **D1 — the island must reach the client without depending on a consumer-owned file.** Preference + order, to be settled by the implementer against verified feasibility: + 1. register the island from `createNetScriptVitePlugin` (the only option requiring no consumer + file) — **feasibility is unverified**; the research could not confirm `@fresh/plugin-vite` + supports registering an island from a JSR specifier, and proved only that *this repo passes no + such option*. **Verify this first.** + 2. if (1) is not supported, the `fresh-ui` **copy-mode** precedent (`registry.generated.ts:334-340`). + A consumer-side shim that a consumer can delete — silently reintroducing the bug — is the **least** + preferred and must be argued for if chosen. +- **D2 — fix `f-client-nav` (`DeferIsland.tsx:222`).** Its current condition disables client nav in + exactly the `partial-miss` case it needs to be enabled for. **Verify Fresh's actual + `f-client-nav`/`f-partial` form-interception semantics before changing it** — the research flagged + this as inference, not fact. If Fresh does intercept `f-partial` regardless of `f-client-nav`, say + so and leave the line alone with evidence. +- **D3 — address the partial-swap case.** The coordinator form renders as a **sibling outside** the + `` (`DeferPage.tsx:257-275`), so a region swap leaves the form's DOM in place while a + page-level swap re-renders an inert form. The acceptance explicitly requires the coordinator to work + *after a page partial swap*; a fix that only works on first paint does not satisfy it. +- **D4 — no change to `decideDeferClientAction`'s policy logic** (`policy.ts:177-208`). It is correct + and already unit-covered; it is simply never executed. If the fix requires changing it, stop and + report. +- **D5 — out of scope:** the `streams`/`shouldStream` sibling path (`runtime/mod.tsx:178`), the dead + `debug` prop (`DeferIsland.tsx:54`), and #1457/#1548's surfaces. + +## Tests — what this PR can honestly prove + +1. **Island marker in server output.** Reuse the existing JSX-tree harness + (`define-page/tests/search-params.test.tsx:93-99,115-134`) to assert the rendered tree carries + whatever marker the chosen D1 mechanism produces. Fails if registration regresses. +2. **`f-client-nav` condition** (D2) asserted directly for the four combinations of + `isPartialRequest` × `hasCachedData`, pinning that `partial-miss` enables client nav. This is the + guard that would have caught the second defect. +3. **`buildDeferFormState` / decision plumbing** extended so `partial-miss → submit` is exercised + with the real inputs `DeferPage` passes (`DeferPage.tsx:267-268`), not synthetic ones. +4. If D1 lands via the Vite plugin, a unit test that the plugin **emits the island registration**. + +**None of these prove the client bundle contains the island or that a browser issues the partial +request.** That is the moved criterion. Say so plainly in the PR body; do not let tests 1–4 be read as +satisfying it. + +## Gates + +Scoped check/lint/fmt over `packages/fresh`, `deno task quality:gate` (**verify it covers +`packages/fresh`; it demonstrably omits several packages — #1542**), `deno task --cwd packages/fresh +test`, `deno task doc:lint`. If D1 changes the CLI registry or scaffold template, add the +`packages/cli` scoped wrappers and expect `scaffold-static` to exercise it in CI. + +## Risks + +- **R1** — D1's preferred option has **unverified feasibility**. If it fails, the fallback changes the + distribution model (copy-mode), which is a materially different PR. That branch point is why this + plan goes to PLAN-EVAL. +- **R2** — the `f-client-nav` claim is inference. Acting on it without verifying Fresh's semantics + risks changing a correct line. +- **R3** — copy-mode (D1.2) means framework fixes stop reaching already-generated apps; that is a + doctrine-relevant trade, not just an implementation detail. + +## Acceptance mapping (#1459) + +| Criterion | This PR | Evidence | +| --- | --- | --- | +| Coordinator registered/hydrated as a real Fresh island, including after a partial swap | **yes** (D1 + D3) | server-output marker test; plugin-emission test | +| A cache miss triggers the configured partial request and the region settles | **partially** — mechanism fixed (D2), not observed in a browser | `f-client-nav` condition tests | +| Regression test builds the client bundle, navigates, asserts the partial request and a single swap | **no — moved to a follow-up with a written reason** | needs net-new browser/bundle capability this repo lacks | + +--- + +# Plan v2 — revised after PLAN-EVAL `FAIL_PLAN` + +The evaluator verified the second defect **against Fresh core source** rather than leaving it as my +inference: `client.ts:256` gates form submission on `checkClientNavEnabled(el)`, and `shared.ts` +confirms `CLIENT_NAV_ATTR = "f-client-nav"`. So `f-client-nav={false}` in the `partial-miss` case +**does** suppress the interception — the second defect is now **fact, not inference**, and R2 is +closed. It also confirmed the client refresh mechanism is form/anchor interception only: **no +background poll, no auto-refresh timer.** Nothing else will save a missed region. + +Amendments, in the order the verdict raised them. + +## A1 — D1 feasibility is RESOLVED, not punted + +I read `@fresh/plugin-vite` v1.0.8 from the JSR cache. It exposes a public config option whose +documented purpose is exactly this case: + +```ts +/** + * Treat these specifiers as island files. This is used to declare + * islands from remote packages. + */ +islandSpecifiers?: string[]; +``` + +(`https://jsr.io/@fresh/plugin-vite/1.0.8/src/utils.ts`, cached; consumed in the plugin's `options()` +hook, which adds each specifier to `islands` and, in non-dev builds, emits a +`fresh-island::` input entry.) + +**D1 is therefore LOCKED to the specifier route** — no copy-mode, no consumer-owned shim. The +scaffold's `vite.config.ts.template:41` currently calls `fresh()` with **no options**; it must pass +`islandSpecifiers` naming the defer island's specifier. + +**A1a — migration is real and must be named (verdict item 6).** Because `islandSpecifiers` is a +`fresh()` option and not a Vite-level setting, `createNetScriptVitePlugin` cannot inject it by +merging Vite config. It is a **scaffold-template change**, so **already-generated apps do not get the +fix until regenerated**. The PR must state this plainly and either carry a CLI-registry/template +commit or file a tracked migration follow-up. It must not be left implicit. + +## A2 — D3 picks a technique + +**Locked: move the coordinator form *inside* the region's ``** (`DeferPage.tsx:257-275` +currently renders it as a sibling outside). That makes a region swap re-render the form, which is the +behaviour the acceptance requires, and it is the minimal structural change. + +**Fallback if that breaks the fallback-render contract:** give the form a stable `key` and force a +remount. `SlotRef` is not pursued. Whichever lands must be stated in the PR with the reason. + +## A3 — D5 call sites enumerated, not assumed + +Every non-test reference to `DeferComponent` in `packages/`: + +- `DeferIsland.tsx:21,111,122` — its own declaration. +- `DeferPage.tsx:8` (import) and `:263` (the **only** render site). + +There is exactly **one** render site. `DeferPage` is reached only from +`builders/define-page/runtime/mod.tsx`, where `shouldDefer = !shouldStream && !!descriptor.config.partial +&& (descriptor.config.delivery ?? 'defer') === 'defer'` (`runtime/mod.tsx:178`). So the streaming path +provably never renders `DeferComponent`, and D5's exclusion is **verified rather than asserted**. + +## A4 — the test commitment, and a correction to the verdict + +The verdict proposed `probe-project-boundary-dev.ts:43` + `FetchHttpAdapter` as an +already-available, non-net-new way to drive the partial request end-to-end. **I checked it, and it +does not do that.** That probe boots a Fresh dev server and issues +`fetch('http://127.0.0.1:${port}/')` in a liveness loop (`:41-52`). There is no browser and no JS +execution, so it can prove the partial **endpoint answers** but cannot prove the **client issues the +request** — and "the client never issues the request" is the entire defect. A server-side fetch +firing the request would be evidence of nothing. + +**What this PR commits to instead**, which is stronger than v1's server-only tests and cheaper than a +browser: + +- **Build the client bundle and assert the defer island is present in it.** With D1 locked to + `islandSpecifiers`, the non-dev build emits a `fresh-island::` entry, so this is directly + checkable. It is modest net-new capability (a `vite build` plus an assertion — **not** a browser + driver), and it attacks the exact evidence the issue reports: *"the generated client bundle + contains none of `DeferComponent`, `decideDeferClientAction`, or `partial-miss`."* +- Plus the v1 tests: `f-client-nav` condition across all four `isPartialRequest` × `hasCachedData` + combinations, and the island-marker assertion in server output. + +**Still moved to #1557:** browser-driven navigation asserting the `/partials/**` request fires and the +boundary swaps exactly once. That needs a real browser and stays out. + +## A5 — closing keyword (verdict item 5): already corrected + +PR #1558's body carried `Closes #1459` while this plan moves one of that issue's criteria — a +self-contradiction, and the #1092 precedent. **Fixed:** the body now reads `Refs #1459` with the +reason stated. The closing keyword goes on whichever PR truthfully completes the issue. + +## A6 — gate commands hardcoded (verdict item 7) + +`arch:check` (`deno.json:156`) does not cover every package, so the gate list is fixed rather than +conditional: run `deno task --cwd packages/fresh test`, the scoped check/lint/fmt wrappers over +`packages/fresh`, **and** an explicit target quality scan over `packages/fresh/src`. If the D1 +template change lands, add the `packages/cli` scoped wrappers and expect `scaffold-static` to +exercise it. + +## Revised acceptance mapping (#1459) + +| Criterion | This PR | Evidence | +| --- | --- | --- | +| Coordinator registered/hydrated as a real Fresh island, incl. after a partial swap | **yes** | `islandSpecifiers` registration + client-bundle presence assertion + form moved inside the `` | +| A cache miss triggers the configured partial request and the region settles | **mechanism proven, not observed in a browser** | `f-client-nav` corrected against verified Fresh semantics (`client.ts:256`) | +| Regression test builds the client bundle, navigates, asserts the request and a single swap | **bundle half yes; navigation half moved to #1557** | stated in the PR body without overclaiming | + +--- + +## PLAN-EVAL v2 result — PASS (recorded with an extraction discrepancy) + +Run `31593309658`, MiniMax M3, on plan v2 commit `63ae41bb4`. + +**The verdict is PASS.** The summary body states `Verdict: **PASS**` and ends with +`## OPENHANDS_VERDICT: PASS`. The machine marker nevertheless recorded +`"verdict":"NONE","verdict_source":"none"` — the token was emitted as a **markdown heading**, so the +extractor did not match it. Filed as **#1563**. + +**Not re-run.** Re-dispatching an evaluator purely to obtain a parseable token would be duplicate +spend of exactly the kind the phase-eval policy forbids, to work around a formatting bug rather than +an evaluation failure. The body verdict is accepted and the discrepancy is recorded instead. + +### What it independently verified + +- A1's mechanism, the streaming exclusion (A3), the sibling-form structure (A2), the `f-client-nav` + condition, and the `arch:check` omission of `packages/fresh` — all confirmed against the head SHA. +- PR #1558 correctly carries `Refs #1459`, not a closing keyword. + +### Amendments to fold into implementation + +**B1 — version citation corrected (my error).** Plan v2 cited `@fresh/plugin-vite@1.0.8`, read from +the local Deno cache. The scaffold actually pins **`jsr:@fresh/plugin-vite@^1.1.2`**. The evaluator +verified `islandSpecifiers?: string[]` exists in **1.1.2** with identical JSDoc +(`https://jsr.io/@fresh/plugin-vite/1.1.2/src/utils.ts:59-63`) and confirmed the mechanism at +`src/mod.ts:234-237` (`fConfig.islandSpecifiers.set(spec, name)`). **The D1 conclusion stands; the +citation was to the wrong version.** The implementer must check any 1.0.8-specific assumption against +1.1.2. + +**B2 — the specifier must be created, not just named.** `packages/fresh/deno.json` has **no** +sub-export for the defer island today. A1 therefore requires **(a)** adding the sub-export and +**(b)** naming that specifier in `vite.config.ts.template`'s `fresh({ islandSpecifiers: [...] })`. +This is an implementer decision, but it is a required step, not an optional detail. + +**B3 — the bundle fixture needs a committed location.** A4 says "build the client bundle and assert +the island is present" without saying where the fixture lives. There is **no `vite build` fixture +under `packages/fresh/tests/` today**. The implementer must commit to a location — a fixture under +`packages/fresh/tests/fixtures/` with a `Deno.test` wrapper, or a scaffolded-app fixture under +`packages/cli/e2e/` — and add it **in the same PR**, not defer it. + +**B4 — migration accepted as scoped.** Already-generated apps do not auto-migrate; carrying either a +template-regeneration commit or a tracked migration issue is acceptable. diff --git a/.llm/runs/release-0.0.6-features--orchestration/slices/research-1459.md b/.llm/runs/release-0.0.6-features--orchestration/slices/research-1459.md new file mode 100644 index 0000000000..8379c20733 --- /dev/null +++ b/.llm/runs/release-0.0.6-features--orchestration/slices/research-1459.md @@ -0,0 +1,121 @@ +# Research — #1459 `DeferComponent` is not hydrated + +Delegated read-only sub-agent (Claude · Opus 5, `drift.md` D-1), bounded brief — 27 tool calls, +~3 min, within budget. Static analysis only; nothing built, run, or reproduced. + +## The reported defect is confirmed, and its cause is simpler than "hydration is broken" + +**`DeferIsland.tsx` is not an island at all.** There is no island registration for it anywhere in the +repo. + +- It is a **named** export, not a default export + (`packages/fresh/src/application/defer/DeferIsland.tsx:111`), re-exported as an ordinary module + symbol (`defer/mod.ts:12`, reachable as `@netscript/fresh/defer`). +- **The mechanism in this repo is directory convention**, resolved by the stock Fresh Vite plugin. + The scaffold calls `fresh()` with **no options** + (`packages/cli/src/kernel/assets/app/vite.config.ts.template:41`), and `createNetScriptVitePlugin` + contains **zero** island logic — no `islands` match across all 438 lines of + `packages/fresh/src/application/vite/vite.ts`. `define-fresh-app.ts:121-135` registers fs-routes + only and never passes a `loadIsland`. +- Every island that actually works in a scaffolded app lives in a **scanned directory**: the app + template's `(_islands)/` folders, and `@netscript/fresh-ui` islands, which are **copied into the + consumer app** (`packages/fresh-ui/registry/islands/`, registered as copy templates at + `registry.generated.ts:334-340`) rather than imported from the package. +- The contrast that proves the pattern: `QueryIsland` + (`packages/fresh/src/application/query/query-island.tsx:39`) ships from the same JSR package and + **does** reach the client — because its documented contract is that the **consumer's own** island + file imports it (`:26-36`), giving it a scanned entry point. `DeferComponent` has no such entry. + +So the component is reached **only** through a server-render path +(`DeferPage.tsx:8` ← `builders/define-page/runtime/mod.tsx:4`), and nothing in a scanned directory +transitively imports it. Both candidate causes hold and compound. + +## A second, independent defect inside the same issue + +`DeferIsland.tsx:222` sets: + +```tsx +f-client-nav={!(isPartialRequest && !hasCachedData)} +``` + +`isPartialRequest && !hasCachedData` **is exactly the `partial-miss` condition**. So in precisely the +case this issue is about, `f-client-nav` is `false` while `f-partial` is set (`:221`). If Fresh only +intercepts `f-partial` on forms under active client navigation, `requestSubmit()` would trigger a +**full document navigation**, not the `/partials/**` request the issue requires. + +**This is not verified** — Fresh core's form/partial interception was not read, so it is inference +from the attribute values. But it means **hydration alone may not fix the issue**, and any fix must +test this line rather than assume it. + +## Why no existing test catches this + +`decideDeferClientAction` (`policy.ts:177`, `partial-miss` branch at `:178-182`) has **exactly one +caller** — the client effect at `DeferIsland.tsx:188`. There is no server-side caller. In the shipped +product it is therefore **never executed at runtime on either side**; its unit coverage is the only +thing exercising it. + +The input plumbing is correct end-to-end (`runtime/mod.tsx:220` → `DeferPage.tsx:170,267-268`), which +is why this looks healthy from the server side. `DeferIsland.test.ts:1` imports only +`buildDeferFormState` and `sanitizeDeferSearchParams` and never renders the component. + +## Partial-swap behaviour + +From `DeferPage.tsx:257-275`: the coordinator form is rendered **as a sibling outside** the +`` — `
`. + +- A swap of the region's own partial replaces the content but **leaves the form's DOM node in + place**, so even a working island would not naturally re-run its effect (deps at `:201-214` are all + props). +- A **page-level** partial swap re-renders the whole `
` including a freshly server-rendered, + inert form — with no island marker, Fresh has nothing to hydrate into it. + +Nothing in the defer code addresses island persistence or re-hydration across `` swaps: no +`key`, no persistence attribute, no remount handling. The docs describe the transport and make no +persistence claim (`docs/site/web-layer/defer-streaming-ui.md:238-245`). + +## The regression test the issue asks for requires net-new capability + +The issue's acceptance says a test should *"build the Fresh client bundle, navigate into a page with +a cache-miss deferred layer, assert the partial endpoint is requested, and assert the named boundary +swaps exactly once."* + +What exists: + +- Pure-function unit tests (`DeferIsland.test.ts`) — never render the component. +- JSX-tree prop assertions (`define-page/tests/search-params.test.tsx:93-99,115-134`) — could assert + an island marker in **server** output, but cannot prove a client bundle. +- A live Fresh dev server over HTTP (`probe-project-boundary-dev.ts:43` with `FetchHttpAdapter`) — + the closest existing "drive a real app". + +What does **not** exist: + +- **No browser driver.** Playwright appears only in docs and in the CLI *scaffolding* Playwright + config for consumer projects (`init-agent.ts`) — not test infra this repo runs. +- **No gate builds or inspects a client bundle** anywhere in `packages/cli/e2e`. + +So the acceptance's regression test needs net-new capability at whichever level it targets. + +## Options (research made no recommendation) + +| Option | Trade-off | +| --- | --- | +| Consumer-side island shim from the scaffold (`routes/(_islands)/DeferIsland.tsx` re-exporting `DeferComponent`) | matches the `QueryIsland`/`fresh-ui` precedent, but every existing scaffolded app needs regeneration, and a consumer who deletes the file silently reintroduces the bug | +| Copy-mode distribution like `fresh-ui` (`registry.generated.ts`) | consistent with `ui:add`, but forks the island per project so framework fixes stop reaching existing apps | +| Declare the island from `createNetScriptVitePlugin` | the only option needing no consumer file — but **feasibility unconfirmed**, depends on a `@fresh/plugin-vite` capability not verified | +| Drop the island; resolve server-side on `ctx.isPartial && !component` | removes the hydration dependency and the `f-client-nav` hazard, but collapses the defer model — the policy branches (`policy.ts:184-208`) have no server equivalent | +| Inline `