Yuki Takei 2 недель назад
Родитель
Сommit
685c4db9bb

+ 0 - 427
.kiro/specs/optimise-deps-for-prod-with-turbo-prune/design.md

@@ -1,427 +0,0 @@
-# Design Document: optimise-deps-for-prod-with-turbo-prune
-
-## Overview
-
-The GROWI production Docker build uses a multi-stage Dockerfile: `pruner` → `deps` → `builder` → `release`. The `pruner` stage already runs `turbo prune @growi/app @growi/pdf-converter --docker` to create a minimal monorepo subset. However, the `builder` stage still calls `assemble-prod.sh`, which contains two fragile post-build symlink-rewriting steps using `sed` and `find`:
-
-- **Step [1b]**: Rewrites `apps/app/node_modules/` top-level symlinks from the workspace-root `.pnpm/` path (an artifact of `pnpm deploy --legacy`) to local `apps/app/node_modules/.pnpm/` paths.
-- **Step [2]**: Rewrites `.next/node_modules/` symlinks (generated by Turbopack) from `../../../../node_modules/.pnpm/` (workspace root in Docker) to `../../node_modules/.pnpm/` (`apps/app/node_modules/`).
-
-Both steps exist because of two coupled design decisions: (1) the `--legacy` flag in `pnpm deploy`, and (2) placing the deploy output at `apps/app/node_modules/` instead of the workspace root.
-
-This design removes both root causes: in pnpm v10, `pnpm deploy --legacy` already creates self-contained relative symlinks (eliminating step [1b]); staging the deploy output at workspace root instead of `apps/app/` means Turbopack's original symlink targets already resolve correctly (eliminating step [2]).
-
-**Purpose**: Eliminate fragile bash symlink manipulation from the production assembly pipeline, replacing it with a structure that exploits the self-contained properties of `turbo prune` + `pnpm deploy --legacy` in pnpm v10.
-
-**Users**: Release engineers and CI/CD pipelines maintaining the production Docker build.
-
-**Impact**: Modifies `apps/app/bin/assemble-prod.sh` (removes ~15 lines) and the `builder` stage artifact staging step in `apps/app/docker/Dockerfile`. No changes to source code, Next.js configuration, or dependency classifications.
-
-### Goals
-- Eliminate step [1b] and step [2] from `assemble-prod.sh` by fixing their root causes
-- Reduce `assemble-prod.sh` to two operations: remove `.next/cache` and remove `next.config.ts`
-- Maintain production server correctness (`GET /` → HTTP 200, zero `ERR_MODULE_NOT_FOUND`)
-- Preserve Docker layer caching behavior (deps layer cached when only source changes)
-
-### Non-Goals
-- Changes to dependency classification (`dependencies` vs `devDependencies`) — handled by `optimise-deps-for-prod` spec
-- Migration to Next.js standalone output mode
-- Changes to the `pruner` or `deps` Docker stages
-- Changes to the Express server, database migrations, or application logic
-
----
-
-## Requirements Traceability
-
-| Requirement | Summary | Components | Flows |
-|-------------|---------|------------|-------|
-| 1.1–1.4 | Eliminate symlink rewrite steps | `assemble-prod.sh` | Assembly Flow |
-| 2.1–2.4 | `pnpm deploy --prod --legacy` creates prod-only workspace-root `node_modules/` (self-contained in pnpm v10) | `assemble-prod.sh` | Assembly Flow |
-| 3.1–3.4 | Release artifact has `node_modules/` at workspace root; `apps/app/node_modules` is a symlink | `assemble-prod.sh`, Dockerfile staging | Assembly Flow, Release Image |
-| 4.1–4.5 | Production server starts, `GET /` returns 200, no broken symlinks | All components | Validation |
-| 5.1–5.5 | `pruner` stage unchanged; Docker layer caching preserved | Dockerfile | Build Flow |
-| 6.1–6.4 | All `.next/node_modules/` packages are in `dependencies`; dep rule from `optimise-deps-for-prod` remains valid | (inherited, no new code) | — |
-
----
-
-## Architecture
-
-### Existing Architecture Analysis
-
-The production assembly pipeline (current):
-
-```
-turbo run build --filter @growi/app
-  └─ Turbopack → apps/app/.next/
-       └─ .next/node_modules/<pkg> → ../../../../node_modules/.pnpm/<pkg>/...
-                                      (workspace root in Docker = /opt/)
-
-assemble-prod.sh (current):
-  [1]   pnpm deploy out --prod --legacy --filter @growi/app
-        └─ out/node_modules/<pkg> → ../../../node_modules/.pnpm/<pkg>/...  ← points to workspace root
-  [mv]  rm -rf apps/app/node_modules && mv out/node_modules apps/app/node_modules
-        └─ apps/app/node_modules/<pkg> → still points to workspace root ✗
-  [1b]  find + sed: rewrite apps/app/node_modules/ symlinks
-        └─ apps/app/node_modules/<pkg> → .pnpm/<pkg>/...  (local) ✓
-  [2]   find + sed: rewrite .next/node_modules/ symlinks
-        └─ ../../../../node_modules/.pnpm/ → ../../node_modules/.pnpm/
-        └─ .next/node_modules/<pkg> → ../../node_modules/.pnpm/...  (= apps/app/node_modules/) ✓
-  [3]   rm -rf .next/cache
-  [4]   rm -f next.config.ts
-```
-
-Root causes of steps [1b] and [2]:
-- **[1b] root cause**: Deploy output is placed at `apps/app/`, but in pnpm v10 `--legacy` already creates self-contained symlinks — step [1b] is no longer needed. (Historical root cause in pre-v10 pnpm: `--legacy` produced workspace-root-pointing symlinks requiring rewriting.)
-- **[2] root cause**: Deploy output is placed at `apps/app/`, but Turbopack builds `.next/node_modules/` symlinks relative to the workspace root (4 levels up)
-
-### Architecture Pattern & Boundary Map
-
-```mermaid
-graph TB
-    subgraph DockerBuilder
-        PrunedRoot[Pruned workspace root]
-        FullDepsNM[node_modules full deps]
-        Build[turbo run build]
-        NextOut[apps/app/.next node_modules symlinks to 4-levels-up]
-        Deploy[pnpm deploy out --prod --legacy]
-        ProdNM[out/node_modules prod-only self-contained]
-        Replace[rm node_modules mv out/node_modules node_modules]
-        Symlink[ln -sfn apps/app/node_modules]
-        Clean[rm cache next.config.ts]
-        Stage[Stage to tmp/release]
-    end
-
-    subgraph ReleaseImage
-        WsRoot[workspace root node_modules prod-only]
-        AppNext[apps/app/.next symlinks resolve naturally]
-        AppNMLink[apps/app/node_modules symlink to ws root]
-        Server[pnpm run server]
-    end
-
-    PrunedRoot --> FullDepsNM
-    FullDepsNM --> Build
-    Build --> NextOut
-    Build --> Deploy
-    Deploy --> ProdNM
-    ProdNM --> Replace
-    Replace --> Symlink
-    Symlink --> Clean
-    Clean --> Stage
-    Stage --> WsRoot
-    Stage --> AppNext
-    Stage --> AppNMLink
-    WsRoot --> AppNext
-    AppNMLink --> WsRoot
-    WsRoot --> Server
-```
-
-**Key decisions**:
-- Keep `--legacy`: in pnpm v10 it creates self-contained `out/node_modules/` with relative `.pnpm/` symlinks — no step [1b] needed. Removing `--legacy` would require `inject-workspace-packages=true` with no practical benefit.
-- Place deploy output at workspace root: Turbopack's `../../../../node_modules/.pnpm/` symlinks already point to workspace root — no step [2] needed
-- `apps/app/node_modules` → symlink `../../node_modules` for `migrate-mongo` script compatibility and Node.js `require()` traversal
-
-### Technology Stack
-
-| Layer | Choice | Role | Notes |
-|-------|--------|------|-------|
-| Build orchestration | Turborepo `turbo prune --docker` | Generates minimal monorepo subset for Docker | `pruner` stage — unchanged |
-| Package manager | pnpm v10+ | `pnpm deploy --prod --legacy` | Keeps `--legacy`; pnpm v10 produces self-contained symlinks regardless |
-| Build assembly | `assemble-prod.sh` | Simplified: deploy → stage (2 ops remain) | Removes steps [1b] and [2] |
-| Container | Docker BuildKit `COPY` | Copies symlinks intact | Requires BuildKit (already in use) |
-
----
-
-## System Flows
-
-### Assembly Flow: Current vs New
-
-```mermaid
-graph TB
-    subgraph Current
-        C1[pnpm deploy --prod --legacy]
-        C2[mv out/node_modules apps/app/node_modules]
-        C3[step 1b rewrite apps/app/node_modules symlinks]
-        C4[step 2 rewrite .next/node_modules symlinks]
-        C5[rm cache next.config.ts]
-        C1 --> C2 --> C3 --> C4 --> C5
-    end
-
-    subgraph New
-        N1[pnpm deploy --prod --legacy]
-        N2[rm node_modules mv out/node_modules node_modules]
-        N3[ln apps/app/node_modules to ws root]
-        N4[rm cache next.config.ts]
-        N1 --> N2 --> N3 --> N4
-    end
-```
-
-### Symlink Resolution in Release Image
-
-```mermaid
-graph LR
-    subgraph ReleaseImage at opt/growi
-        NM[node_modules from pnpm deploy]
-        PNPM[node_modules .pnpm react at 18.2.0]
-        AppNMLink[apps/app/node_modules symlink to dotdot/dotdot/node_modules]
-        NextNM[apps/app/.next/node_modules react-xxx symlink to 4-levels-up]
-        MigrateScript[migrate script node_modules/migrate-mongo]
-    end
-
-    NextNM -->|../../../../node_modules/.pnpm/| PNPM
-    AppNMLink -->|../../node_modules/| NM
-    NM --> PNPM
-    MigrateScript -->|via apps/app/node_modules symlink| NM
-```
-
----
-
-## Components and Interfaces
-
-| Component | Domain | Intent | Req Coverage | Key Dependencies | Contracts |
-|-----------|--------|--------|--------------|------------------|-----------|
-| `assemble-prod.sh` | Build Assembly | Assemble prod artifact: deploy prod deps, stage symlinks, clean | 1.1–1.4, 2.1–2.4, 3.1–3.3 | pnpm, filesystem | Batch |
-| Dockerfile `builder` staging step | Container Build | Copy release artifact to `/tmp/release/` with correct structure | 3.1, 3.3, 5.1–5.3 | Docker BuildKit COPY | Batch |
-| Release image structure | Container Runtime | `node_modules/` at workspace root; `apps/app/node_modules` symlink | 3.2, 3.4, 4.1–4.4 | `assemble-prod.sh` output | State |
-| `reusable-app-prod.yml` archive step | CI Pipeline | Archive workspace-root `node_modules/` + `apps/app/node_modules` symlink for `launch-prod` and Playwright jobs | 4.3, 4.5 | `assemble-prod.sh` output, GitHub Actions `tar` | Batch |
-
-### Build Assembly
-
-#### `apps/app/bin/assemble-prod.sh`
-
-| Field | Detail |
-|-------|--------|
-| Intent | Produce a production-ready artifact by deploying prod-only deps to workspace root and creating `apps/app/node_modules` compatibility symlink |
-| Requirements | 1.1, 1.2, 1.3, 1.4, 2.1, 2.2, 2.3, 2.4, 3.1, 3.2, 3.3 |
-
-**Responsibilities & Constraints**
-- Run from workspace root (same CWD as current usage)
-- Deploy production dependencies using `pnpm deploy out --prod --legacy --filter @growi/app`
-- Replace workspace root `node_modules/` (full deps) with deploy output (prod-only)
-- Create `apps/app/node_modules` as a symlink to `../../node_modules` for migration script and Node.js resolution compatibility
-- Remove `.next/cache` to reduce release image size
-- Remove `next.config.ts` to prevent Next.js from attempting TypeScript install at server startup
-
-**Constraints**:
-- Must run AFTER `pnpm deploy` output is created (pnpm requires workspace root `node_modules/` during deploy)
-- Must run AFTER `turbo run build` (`.next/` must exist for cache removal)
-- `rm -rf node_modules` destroys the workspace root's full-deps `node_modules/`; developers running this locally must run `pnpm install` to restore the development environment
-
-**Contracts**: Batch [x]
-
-##### Batch / Job Contract
-- Trigger: Called by Dockerfile `builder` stage via `RUN bash apps/app/bin/assemble-prod.sh`; also callable locally
-- Input: Workspace root with `node_modules/` (full deps) and `apps/app/.next/` (built)
-- Output:
-  - `node_modules/` at workspace root (prod-only, self-contained relative symlinks)
-  - `apps/app/node_modules` → symlink `../../node_modules`
-  - `apps/app/.next/` without `.next/cache/`
-  - `apps/app/next.config.ts` removed
-- Idempotency: Re-runnable; `rm -rf out` at start cleans previous output
-
-**Implementation Notes**
-- Integration: Keeps `pnpm deploy out --prod --legacy` and changes `mv out/node_modules apps/app/node_modules` to `rm -rf node_modules && mv out/node_modules node_modules && ln -sfn ../../node_modules apps/app/node_modules`; removes step [1b] and step [2] bash blocks entirely
-- Validation: Verify that `out/node_modules/react` symlink target starts with `.pnpm/` (not `../../../node_modules/.pnpm/`) after running `pnpm deploy out --prod --legacy --filter @growi/app` — confirmed in pnpm v10.32.1
-- Risks: pnpm version may affect `--legacy` behavior — pnpm v9 with `--legacy` produced workspace-root-pointing symlinks (step [1b] was needed); pnpm v10 does not. If downgrading pnpm below v10, step [1b] may need to be reinstated.
-
----
-
-#### Dockerfile `builder` stage — artifact staging step
-
-| Field | Detail |
-|-------|--------|
-| Intent | Copy the production artifact (workspace root `node_modules/` + `apps/app/` contents) into `/tmp/release/` for the `COPY --from=builder` instruction in the `release` stage |
-| Requirements | 3.1, 3.3, 5.1, 5.2, 5.3 |
-
-**Responsibilities & Constraints**
-- Copy `node_modules/` from workspace root (prod-only, after `assemble-prod.sh`) to `/tmp/release/node_modules/`
-- Copy `apps/app/node_modules` **as a symlink** (not dereferenced) to `/tmp/release/apps/app/node_modules`
-- Use `cp -a` (includes `-d`/`-P` flag: no symlink dereferencing) for all copies
-- Docker BuildKit `COPY` preserves symlinks; verify with `RUN test -L apps/app/node_modules` if needed
-
-**Contracts**: Batch [x]
-
-##### Batch / Job Contract
-- Trigger: `RUN ...` step in Dockerfile `builder` stage, after `RUN bash apps/app/bin/assemble-prod.sh`
-- Input: `/opt/node_modules/` (prod-only) + `/opt/apps/app/` (with `.next/`, `dist/`, etc.) + `/opt/apps/app/node_modules` (symlink)
-- Output: `/tmp/release/` with:
-  ```
-  /tmp/release/
-  ├── package.json
-  ├── node_modules/              ← workspace-root prod node_modules
-  └── apps/app/
-      ├── .next/                 ← symlinks: ../../../../node_modules/.pnpm/ (resolvable in release)
-      ├── dist/
-      ├── config/
-      ├── public/
-      ├── resource/
-      ├── tmp/
-      ├── package.json
-      └── node_modules           ← symlink: ../../node_modules (preserved)
-  ```
-
-**Implementation Notes**
-- Integration: Change `cp -a apps/app/node_modules /tmp/release/apps/app/` to `cp -a node_modules /tmp/release/` and add `cp -a apps/app/node_modules /tmp/release/apps/app/` (symlink preserved by `-a`). Remove `apps/app/node_modules` from the existing long `cp -a` list that copies `.next`, `config`, `dist`, etc.
-- Validation: In Dockerfile, add `RUN test -L /tmp/release/apps/app/node_modules` to assert symlink preservation before the release stage
-- Risks: `cp -a` on a symlink-to-directory may dereference on some OS/busybox versions — test on the actual base image
-
----
-
-### Release Image Structure
-
-The `release` stage copies `/tmp/release/` to `${appDir}` (e.g. `/opt/growi/`):
-
-```
-/opt/growi/
-├── package.json
-├── node_modules/              ← prod-only (pnpm deploy output, self-contained)
-│   ├── .pnpm/
-│   │   ├── react@18.2.0/
-│   │   │   └── node_modules/react/  ← physical package files
-│   │   └── @growi+core@.../
-│   │       └── node_modules/@growi/core/  ← physically injected by pnpm deploy
-│   └── react                 ← symlink: .pnpm/react@18.2.0/node_modules/react  ✓
-└── apps/
-    └── app/
-        ├── .next/
-        │   └── node_modules/
-        │       └── react-xxx  ← symlink: ../../../../node_modules/.pnpm/react@18.2.0/...  ✓
-        ├── dist/
-        ├── node_modules       ← symlink: ../../node_modules → /opt/growi/node_modules/  ✓
-        └── package.json
-```
-
-**Resolution chains**:
-- `.next/node_modules/react-xxx` → `../../../../node_modules/.pnpm/react@18.2.0/node_modules/react` → `/opt/growi/node_modules/.pnpm/react@18.2.0/node_modules/react` ✓
-- `apps/app/node_modules/migrate-mongo` → via symlink `../../node_modules` → `/opt/growi/node_modules/migrate-mongo` ✓
-- Node.js `require('express')` from `apps/app/dist/server/app.js` → traverses to `apps/app/node_modules/` (symlink) → `/opt/growi/node_modules/express` ✓
-
----
-
-### CI Pipeline
-
-#### `.github/workflows/reusable-app-prod.yml` — `archive-prod-files` step
-
-| Field | Detail |
-|-------|--------|
-| Intent | Archive the complete production artifact (workspace-root `node_modules/` + `apps/app/` contents) so that `launch-prod` and `run-playwright` jobs can extract a self-contained release |
-| Requirements | 4.3, 4.5 |
-
-**Responsibilities & Constraints**
-- Include workspace-root `node_modules/` in the tarball (prod-only, from `assemble-prod.sh` output)
-- Include `apps/app/node_modules` as a symlink (preserved by `tar` default behaviour — no `--dereference`)
-- When extracted, the structure must satisfy: `apps/app/node_modules → ../../node_modules` resolves to the extracted workspace-root `node_modules/`
-
-**Implementation Notes**
-- Add `node_modules \` immediately before `apps/app/.next \` in the `tar -zcf` command
-- `tar` archives symlinks without following them by default; `apps/app/node_modules` is correctly preserved as a symlink pointing to `../../node_modules`
-- The extraction in both `launch-prod` (`tar -xf`) and `run-playwright` (`tar -xf ... -C /tmp/growi-prod`) restores the symlink; `../../node_modules` resolves correctly because workspace-root `node_modules/` is now also extracted
-
----
-
-## Testing Strategy
-
-### Production Server Startup Procedure (updated)
-
-The procedure from `optimise-deps-for-prod/design.md` is simplified. With the new approach, after `assemble-prod.sh`, the workspace root's `node_modules/` IS already the prod-only deploy output. The `mv node_modules node_modules.bak` step is **no longer needed**.
-
-**Step 1 — Clean build**
-```bash
-turbo run build --filter @growi/app
-```
-
-**Step 2 — Production assembly**
-```bash
-bash apps/app/bin/assemble-prod.sh
-```
-
-> **Note**: `assemble-prod.sh` now does `rm -rf node_modules && mv out/node_modules node_modules`. After this, the workspace root's `node_modules/` is prod-only. `apps/app/node_modules` is a symlink. Run `pnpm install` to restore the development environment after testing.
-
-**Step 2b — Restore `next.config.ts`**
-```bash
-git show HEAD:apps/app/next.config.ts > apps/app/next.config.ts
-```
-
-**Step 3 — Start production server** (from `apps/app/`)
-```bash
-cd apps/app && pnpm run server > /tmp/server.log 2>&1 &
-timeout 60 bash -c 'until grep -q "Express server is listening" /tmp/server.log; do sleep 2; done'
-```
-
-**Step 4 — Verify**
-```bash
-HTTP_CODE=$(curl -s -o /tmp/response.html -w "%{http_code}" http://localhost:3000/)
-echo "HTTP: $HTTP_CODE"  # → 200
-grep -c "ERR_MODULE_NOT_FOUND" /tmp/server.log  # → 0
-```
-
-**Step 5 — Stop and restore**
-```bash
-kill $(lsof -ti:3000)
-pnpm install  # restore full-deps node_modules for development
-```
-
-### Symlink Integrity Verification
-
-After `assemble-prod.sh`:
-```bash
-# Verify apps/app/node_modules is a symlink
-test -L apps/app/node_modules && echo "OK: symlink" || echo "FAIL: not a symlink"
-
-# Verify .next/node_modules symlinks resolve (no broken links)
-find apps/app/.next/node_modules -maxdepth 2 -type l | while read link; do
-  linkdir=$(dirname "$link"); target=$(readlink "$link")
-  resolved=$(cd "$linkdir" 2>/dev/null && realpath "$target" 2>/dev/null || echo "BROKEN")
-  [ "$resolved" = "BROKEN" ] && echo "BROKEN: $link"
-done
-# Expected: no output
-
-# Verify pnpm deploy without --legacy creates self-contained symlinks
-readlink out/node_modules/react 2>/dev/null  # Expected: .pnpm/react@.../node_modules/react (not ../../../...)
-```
-
-### Docker Build Verification
-
-After each change, verify Docker build produces a working release image:
-```bash
-docker build -f apps/app/docker/Dockerfile -t growi-test .
-docker run --rm growi-test ls /opt/growi/node_modules/.pnpm | head -5  # → packages listed
-docker run --rm growi-test test -L /opt/growi/apps/app/node_modules && echo "symlink OK"
-```
-
----
-
-## Migration Strategy
-
-The change is non-breaking and phased:
-
-```mermaid
-graph LR
-    P1[Phase 1 verify pnpm deploy without legacy creates self-contained symlinks]
-    P2[Phase 2 update assemble-prod.sh remove legacy rm step 1b step 2 add new mv and ln]
-    P3[Phase 3 update Dockerfile staging add node_modules at workspace root]
-    P4[Phase 4 validate Docker build and production server startup]
-
-    P1 --> P2 --> P3 --> P4
-```
-
-**Rollback**: Each phase modifies only `assemble-prod.sh` and/or the Dockerfile staging step. Rolling back is a targeted revert of those two files. The production build pipeline (turbo prune, deps install, turbo build) is unchanged.
-
-**Phase 1 gate**: `readlink out/node_modules/react` must start with `.pnpm/` after `pnpm deploy out --prod --legacy --filter @growi/app`. Verified in pnpm v10.32.1: result is `.pnpm/react@18.2.0/node_modules/react`. If upgrading/downgrading pnpm, re-verify this output.
-
----
-
-## Error Handling
-
-### Assembly Failures
-
-- **`pnpm deploy out --prod` fails**: The script exits with `set -euo pipefail`. Inspect pnpm output; check `pnpm-lock.yaml` is not dirty.
-- **`rm -rf node_modules` fails**: Unlikely; check filesystem permissions.
-- **`ln -sfn ../../node_modules apps/app/node_modules` fails**: Existing `apps/app/node_modules` is a real directory (from previous dev install). The `rm -rf apps/app/node_modules` step before `ln` handles this.
-- **Broken symlinks in release image**: Indicates a package from `.next/node_modules/` is missing from `dependencies` — classification regression from `optimise-deps-for-prod`. Run the devDependencies regression check:
-  ```bash
-  # verify no devDep appears in .next/node_modules/
-  ```
-
-### Production Server Failures
-- **`ERR_MODULE_NOT_FOUND`**: Package in `dependencies` was not included in `pnpm deploy` output. Check whether the package is listed in `dependencies` (not `devDependencies`) in `apps/app/package.json`.
-- **`TypeError: Cannot read properties of null (reading 'useContext')`**: Broken symlink in `node_modules/` (React resolved from wrong location). Verify `apps/app/node_modules` symlink integrity.

+ 0 - 101
.kiro/specs/optimise-deps-for-prod-with-turbo-prune/requirements.md

@@ -1,101 +0,0 @@
-# Requirements Document
-
-## Project Description (Input)
-optimise-deps-for-prod-with-turbo-prune
-
----
-
-## Introduction
-
-The GROWI monorepo's production Docker build already uses `turbo prune @growi/app --docker` in its `pruner` stage to create a minimal monorepo subset. However, the `builder` stage still calls `assemble-prod.sh`, which contains two complex, fragile symlink-rewriting steps:
-
-1. **Step [1b]**: Rewrites `apps/app/node_modules/` top-level symlinks from the workspace-root `.pnpm/` path (generated by `pnpm deploy --prod --legacy`) to the local `apps/app/node_modules/.pnpm/` path.
-2. **Step [2]**: Rewrites `.next/node_modules/` symlinks (generated by Turbopack during `next build`) from the pruned-root `.pnpm/` path to `apps/app/node_modules/.pnpm/`.
-
-These steps exist because `pnpm deploy --prod --legacy` creates top-level symlinks that point to the workspace-root `.pnpm/` store, and Turbopack creates `.next/node_modules/` symlinks pointing to the pruned-root `.pnpm/` store — neither of which exists in the release image.
-
-The key insight is that within a `turbo prune`-generated context, `pnpm install` creates a **self-contained** `node_modules/` where all top-level symlinks already point to the local `.pnpm/` store at the same workspace root. The `.next/node_modules/` symlinks generated by Turbopack during `next build` also point to that same workspace-root `.pnpm/`. If the release image preserves this relative directory structure (keeping `node_modules/` at the same level relative to `apps/app/`), **no symlink rewriting is needed**.
-
-This specification replaces the `pnpm deploy --prod` + symlink-rewrite assembly pattern with a `pnpm install --prod`-based approach that exploits the pruned monorepo context, eliminating the fragile bash symlink manipulation.
-
----
-
-## Requirements
-
-### Requirement 1: Eliminate symlink rewrite steps from the production assembly pipeline
-
-**Objective:** As a release engineer, I want the production assembly pipeline to not require any post-build symlink rewriting, so that the assembly script is simple, reliable, and not sensitive to changes in pnpm or Turborepo internal symlink structures.
-
-#### Acceptance Criteria
-
-1. The build system shall not contain any `sed`-based symlink path rewriting for `apps/app/node_modules/` or `apps/app/.next/node_modules/` in the production assembly process.
-2. When the production assembly completes, every symlink in `apps/app/.next/node_modules/` shall resolve to an existing file without any path rewriting step having been executed.
-3. When the production assembly completes, every top-level symlink in the production `node_modules/` directory shall resolve to an existing file without any path rewriting step having been executed.
-4. The production assembly script (`assemble-prod.sh`) shall be reduced to only: removing `.next/cache` and removing `next.config.ts`.
-
----
-
-### Requirement 2: Replace `pnpm deploy --prod` with `pnpm install --prod` in the pruned context
-
-**Objective:** As a release engineer, I want production dependency isolation to be achieved via `pnpm install --prod` within the `turbo prune`-generated workspace, so that the node_modules layout is naturally self-contained without requiring post-build fixups.
-
-#### Acceptance Criteria
-
-1. When the builder stage runs `pnpm install --prod --frozen-lockfile` after `next build`, the build system shall produce a `node_modules/` directory at the pruned workspace root that contains only production dependencies (those listed under `dependencies` in `apps/app/package.json`).
-2. After `pnpm install --prod`, the `node_modules/.pnpm/` store shall not contain any package that is listed exclusively in `devDependencies` in `apps/app/package.json`.
-3. The `node_modules/.pnpm/` store shall contain every package that appears in `apps/app/.next/node_modules/` after the production build.
-4. When the production `node_modules/` (pruned to prod-only) is placed at the workspace root level in the release image alongside `apps/app/`, all `.next/node_modules/` symlinks shall resolve correctly through the standard pnpm relative-path structure without modification.
-
----
-
-### Requirement 3: Restructure the release artifact to include `node_modules/` at the workspace root level
-
-**Objective:** As a release engineer, I want the release artifact to preserve the pruned monorepo's directory structure (with `node_modules/` at the same level as `apps/app/`), so that Turbopack-generated `.next/node_modules/` symlinks resolve naturally.
-
-#### Acceptance Criteria
-
-1. The builder stage shall stage the release artifact with `node_modules/` at the workspace root level (alongside `apps/app/`), not inside `apps/app/node_modules/`.
-2. When the release image is launched, `apps/app/.next/node_modules/<package>` symlinks (which reference `../../../../node_modules/.pnpm/<package>/...`) shall resolve to files within the staged `node_modules/.pnpm/` directory.
-3. The release image shall not require `apps/app/node_modules/` to exist as a separate directory; Node.js module resolution for the Express server shall be satisfied by the workspace-root `node_modules/`.
-4. If `apps/app/node_modules/` is required for `pnpm run server` to resolve `apps/app` local dependencies (e.g. for `migrate-mongo`), the build system shall provide a mechanism to satisfy this without duplicating the `.pnpm/` store.
-
----
-
-### Requirement 4: Maintain production server correctness
-
-**Objective:** As a release engineer, I want the production server to start and serve pages correctly after the assembly pipeline change, so that the simplification does not introduce any runtime regressions.
-
-#### Acceptance Criteria
-
-1. When the production server is started with `pnpm run server` in the release image, the build system shall not throw `ERR_MODULE_NOT_FOUND` or `Failed to load external module` errors.
-2. When a browser accesses the root page `/`, the GROWI server shall respond with HTTP 200 and render the page content without SSR errors in the server log.
-3. The GROWI server shall pass the existing `launch-prod` CI job (MongoDB 6.0 and 8.0) after this change.
-4. While the production server is running, zero broken symlinks shall exist in the `node_modules/` and `.next/node_modules/` directories (verified with workspace-root `node_modules` absent from the environment).
-5. The production server smoke test shall use `GET /` (not `/login`) as the verification URL, as `/login` returns HTTP 200 even when SSR is broken.
-
----
-
-### Requirement 5: Preserve compatibility with the existing `turbo prune` Docker multi-stage build
-
-**Objective:** As a release engineer, I want the new assembly approach to integrate cleanly into the existing Dockerfile's multi-stage structure (`pruner` → `deps` → `builder` → `release`), so that Docker layer caching and build efficiency are maintained.
-
-#### Acceptance Criteria
-
-1. The `pruner` stage (`turbo prune @growi/app @growi/pdf-converter --docker`) shall remain unchanged; the simplification applies only to the `builder` stage and the `release` staging step.
-2. When only source files (not `package.json` or lockfile) change, the Docker layer cache for the dependency installation step shall remain valid (i.e., `pnpm install` shall not re-run unnecessarily).
-3. The `builder` stage shall produce the pruned, prod-only `node_modules/` without requiring any external tools beyond `pnpm` and standard Unix utilities.
-4. The `assemble-prod.sh` script shall remain as a thin wrapper callable from the `builder` stage, containing only the operations that cannot be expressed as a `RUN` step in the Dockerfile (e.g., `next.config.ts` removal and cache cleanup).
-5. If `assemble-prod.sh` is reduced to only two operations (cache removal and `next.config.ts` removal), those operations shall be inlinable directly into the Dockerfile as `RUN` steps, eliminating the need for `assemble-prod.sh` entirely.
-
----
-
-### Requirement 6: Validate against the dep-classification baseline from `optimise-deps-for-prod`
-
-**Objective:** As a developer, I want the new assembly approach to be built on top of the correct `dependencies` vs `devDependencies` classification established in the `optimise-deps-for-prod` spec, so that the two workstreams are compatible and the correctness guarantees of that spec are preserved.
-
-#### Acceptance Criteria
-
-1. The build system shall verify that every package appearing in `apps/app/.next/node_modules/` after `next build` is listed under `dependencies` (not `devDependencies`) in `apps/app/package.json`.
-2. After `pnpm install --prod`, the `node_modules/.pnpm/` store shall contain all packages that were present in `.next/node_modules/` before the `--prod` pruning step.
-3. If any package appears in `.next/node_modules/` but is listed under `devDependencies`, the build system shall surface this as a detectable error (a broken symlink or `ERR_MODULE_NOT_FOUND`) that blocks the production server from starting.
-4. The dep-classification rule documented in `.kiro/steering/tech.md` (Turbopack Externalisation Rule) shall remain valid and apply to the new assembly approach: any package statically imported in SSR-executed code must be in `dependencies`.

+ 0 - 107
.kiro/specs/optimise-deps-for-prod-with-turbo-prune/research.md

@@ -1,107 +0,0 @@
-# Research: optimise-deps-for-prod-with-turbo-prune
-
-## Summary
-- Feature: `optimise-deps-for-prod-with-turbo-prune`
-- Discovery Scope: Extension (modifying existing build pipeline)
-- Key Findings:
-  1. **pnpm v10**: `pnpm deploy --prod --legacy` creates self-contained symlinks within the deploy output's local `.pnpm/` store (verified: `out/node_modules/react` → `.pnpm/react@18.2.0/node_modules/react`). The `--legacy` flag no longer causes workspace-root-pointing symlinks. Without `--legacy`, pnpm v10 requires `inject-workspace-packages=true` and also produces self-contained symlinks — but introduces an extra config dependency. Keeping `--legacy` is the simpler choice.
-  2. Turbopack-generated `.next/node_modules/` symlinks for non-scoped packages use `../../node_modules/.pnpm/` path (2 levels up from `.next/node_modules/` = `apps/app/node_modules/`). After step [2] rewrite: same result. If `node_modules/` is placed at workspace root (4 levels up from `.next/node_modules/` = original Turbopack output `../../../../node_modules/.pnpm/`), no rewrite is needed.
-  3. The existing Dockerfile already uses `turbo prune @growi/app @growi/pdf-converter --docker` in the `pruner` stage. The `assemble-prod.sh` runs inside the Docker `builder` stage after `turbo run build`. The workspace root in Docker is `$OPT_DIR` (e.g. `/opt/`).
-
-## Research Log
-
-### Topic: `.next/node_modules/` symlink path analysis
-- Context: Determining why step [2] exists and what its target resolves to
-- Findings:
-  - Examined actual `.next/node_modules/@codemirror/` in devcontainer
-  - Non-scoped packages: `../../node_modules/.pnpm/<pkg>/...` (2 levels up from `.next/node_modules/` = `apps/app/node_modules/`)
-  - Scoped packages (inside `@scope/`): `../../../node_modules/.pnpm/<pkg>/...` (3 levels up from `@scope/` = `apps/app/node_modules/`)
-  - These are AFTER step [2] which rewrote from `../../../../` to `../../`
-  - Original Turbopack output (pre-rewrite): `../../../../node_modules/.pnpm/` (4 levels up from `.next/node_modules/` = Docker workspace root `/opt/node_modules/`)
-- Implications: If workspace root's `node_modules/` contains prod deps, the original Turbopack symlinks resolve correctly WITHOUT any rewriting
-
-### Topic: `pnpm deploy` symlink behavior with and without `--legacy` (pnpm v10)
-- Context: Understanding why step [1b] rewrites `apps/app/node_modules/` symlinks
-- Findings:
-  - **pnpm v10 + `--legacy`**: produces a pnpm-native `.pnpm/` structure with self-contained relative symlinks. Verified in pnpm v10.32.1: `out/node_modules/react` → `.pnpm/react@18.2.0/node_modules/react`; `out/node_modules/@codemirror/state` → `../.pnpm/@codemirror+state@6.5.4/node_modules/@codemirror/state`. The `--legacy` flag now only bypasses the `inject-workspace-packages` gate introduced in pnpm v10; it does NOT produce hoisted/workspace-root-pointing symlinks.
-  - **pnpm v10 without `--legacy`**: fails with `ERR_PNPM_DEPLOY_NONINJECTED_WORKSPACE` unless `inject-workspace-packages=true` is set in `.npmrc`. When set, it also produces self-contained symlinks — identical output to `--legacy`. Introduces an extra config dependency with no practical benefit over keeping `--legacy`.
-  - **Legacy assumption (pre-pnpm v10)**: Earlier pnpm versions with `--legacy` created symlinks referencing the workspace-root `.pnpm/` store, which required step [1b] to rewrite them after `mv`. This assumption is no longer valid in pnpm v10.
-  - `pnpm deploy` (with or without `--legacy`) physically INJECTS workspace packages (copies, not symlinks) into the deploy output.
-- Implications: Step [1b] is no longer needed in pnpm v10 regardless of `--legacy`. The real root cause of step [1b] was placing the deploy output at `apps/app/` (not `--legacy` itself). Changing the placement to workspace root eliminates both step [1b] and step [2].
-
-### Topic: `apps/app/node_modules` compatibility symlink for migration scripts
-- Context: The `migrate` script in `apps/app/package.json` uses path `node_modules/migrate-mongo/bin/migrate-mongo` (relative to `apps/app/`)
-- Findings:
-  - With the new structure (`node_modules/` at workspace root), the direct path `node_modules/migrate-mongo/...` from `apps/app/` would fail
-  - Creating `apps/app/node_modules` as a symlink to `../../node_modules` (workspace root) resolves this
-  - Node.js module resolution also follows symlinks, so `require()` calls work correctly
-  - `cp -a` preserves symlinks (uses `-d` flag, no dereferencing), so Docker COPY will preserve it
-- Implications: A single `ln -sfn ../../node_modules apps/app/node_modules` step replaces the complex `pnpm deploy + mv + symlink rewrite` chain
-
-### Topic: `turbo prune --docker` output structure (context7 official docs)
-- Sources: Turborepo official documentation via context7 (/vercel/turborepo)
-- Findings:
-  - `turbo prune <app> --docker` creates:
-    - `out/json/` – package.json files only (for Docker layer caching)
-    - `out/full/` – full source code needed for the target
-    - `out/pnpm-lock.yaml` – pruned lockfile
-  - After `pnpm install --frozen-lockfile` on the pruned `out/json/`, `node_modules/` is created at the workspace root of the pruned context with self-contained symlinks
-  - Official examples use Next.js standalone output mode (`output: 'standalone'`) which bundles needed modules into `.next/standalone/`, avoiding symlink issues entirely. GROWI does NOT use standalone mode.
-- Implications: GROWI's existing `turbo prune` usage in the Dockerfile is correct. The remaining issue is the `assemble-prod.sh` post-build assembly.
-
-### Topic: Release image directory structure requirements
-- Context: Determining what directory structure the release image needs for symlinks to resolve
-- Findings:
-  - Current release image: `${appDir}/apps/app/.next/` + `${appDir}/apps/app/node_modules/` (from pnpm deploy)
-  - `.next/node_modules/` symlinks (after step [2]): `../../node_modules/.pnpm/` → `apps/app/node_modules/.pnpm/` ✓
-  - Proposed release image: `${appDir}/node_modules/` + `${appDir}/apps/app/.next/`
-  - `.next/node_modules/` symlinks (NO rewrite, original Turbopack output): `../../../../node_modules/.pnpm/` → `${appDir}/node_modules/.pnpm/` ✓
-- Implications: Placing `node_modules/` (from pnpm deploy output) at workspace root level in release image, instead of `apps/app/node_modules/`, eliminates step [2] entirely.
-
-## Architecture Pattern Evaluation
-
-| Option | Description | Strengths | Risks / Limitations | Notes |
-|--------|-------------|-----------|---------------------|-------|
-| A: Remove `--legacy` only | Drop `--legacy` from `pnpm deploy`, keep `apps/app/node_modules/` placement | Minimal change | Step [2] still required; pnpm v10 requires `inject-workspace-packages=true` | Partial improvement; adds config dependency |
-| B: `pnpm install --prod` post-build | After build, run `pnpm install --prod --frozen-lockfile` in pruned context | Conceptually clean | Workspace packages remain as symlinks (need `packages/` in release) | Requires larger release image |
-| C: Keep `--legacy` + workspace-root staging | Keep `--legacy`, place deploy output at workspace root, create `apps/app/node_modules` symlink | Eliminates BOTH step [1b] AND step [2]; workspace packages still injected (no `packages/` needed); no `.npmrc` changes | `--legacy` flag remains (cosmetically); pnpm v10 behavior verified | **Selected** |
-
-## Design Decisions
-
-### Decision: Keep `--legacy` in `pnpm deploy`; eliminate step [1b] by changing placement (not by removing `--legacy`)
-- Context: The original assumption was that `--legacy` caused workspace-root-pointing symlinks, requiring step [1b]. Verified in pnpm v10: `--legacy` produces self-contained `.pnpm/` symlinks — step [1b] is unnecessary regardless of `--legacy`.
-- Alternatives:
-  1. Remove `--legacy` — requires `inject-workspace-packages=true` in pnpm v10 (extra config); same symlink output
-  2. Keep `--legacy` — works in pnpm v10 without any `.npmrc` changes (selected)
-- Selected Approach: Keep `--legacy`; eliminate step [1b] by changing placement from `apps/app/` to workspace root
-- Rationale: The true root cause of step [1b] was placing the deploy output at `apps/app/` (misaligned with Turbopack's symlink base), not `--legacy` itself. Fixing placement eliminates both step [1b] and step [2] without requiring `.npmrc` changes.
-- Trade-offs: `--legacy` flag remains in the script. It is now a pnpm v10 gate-bypass, not a linker-mode selector.
-
-### Decision: Place deploy output at workspace root, not `apps/app/` (eliminates step [2])
-- Context: Turbopack generates `.next/node_modules/` symlinks pointing to `../../../../node_modules/.pnpm/` (workspace root in Docker). Step [2] rewrites these to point to `apps/app/node_modules/.pnpm/`.
-- Alternatives:
-  1. Keep `apps/app/node_modules/`, apply step [2] rewrite (current)
-  2. Place deploy output at workspace root, no rewrite needed (selected)
-- Selected Approach: `mv out/node_modules node_modules` (replace workspace root `node_modules/`) + `ln -sfn ../../node_modules apps/app/node_modules`
-- Rationale: Turbopack's original symlink targets (`../../../../node_modules/.pnpm/`) already point to workspace root. Preserving this structure means no rewriting.
-- Trade-offs: Release image now includes `node_modules/` at workspace root (alongside `apps/app/`). The `package.json` at workspace root is already copied in current staging, so minimal structural change.
-- Follow-up: Verify Docker `COPY` preserves `apps/app/node_modules` symlink correctly
-
-### Decision: Use `pnpm deploy --prod` (not `pnpm install --prod`)
-- Context: Requirements described `pnpm install --prod` as the mechanism. Design evaluated both options.
-- Alternatives:
-  1. `pnpm install --prod --frozen-lockfile` — modifies existing workspace node_modules
-  2. `pnpm deploy out --prod` — creates clean deploy output (selected)
-- Selected Approach: `pnpm deploy out --prod --legacy --filter @growi/app`
-- Rationale: `pnpm deploy` handles workspace package injection (physically copies `@growi/*` packages into deploy output, no `packages/` directory needed in release image). `pnpm install --prod` would leave `node_modules/@growi/core` as a symlink requiring `packages/core/` to exist in release.
-- Trade-offs: `pnpm deploy` takes slightly longer than `pnpm install --prod`. Both produce prod-only node_modules.
-
-## Risks & Mitigations
-- pnpm version compatibility: `--legacy` behavior changed between pnpm v9 and v10 (v9: hoisted symlinks; v10: self-contained). The implementation assumes pnpm v10+ behavior. — Mitigation: verify in CI with the same pnpm version as the Dockerfile; if downgrading pnpm, step [1b] may need to be reinstated.
-- `apps/app/node_modules` symlink + Docker COPY: Docker BuildKit COPY should preserve symlinks, but must be verified — Mitigation: add a verification step in CI that checks symlink integrity in release image
-- `rm -rf node_modules` in `assemble-prod.sh`: destroys workspace root `node_modules/` locally (dev workflow change) — Mitigation: document updated local testing procedure; developers must run `pnpm install` to restore after local testing
-
-## References
-- [turbo prune --docker official docs](https://github.com/vercel/turborepo/blob/main/apps/docs/content/docs/reference/prune.mdx) — `--docker` flag splits output into `json/` and `full/`
-- [Turborepo Docker guide](https://github.com/vercel/turborepo/blob/main/apps/docs/content/docs/guides/tools/docker.mdx) — multi-stage Dockerfile with prune
-- [pnpm deploy docs](https://pnpm.io/cli/deploy) — workspace package injection behavior

+ 0 - 22
.kiro/specs/optimise-deps-for-prod-with-turbo-prune/spec.json

@@ -1,22 +0,0 @@
-{
-  "feature_name": "optimise-deps-for-prod-with-turbo-prune",
-  "created_at": "2026-03-16T00:00:00.000Z",
-  "updated_at": "2026-03-16T00:00:00.000Z",
-  "language": "en",
-  "phase": "completed",
-  "approvals": {
-    "requirements": {
-      "generated": true,
-      "approved": true
-    },
-    "design": {
-      "generated": true,
-      "approved": true
-    },
-    "tasks": {
-      "generated": true,
-      "approved": true
-    }
-  },
-  "ready_for_implementation": true
-}

+ 0 - 92
.kiro/specs/optimise-deps-for-prod-with-turbo-prune/tasks.md

@@ -1,92 +0,0 @@
-# Implementation Plan
-
-## Task Overview
-
-| Phase | Major Task | Sub-tasks | Requirements |
-|-------|-----------|-----------|--------------|
-| 1 | Verify `pnpm deploy --legacy` symlink behavior in pnpm v10 | 1.1 | 2.1, 2.2, 2.3 |
-| 2 | Simplify `assemble-prod.sh` | 2.1, 2.2 | 1.1–1.4, 2.1, 2.4, 3.1–3.3, 5.4, 5.5 |
-| 3 | Update Dockerfile staging | — | 3.1, 3.3, 5.1–5.3 |
-| 3b | Update CI workflow archive step | — | 4.3, 4.5 |
-| 4 | Validate end-to-end | 4.1, 4.2, 4.3 | 1.1–1.4, 2.3, 2.4, 3.2, 3.4, 4.1–4.5, 6.1–6.4 |
-
----
-
-- [x] 1. Verify `pnpm deploy --legacy` creates self-contained symlinks in pnpm v10
-
-- [x] 1.1 Run `pnpm deploy out --prod --legacy --filter @growi/app` and inspect top-level symlinks in `out/node_modules/`
-  - From workspace root, run `rm -rf out && pnpm deploy out --prod --legacy --filter @growi/app`
-  - Inspect a non-scoped top-level symlink: `readlink out/node_modules/react` — expected result starts with `.pnpm/react@` (e.g. `.pnpm/react@18.2.0/node_modules/react`), NOT `../../../node_modules/.pnpm/`
-  - Inspect a scoped top-level symlink: `readlink out/node_modules/@codemirror/state` — expected result starts with `../.pnpm/@codemirror`
-  - If either symlink points to `../../../node_modules/.pnpm/` or `../../../../node_modules/.pnpm/`, pnpm version is pre-v10 and step [1b] must be retained; do not proceed with removal
-  - Verify `out/node_modules/.pnpm/` exists and contains physical package directories (not symlinks to the workspace-root `.pnpm/` store)
-  - Clean up: `rm -rf out`
-  - **Verified in pnpm v10.32.1**: `react` → `.pnpm/react@18.2.0/node_modules/react`; `@codemirror/state` → `../.pnpm/@codemirror+state@6.5.4/node_modules/@codemirror/state` ✓
-  - _Requirements: 2.1, 2.2, 2.3_
-
----
-
-- [x] 2. Simplify `assemble-prod.sh` by deleting both symlink-rewrite steps and restructuring node_modules placement
-
-- [x] 2.1 Delete the step [1b] bash block (keep `--legacy`)
-  - In `apps/app/bin/assemble-prod.sh`, keep `pnpm deploy out --prod --legacy --filter @growi/app` unchanged
-  - Delete the entire step [1b/4] block: the `echo "[1b/4]..."` line, the `find apps/app/node_modules -maxdepth 2 -type l | while read ...` loop, and the closing `echo "[1b/4] Done."` line
-  - In pnpm v10, `--legacy` already creates self-contained `.pnpm/` symlinks; step [1b] rewriting is no longer needed
-  - Update the progress step count in remaining echo messages to reflect removal of step [1b]
-  - _Requirements: 1.1, 1.3, 2.1_
-
-- [x] 2.2 Replace node_modules placement with workspace-root staging, add compatibility symlink, and remove step [2] bash block
-  - Replace `rm -rf apps/app/node_modules` + `mv out/node_modules apps/app/node_modules` with the following sequence:
-    1. `rm -rf node_modules` — remove the workspace-root full-deps `node_modules/` (the pnpm install output)
-    2. `mv out/node_modules node_modules` — place the prod-only deploy output at workspace root
-    3. `rm -rf apps/app/node_modules` — remove any pre-existing `apps/app/node_modules/` (e.g. from a previous run)
-    4. `ln -sfn ../../node_modules apps/app/node_modules` — create a compatibility symlink for the `migrate-mongo` script path and Node.js `require()` traversal
-    5. `rm -rf out` — clean up remaining deploy output (source files copied by `pnpm deploy`)
-  - Delete the entire step [2/4] block: the `echo "[2/4]..."` line, the `if [ -d ... ]` block with the `find .next/node_modules ...` sed loop, and the closing `echo "[2/4] Done."` line
-  - The `.next/node_modules/` symlinks generated by Turbopack point to `../../../../node_modules/.pnpm/...` (workspace root). With `node_modules/` now at workspace root, those symlinks resolve naturally — no rewriting needed
-  - **Note**: After running this script locally, the workspace-root `node_modules/` is prod-only. Developers must run `pnpm install` from workspace root after local testing to restore the full development environment
-  - **Note on 5.4/5.5**: The remaining two operations (rm `.next/cache`, rm `next.config.ts`) could be inlined as `RUN` steps directly in the Dockerfile, eliminating `assemble-prod.sh` entirely — defer this to a follow-up if desired
-  - _Requirements: 1.1, 1.2, 1.3, 1.4, 2.4, 3.1, 3.2, 3.3, 5.4, 5.5_
-
----
-
-- [x] 3. (P) Update Dockerfile `builder`-stage artifact staging to include workspace-root `node_modules/`
-  - In `apps/app/docker/Dockerfile`, in the `builder` stage `RUN mkdir -p /tmp/release/apps/app && cp ...` step, add `cp -a node_modules /tmp/release/ && \` immediately before the `cp -a apps/app/.next ...` line to copy the workspace-root prod `node_modules/` into the release artifact
-  - The existing `apps/app/node_modules` entry in the long `cp -a` list continues to be correct: `cp -a` uses `-d`/`-P` (no symlink dereferencing), so the symlink created by `assemble-prod.sh` is copied as a symlink, not dereferenced
-  - The resulting `/tmp/release/` structure will be: `node_modules/` (workspace root, prod-only) + `apps/app/.next/` + `apps/app/node_modules` (symlink to `../../node_modules`)
-  - This task modifies only the Dockerfile staging `RUN` step; no changes to the `pruner`, `deps`, or `release` stages
-  - Can be written concurrently with task 2 (different file); testing requires task 2 to be complete first
-  - _Requirements: 3.1, 3.3, 5.1, 5.2, 5.3_
-
----
-
-- [x] 3b. Update CI workflow archive step to include workspace-root `node_modules/`
-  - In `.github/workflows/reusable-app-prod.yml`, in the `build-prod` job `archive-prod-files` step, add `node_modules \` immediately before `apps/app/.next \` in the `tar -zcf` command
-  - With the new `assemble-prod.sh`, `apps/app/node_modules` is a symlink to `../../node_modules`. Without this change, `launch-prod` and `run-playwright` extract a broken symlink (the target `../../node_modules` is absent from the tarball)
-  - `tar` preserves symlinks by default (no `--dereference`): `apps/app/node_modules` is archived and extracted as a symlink; workspace-root `node_modules/` is archived and extracted as a real directory — `../../node_modules` resolves correctly after extraction
-  - Verify the tarball contents: `tar -tzf production.tar.gz | grep -E '^node_modules/$'` should show `node_modules/`
-  - _Requirements: 4.3, 4.5_
-
----
-
-- [x] 4. Validate the simplified assembly pipeline end-to-end
-
-  Follow the **Production Server Startup Procedure** established in `optimise-deps-for-prod/design.md` (Steps 1–6).
-  Key difference from that procedure: **`mv node_modules node_modules.bak` is NOT needed** — after `assemble-prod.sh`, workspace-root `node_modules/` is already the prod-only deploy output; `apps/app/node_modules` is a symlink to `../../node_modules`.
-
-- [x] 4.1 Run the updated `assemble-prod.sh` and verify symlink integrity
-  - Follow Steps 1–2 of the Production Server Startup Procedure (build → assemble)
-  - After `assemble-prod.sh`, run additional assertions specific to this spec:
-    - `test -L apps/app/node_modules && echo OK` → `OK` (symlink, not a directory)
-    - `readlink apps/app/node_modules` → `../../node_modules`
-  - Run `bash apps/app/bin/check-next-symlinks.sh` (established tool from `optimise-deps-for-prod`) — expect `OK: All apps/app/.next/node_modules symlinks resolve correctly.`
-  - _Requirements: 1.1, 1.2, 1.3, 1.4, 2.3, 3.2, 6.1, 6.2, 6.3_
-
-- [x] 4.2 Start the production server and verify HTTP 200 on root page
-  - Follow Steps 3–5 of the Production Server Startup Procedure (start → verify → stop)
-  - Use `GET /` (not `/login`) as the smoke-test URL (established rule from `optimise-deps-for-prod`)
-  - _Requirements: 3.4, 4.1, 4.2, 4.3, 4.4, 4.5, 6.3, 6.4_
-
-- [x] 4.3 Restore the development environment after local validation
-  - Follow Step 6 of the Production Server Startup Procedure (restore `next.config.ts`, `pnpm install`)
-  - _Requirements: 5.4_

+ 55 - 43
.kiro/specs/optimise-deps-for-prod/design.md

@@ -38,65 +38,76 @@ This feature corrects the `devDependencies` / `dependencies` classification in `
 
 ## Architecture
 
-### Existing Architecture Analysis
+### Current Architecture Analysis
 
-The production assembly pipeline is:
+The production assembly pipeline:
 
 ```
 turbo run build
   └─ Turbopack build → .next/ (with .next/node_modules/ symlinks → ../../../../node_modules/.pnpm/)
 
-assemble-prod.sh
-  ├─ pnpm deploy out --prod --legacy   → out/node_modules/ (pnpm-native: .pnpm/ + symlinks)
-  ├─ rm + mv out/node_modules → apps/app/node_modules/
-  ├─ [1b] symlink rewrite in apps/app/node_modules/:
-  │     non-scoped: ../../../node_modules/.pnpm/ → .pnpm/
-  │     scoped:    ../../../../node_modules/.pnpm/ → ../.pnpm/
-  ├─ rm -rf .next/cache
-  ├─ next.config.ts removal
-  └─ [2] symlink rewrite in .next/node_modules/:
-        ../../../../node_modules/.pnpm/ → ../../node_modules/.pnpm/
-
-cp -a to /tmp/release/           → preserves pnpm symlinks intact
+assemble-prod.sh (current — no symlink rewriting):
+  [1]   pnpm deploy out --prod --legacy --filter @growi/app
+        └─ out/node_modules/<pkg> → .pnpm/<pkg>/...  (self-contained in pnpm v10 ✓)
+  [mv]  rm -rf node_modules && mv out/node_modules node_modules
+        └─ workspace-root node_modules/ is now prod-only ✓
+  [ln]  rm -rf apps/app/node_modules && ln -sfn ../../node_modules apps/app/node_modules
+        └─ apps/app/node_modules → ../../node_modules ✓
+  [3]   rm -rf .next/cache
+  [4]   rm -f next.config.ts
+
+cp -a node_modules /tmp/release/   → workspace-root prod node_modules
+cp -a apps/app/.next ...           → preserves pnpm symlinks intact
 COPY --from=builder /tmp/release/ → release image
 ```
 
-Two symlink rewrite steps are required:
-
-1. **`apps/app/node_modules/` rewrite** (`[1b]`): `pnpm deploy --prod` generates top-level symlinks in `out/node_modules/` pointing to the workspace-root `.pnpm/`. After `mv out/node_modules apps/app/node_modules`, these symlinks still reference the workspace-root path, which does not exist in production. Rewriting them to point within `apps/app/node_modules/.pnpm/` makes the deploy self-contained.
-
-2. **`.next/node_modules/` rewrite** (`[2]`): Turbopack generates symlinks in `.next/node_modules/` pointing to `../../../../node_modules/.pnpm/` (workspace root). After rewriting to `../../node_modules/.pnpm/`, they resolve to `apps/app/node_modules/.pnpm/`, preserving pnpm's sibling-resolution for transitive dependencies.
+The `.next/node_modules/` symlinks (pointing `../../../../node_modules/.pnpm/`) resolve naturally to the workspace-root `node_modules/` — no symlink rewriting is needed. In pnpm v10, `--legacy` produces self-contained `.pnpm/` symlinks within the deploy output (step [1b] is unnecessary); placing the output at workspace root means Turbopack's original symlink targets already resolve (step [2] is unnecessary).
 
 ### Architecture Pattern & Boundary Map
 
 ```mermaid
 graph TB
-    subgraph BuildEnv
-        TurboBuild[turbo run build]
-        NextModules[.next/node_modules symlinks]
-        AssembleScript[assemble-prod.sh]
-        DeployOut[pnpm deploy out]
-        AppNodeModules[apps/app/node_modules .pnpm + symlinks]
-        RewriteStep[symlink rewrite step]
+    subgraph DockerBuilder
+        PrunedRoot[Pruned workspace root]
+        FullDepsNM[node_modules full deps]
+        Build[turbo run build]
+        NextOut[apps/app/.next node_modules symlinks to 4-levels-up]
+        Deploy[pnpm deploy out --prod --legacy]
+        ProdNM[out/node_modules prod-only self-contained]
+        Replace[rm node_modules mv out/node_modules node_modules]
+        Symlink[ln -sfn apps/app/node_modules]
+        Clean[rm cache next.config.ts]
+        Stage[Stage to tmp/release]
     end
 
     subgraph ReleaseImage
-        ReleaseDist[release artifact]
-        ProdServer[pnpm run server]
+        WsRoot[workspace root node_modules prod-only]
+        AppNext[apps/app/.next symlinks resolve naturally]
+        AppNMLink[apps/app/node_modules symlink to ws root]
+        Server[pnpm run server]
     end
 
-    TurboBuild --> NextModules
-    AssembleScript --> DeployOut
-    DeployOut --> AppNodeModules
-    AssembleScript --> RewriteStep
-    RewriteStep --> NextModules
-    AppNodeModules --> ReleaseDist
-    NextModules --> ReleaseDist
-    ReleaseDist --> ProdServer
+    PrunedRoot --> FullDepsNM
+    FullDepsNM --> Build
+    Build --> NextOut
+    Build --> Deploy
+    Deploy --> ProdNM
+    ProdNM --> Replace
+    Replace --> Symlink
+    Symlink --> Clean
+    Clean --> Stage
+    Stage --> WsRoot
+    Stage --> AppNext
+    Stage --> AppNMLink
+    WsRoot --> AppNext
+    AppNMLink --> WsRoot
+    WsRoot --> Server
 ```
 
 **Key decisions**:
-- Symlink rewrite (not `cp -rL`) preserves pnpm's sibling resolution for transitive deps (see `research.md` — Decision: Symlink Rewrite over cp -rL).
+- Workspace-root staging (not `apps/app/` staging): Turbopack's `.next/node_modules/` symlinks point `../../../../node_modules/.pnpm/` (workspace root). Placing the deploy output at workspace root means these symlinks resolve naturally — no step [2] rewriting needed (see `research.md` — Session 3: Decision: workspace-root staging).
+- Keep `--legacy` in `pnpm deploy`: In pnpm v10, `--legacy` produces self-contained `.pnpm/` symlinks within the deploy output — step [1b] rewriting is no longer needed (see `research.md` — Session 3).
+- `apps/app/node_modules` as a symlink to `../../node_modules`: satisfies `migrate-mongo` script path and Node.js `require()` traversal without duplicating the `.pnpm/` store.
 - `pnpm deploy --prod` (not `--dev`) is the correct scope; only runtime packages belong in the artifact.
 
 ### Technology Stack
@@ -104,7 +115,7 @@ graph TB
 | Layer | Choice | Role | Notes |
 |-------|--------|------|-------|
 | Package manifest | `apps/app/package.json` | Declares runtime vs build-time deps | 23 entries move from `devDependencies` to `dependencies` |
-| Build assembly | `apps/app/bin/assemble-prod.sh` | Produces self-contained release artifact | Already contains symlink rewrite; no changes needed in Phase 1 |
+| Build assembly | `apps/app/bin/assemble-prod.sh` | Produces self-contained release artifact | No symlink rewriting; workspace-root staging + `apps/app/node_modules` symlink (pnpm v10) |
 | Bundler | Turbopack (Next.js 16) | Externalises packages to `.next/node_modules/` | Externalisation heuristic: static module-level imports in SSR code paths |
 | Package manager | pnpm v10 with `--legacy` deploy | Produces pnpm-native `node_modules` with `.pnpm/` virtual store | `inject-workspace-packages` not required with `--legacy` |
 
@@ -319,8 +330,9 @@ bash apps/app/bin/assemble-prod.sh
 ```
 
 > **注意**:
-> - `assemble-prod.sh` は `apps/app/next.config.ts` を削除する。**`next.config.ts` はサーバーテスト完了後(Step 6)に復元すること。** サーバー起動前に復元すると、Next.js が起動時に TypeScript インストールを試みて pnpm install が走り、`apps/app/node_modules` の symlink が上書きされ HTTP 500 となる。
-> - `pnpm deploy` はサイドエフェクトとしてワークスペースルートの `node_modules` を再作成する。Docker の release stage では `apps/app/node_modules/` のみ COPY され、ワークスペースルートの `node_modules` は含まれない。`pnpm deploy --prod --legacy` が workspace パッケージ(`@growi/core` 等)を `.pnpm/` ローカルストアの実体ディレクトリとしてデプロイするため、`apps/app/node_modules/` は自己完結している。より正確な production 再現テストを行う場合は `mv node_modules node_modules.bak` でワークスペースルートを退避してからサーバーを起動し、テスト後に `mv node_modules.bak node_modules` で復元すること。
+> - `assemble-prod.sh` は `apps/app/next.config.ts` を削除する。**`next.config.ts` はサーバーテスト完了後(Step 6)に復元すること。** サーバー起動前に復元すると、Next.js が起動時に TypeScript インストールを試みて pnpm install が走り、`apps/app/node_modules` symlink が上書きされ HTTP 500 となる。
+> - `assemble-prod.sh` は `rm -rf node_modules && mv out/node_modules node_modules` を実行するため、ワークスペースルートの `node_modules/` が prod-only に置き換わる。**`mv node_modules node_modules.bak` は不要**(新アプローチではワークスペースルートが自動的に prod-only になる)。テスト後は `pnpm install` で開発環境を復元すること。
+> - `apps/app/node_modules` は `../../node_modules` へのシンボリックリンクになる。Docker release image でもこの構造が再現される。
 
 **Step 3 — プロダクションサーバー起動**(`apps/app/` から実行)
 
@@ -410,7 +422,7 @@ done
 
 **devcontainer における再現性について**
 
-`pnpm deploy --prod` 後もワークスペースルートの `node_modules/` が残存するため、**必ず `mv node_modules node_modules.bak` を実施してからサーバーを起動すること**。この手順を省略すると `apps/app/node_modules/` の壊れたシンボリックリンクがワークスペースルートの `node_modules/` によって補完され、誤って green と判定される
+`assemble-prod.sh` は `rm -rf node_modules && mv out/node_modules node_modules` を実行するため、ワークスペースルートの `node_modules/` が prod-only の deploy 出力に置き換わる。**`mv node_modules node_modules.bak` は不要**(以前のアプローチでは必要だったが、現アプローチでは `assemble-prod.sh` がワークスペースルートを自動的に prod-only にする)。テスト完了後は `pnpm install` で開発環境を復元すること
 
 ---
 
@@ -439,9 +451,9 @@ done
 
 ### Phase 5 — Final Coverage Check (Req 5.1, 5.3)
 
-- After deploy, assert that every symlink in `apps/app/.next/node_modules/` AND `apps/app/node_modules/` resolves to an existing file (zero broken symlinks, verified with workspace-root `node_modules` renamed).
+- After deploy, assert that every symlink in `apps/app/.next/node_modules/` AND `apps/app/node_modules/` resolves to an existing file (zero broken symlinks; `apps/app/node_modules` is a symlink to `../../node_modules` which is the prod-only workspace-root `node_modules/`).
 - Assert no package listed in `devDependencies` appears in `apps/app/.next/node_modules/` after a production build.
-- Run Production Server Startup Procedure (including `mv node_modules node_modules.bak`) and assert `GET /` HTTP 200 with expected content.
+- Run Production Server Startup Procedure and assert `GET /` HTTP 200 with expected content.
 
 ---
 

+ 54 - 0
.kiro/specs/optimise-deps-for-prod/research.md

@@ -196,3 +196,57 @@ All other packages remain in `dependencies` because either `ssr: false` wrapping
 ### Finding: CI symlink integrity check added
 
 `check-next-symlinks.sh` was added to the `build-prod` CI job (runs after `assemble-prod.sh`) to detect broken symlinks in `.next/node_modules/` automatically. This prevents future classification regressions regardless of which code paths are exercised at runtime by `server:ci`. The `fslightbox-react` exception is hardcoded in the script.
+
+---
+
+## Session 3: Assembly Pipeline Simplification
+
+### Finding: pnpm v10 `--legacy` creates self-contained symlinks (step [1b] eliminated)
+
+**Verification** — `pnpm deploy out --prod --legacy --filter @growi/app` in pnpm v10.32.1:
+- `out/node_modules/react` → `.pnpm/react@18.2.0/node_modules/react` (self-contained ✓)
+- `out/node_modules/@codemirror/state` → `../.pnpm/@codemirror+state@6.5.4/node_modules/@codemirror/state` ✓
+
+**NOT** `../../../node_modules/.pnpm/...` (workspace-root-pointing) as in pre-v10 pnpm.
+
+**Implication**: Step [1b] is no longer needed. The root cause of step [1b] was placing the deploy output at `apps/app/` (not `--legacy` itself). Removing `--legacy` would require `inject-workspace-packages=true` in `.npmrc` with no practical benefit — keeping `--legacy` is the simpler choice.
+
+**pnpm version sensitivity**: If downgrading below pnpm v10, `--legacy` may again produce workspace-root-pointing symlinks requiring step [1b] to be reinstated. Verify with `readlink out/node_modules/react` — must start with `.pnpm/`.
+
+---
+
+### Finding: `.next/node_modules/` symlink path analysis (step [2] eliminated)
+
+**Analysis of actual `.next/node_modules/` contents** (Turbopack original output, before any rewrite):
+- Non-scoped packages: `../../../../node_modules/.pnpm/<pkg>/...` (4 levels up from `.next/node_modules/` = Docker workspace root)
+- Scoped packages (`@scope/pkg`): `../../../../../node_modules/.pnpm/<pkg>/...`
+
+Step [2] in the old `assemble-prod.sh` rewrote these from `../../../../` to `../../` to point to `apps/app/node_modules/`. If the workspace-root `node_modules/` contains the prod deps instead, the original Turbopack symlinks resolve correctly without any rewriting.
+
+**Implication**: Placing the deploy output at workspace root eliminates step [2] entirely.
+
+---
+
+### Design Decisions
+
+#### Decision: Workspace-root staging (eliminates step [2])
+
+- **Alternatives**:
+  1. Keep `apps/app/node_modules/` placement, apply step [2] rewrite (old approach)
+  2. `pnpm install --prod` post-build — workspace packages remain as symlinks, requiring `packages/` in release image
+  3. Place deploy output at workspace root, no rewrite needed (selected)
+- **Selected**: `rm -rf node_modules && mv out/node_modules node_modules` + `ln -sfn ../../node_modules apps/app/node_modules`
+- **Rationale**: Turbopack's original symlink targets (`../../../../node_modules/.pnpm/`) already point to workspace root. Preserving this structure requires no rewriting.
+- **Trade-off**: `rm -rf node_modules` destroys the full-deps workspace root `node_modules/` locally — developers must run `pnpm install` to restore after local testing.
+
+#### Decision: Keep `--legacy` in `pnpm deploy` (eliminates step [1b])
+
+- **Context**: In pnpm v10, `--legacy` produces self-contained `.pnpm/` symlinks regardless. The `--legacy` flag is now a pnpm v10 gate-bypass (skips `inject-workspace-packages` check), not a linker-mode selector.
+- **Alternatives**: Remove `--legacy` — requires `inject-workspace-packages=true` in `.npmrc`; same symlink output but adds config dependency.
+- **Selected**: Keep `--legacy` (no `.npmrc` changes required).
+
+#### Decision: `apps/app/node_modules` as a symlink to `../../node_modules`
+
+- **Context**: The `migrate` script in `apps/app/package.json` uses path `node_modules/migrate-mongo/...` relative to `apps/app/`. With deploy output at workspace root, this direct path would fail without the symlink.
+- **Selected**: `ln -sfn ../../node_modules apps/app/node_modules`
+- **Rationale**: Satisfies migration script path + Node.js `require()` traversal without duplicating the `.pnpm/` store. `cp -a` and Docker BuildKit `COPY` both preserve symlinks correctly.

+ 1 - 1
.kiro/specs/optimise-deps-for-prod/spec.json

@@ -1,7 +1,7 @@
 {
   "feature_name": "optimise-deps-for-prod",
   "created_at": "2026-03-12T05:00:00Z",
-  "updated_at": "2026-03-16T00:00:00Z",
+  "updated_at": "2026-03-17T00:00:00Z",
   "language": "en",
   "phase": "implementation-complete",
   "cleanup_completed": true,