# 開発者向けの説明
利用の方法は [README](../README.md) にあります。この文書は、仕組みと開発の方法をまとめたものです。設計書は [docs/design/](design/README.md)、画面の仕様は [docs/ui-spec.md](ui-spec.md) にあります。
開発中は `pnpm dev` のあと `http://localhost:5173/m3e/?mock` を開きます。DSH の起動は不要です。偽データは開発時だけ読み込まれ、本番ビルドには含まれません。`features/*/routes.tsx` と `features/*/mock.ts` は自動で集めるので、機能の担当は共有登録ファイルを編集する必要がありません。
## 端末ごとの切り替え
- この端末の選択は、Cookie `dsh-webui`(値は `m3e` か `classic`)に入っています。iPhone のホーム画面に追加した PWA は Safari と別の Cookie を持つので、別々に選べます。
- サーバーは、DSH が返すすべての index の `
` の先頭に、小さなスクリプトを差し込みます(`tapIndex`)。今の画面の index(`/` と `/index.html`)で Cookie が `m3e` なら、今の画面のコードを読み込む前に `/m3e/` へ移動します。判定は [src/shared/ui-choice.ts](../src/shared/ui-choice.ts) にあります。
- 切り替える方法は 3 つあります。
- 今の画面の「設定 → 一般 → この端末で M3E の画面を使う」
- M3E の画面の「今の画面に戻す」ボタン
- URL の `?ui=m3e` と `?ui=classic`。どちらかの画面が壊れたときの戻り道です。
- `/` の経路を横取りする方法は使っていません。今の画面を返す処理(fallback)を外から呼び出せないためです。
## 仕組み
- **サーバー側**([src/host/index.ts](../src/host/index.ts)): `/m3e` を prefix 経路として登録し、上の切り替えスクリプトを差し込みます。画面本体は、今の UI と同じ `authorizeIndex`(ログイン確認)と `renderIndex`(起動データの埋め込み)を通して返します。標準画面向けの相対 URL の先読み指定を取り除き、M3E の index には `` を入れます。通信は Host のルート、画面遷移は `/m3e/` を基準にします。
- **今の画面に入る部品**([src/client/index.tsx](../src/client/index.tsx)): 設定の一般セクション(`settings.general.item`)に切り替えの行を追加します。React は今の画面が持っているものを使います。M3E の画面はこの部品を読み込みません。
- **ブラウザ側の起動**([web/src/dsh/boot.ts](../web/src/dsh/boot.ts)): DSH が埋め込む `__DSH_BOOT__` から、ジョブの controller を含む通信用の `TRANSPORT_PLUGINS` と、その依存だけを残して起動します。残したプラグインは個別の URL で読み込みます。DSH は、宣言していない組み合わせの一括 URL には 404 を返すためです。
- **共有ライブラリ**: 今の UI は、プラグインが外部参照する `@deepseek-ai/cordis` と `@deepseek-ai/dsh-client-store` を自分のビルドから渡しています。この UI では、同じものを自前で同梱して渡します。**版は本番 DSH と合わせる必要があります**。対応する版と固定版は [src/shared/dsh-compat.ts](../src/shared/dsh-compat.ts) の 1 か所にまとめてあり、`package.json` と食い違うと `pnpm test` が止まります。
- **DSH との境界**: どこが DSH のどの部分に依存しているか、どの版で何を確かめたかは [dsh-compatibility.md](dsh-compatibility.md) にあります。起動の直後に、使う controller のメソッドが揃っているかを `web/src/dsh/contract.ts` で確かめます。
## ビルドで補っていること
- `@deepseek-ai/dsh-client-store` は zustand と immer を使っていますが、依存関係として宣言していません。そのため [pnpm-workspace.yaml](../pnpm-workspace.yaml) の `packageExtensions` で補っています。
- `@deepseek-ai/cordis-plugin-loader` は、読み込まれた時点で Node.js の内部 API を探しに行きます。そのため [vite.config.ts](../vite.config.ts) で `node:module` を代わりのファイルに差し替え、`process.versions.node` を `"0"` に置き換えています。
## コマンド
```bash
pnpm install
pnpm dev # http://localhost:5173/m3e/?mock(DSH は起動しない)
pnpm test # 既存のテストと、ルーター・承認/質問・セッション・偽データの単体テスト
pnpm typecheck
pnpm build # dist/(M3E の画面)、lib/index.js(サーバー側)、lib/client.js(今の画面に入る部品)
pnpm exec playwright test -c e2e/playwright.config.ts # ?mock の画面。専用 Vite を自動で起動
env M3E_DSH_VERSION=0.2.0-rc.2 pnpm exec playwright test -c e2e-dsh/playwright.config.ts # 実 DSH と偽の LLM
```
## 試験の使い分け
| 試験 | 確かめる対象 | 起動・ポート |
|---|---|---|
| `pnpm test` | 変換、参照の寿命、偽 controller、各機能の単体試験 | DSH やブラウザは起動しません |
| `e2e/playwright.config.ts` | `?mock` の画面と操作 | Vite を `127.0.0.1:5191` で起動し、ブラウザは `http://localhost:5191` を使います |
| `e2e-dsh/playwright.config.ts` | 実 DSH の Host・通信と M3E。LLM は Messages 形式の偽物です | `127.0.0.1` の空きポートを自動で使います。Vite は使いません |
偽データ e2e は `reuseExistingServer: false` と `--strictPort` を使います。既定ポートが占有されている場合は、既存サーバーを流用せず、`e2e/playwright.config.ts` を継承した `tmp/e2e-alt-port.config.ts` で `use.baseURL`、`webServer.command`、`webServer.port` を同じ空きポートにそろえます。試験条件は変えません。今回の移行では、まとめ側が 5192、段階 5 の worktree が 5194 を使いました。一時設定は Git 管理外なので、新しい worktree にあるとは限りません。
```bash
pnpm exec playwright test -c tmp/e2e-alt-port.config.ts
```
実 DSH の試験は、初回だけ指定版を npm から `tmp/dsh-integration/` に入れます。M3E をビルド・pack し、試験用の新しい `HOME` と `DSH_HOME` で起動します。利用者の DSH は使いません。
| 環境変数 | 指定するもの |
|---|---|
| `M3E_DSH_VERSION` | 試す DSH の版。省略時は `src/shared/dsh-compat.ts` の対応版 |
| `M3E_DSH_DIR` | 入れ済みの DSH のディレクトリ(`node_modules/.bin/dsh` を含む場所) |
| `M3E_SKIP_BUILD=1` | 既存の明示的 build だけを省きます。pack と、その prepack の build・検査は必ず行います |
| `M3E_CHROMIUM_PATH` | Chromium の実行ファイル。偽データ e2e と実 DSH の両方で使えます |
環境変数は `env M3E_DSH_VERSION=… pnpm exec …` の形で渡します。試験用 Host の `DEEPSEEK_BASE_URL`、架空の API キー、`SSH_CONNECTION` は `e2e-dsh/dsh-host.ts` が設定します。
期限つき質問の spec は `e2e-dsh/timed-questions-fixtures.ts` を使います。`startDsh(llm.url, { timedQuestionSeconds: 2 })` が隔離ホームの `cordis.patch.yml` に専用 preset を置き、`dsh-tool-ask-user` の `mode: 'timed'` と `timeout: 2` を起動前に設定します。この項目は公開設定ではなく、設定 RPC からの変更ではありません。通常の legacy 質問とは別の Host を使い、追加の環境変数は不要です。全体実行にも含まれ、対象だけなら次のように実行します。
```bash
env M3E_DSH_VERSION=0.2.0-rc.2 pnpm exec playwright test -c e2e-dsh/playwright.config.ts e2e-dsh/timed-questions.spec.ts e2e-dsh/timed-questions-hidden.spec.ts e2e-dsh/timed-questions-race.spec.ts
```
`fixture-contract.spec.ts` は偽記録と不正入力を、実物の V4・Session・commands・compaction の検査関数で照合します。この spec は既定の `tmp/dsh-integration/dsh-<版>/` から関数を読み込むため、`M3E_DSH_DIR` を指定しても、全体実行には既定の場所に同じ版が必要です。Host 上で全 fixture の会話を生成したという確認ではありません。
**フォルダ選択の方式に注意してください。** macOS / Windows の loopback 起動では、Host が OS のダイアログを使う `native` を選ぶ場合があります。統合試験は SSH 起動の印で `browse` を選び、ブラウザのフォルダ一覧を操作します。実際に SSH 接続するわけではなく、bind は `127.0.0.1` のままです。この構成では open-in-app の候補が空になるため、native のフォルダ選択や外部アプリ起動の確認には使えません。
Playwright は途中で中断せず、Host・偽 LLM・Vite の終了処理まで待ちます。対象を絞る場合は、実行の最初からファイルや `-g` を指定してください。結果は偽データが `tmp/e2e-report.json`、実 DSH が `tmp/dsh-integration/report.json` に出ます。
最新の件数と確認範囲は [互換性文書の版ごとの確認結果](dsh-compatibility.md#版ごとの確認結果) と [実 DSH の統合試験](dsh-compatibility.md#実-dsh-の統合試験) にまとめます。実 DSH の通常成功と既知の差の期待失敗を分け、偽データの成功を実 DSH の成功に数えません。追加・変更した試験は、同文書の[壊して確かめる手順](dsh-compatibility.md#試験の強さの確かめ方)でも確認します。文書だけの変更では試験・ビルドを流さず、記述・リンク・差分を確認します。
## 偽データの選び方
開発用の状態は URL で選べます。
- `?mock`:3 ワークスペース、待機中と実行中の 2 セッション
- `?mock&scenario=disconnected`:接続切れ
- `?mock&scenario=reconnecting`:再接続中
- `?mock&scenario=approval-demo#/s/readme-review`:1 秒後に仮の承認シート
- `?mock&scenario=approval-demo#/s/readme-review/trace`:トレース表示中の承認
各機能は `web/src/dsh/mock/kit.ts` の `MockKit` を使い、自分の `mock.ts` の `extendMock(kit)` でデータやシナリオを追加できます。共通セッション `readme-review` と `approval-sheet` の履歴は変更せず、必要なセッションを追加してください。共有部品の引数と戻り値、静的調査の結果は [00 の実装メモ](design/00-foundation.md#実装メモ) にあります。
偽物も、実物の拒否条件・準備・参照・取消・通知に合わせます。現在の契約と残す差は [偽データの決まりと意図した簡略化](dsh-compatibility.md#偽データの決まりと意図した簡略化) を参照してください。V4 の見本を変えるときは [規則の対応表と表示期待値の再生成手順](../tests/helpers/s6b-v4-rules.md) を使い、対象履歴を消して通すことや、本体と同じ変換で期待値を作ることを避けます。
DSH に入れる手順は README の「Quick Start」にあります。
## 配布
当面は **GitHub Releases の添付 tgz の URL** で配ります。パッケージ名は `dsh-webui-m3e`、版は **0.0.8**、対応する DSH は **0.2.0-rc.2 のみ**です。0.0.7 は以前配布した内容と main の内容が異なるため、その番号を再利用しません。npm 公開と CI の追加は行っていません。
**2026-10-05 に v0.0.8 をリリースしました。** 指示役が PR #20 を通常のマージで取り込み、main の先頭 `d00515e0776f46608a5c3ffe70eb529974ecf744` で確認を通した後、同じコミットを対象にタグ `v0.0.8` と [GitHub Release](https://github.com/AliceSaikawa/dsh-webui-m3e/releases/tag/v0.0.8) を作成しました。添付は pnpm で作った `dsh-webui-m3e-0.0.8.tgz`(4,465,060 バイト)、SHA-256 は `1d77d0ac8cc85333c1937927f879d5c83774c00ee3519080f584f7120b9bae24` です。
npm レジストリにはアカウントが無いため未公開で、名前だけでの追加はできません。Issue #18 の主経路である npm は満たしていませんが、指定された代替経路では、GitHub URL によるソース取得・ビルド・version の手編集が不要な導入を確認しました。公開状況と検証記録はこの文書、`dsh-compatibility.md`、`handoff.md` で管理します。
将来 npm にも出せるよう、`private` を外した状態、`publishConfig`、`prepack`、`check:pack`、本番モジュール検査をそのまま残しています。GitHub Releases の tgz を作る際も同じ build・pack・検査を使います。
配布判定の単体試験は受け直しで 28 件になりました。前回は監査の 35 変異と追加の 11 変異をすべて検出し、今回は再監査で生存した N02・N13 も検出しました。別の新しいセッションでの当て直しでも、N02・N13 と近い変異 4 個を検出しました。patch の名前の行末の空白の扱いをゆるめる変異 1 個は試験を通りましたが、取り出される名前が変わらない(結果が同じになる)変異と判断しています。指示役の最後の検証(`08c6cd2`)は、型検査とビルド成功、単体 1,039 件、`check:pack`、偽データ e2e 159 件、実 DSH 2 回(各回、通常 33 件の成功と既知の差の期待失敗 1 件)です。稼働中の追加・更新・旧版復帰・削除とデータ保持は、前回の隔離環境での手動確認を引き継ぎます。`e2e-dsh/dsh-host.ts` の単独パッチと、代替の `e2e-dsh/plugin-cli.ts` の追加は前回、それぞれ 1 回試して保護フックに拒否されました。同じ変更を別手段で再試行していません。作業の指示で決めておいた代替手順に従い、導入ライフサイクルの自動の試験は無いまま、下記の手動結果を残しています。公開権限・2 要素認証は公開する人の環境で確認が必要です。
### ソースからのビルドと配布物の検査
```bash
git clone https://github.com/AliceSaikawa/dsh-webui-m3e.git
cd dsh-webui-m3e
pnpm install --frozen-lockfile
pnpm build
pnpm run check:pack
pnpm pack --pack-destination tmp/release
node scripts/check-pack.ts tmp/release/dsh-webui-m3e-0.0.8.tgz
```
Node.js・pnpm・npm と `tar` が必要です。開発では TypeScript ファイルを Node で直接実行するため、その実行に対応した Node を使います。確認環境は macOS 27.2(26B5086k)、Node 26.7.0、pnpm 11.17.0、npm 11.19.0 です。DSH 0.2.0-rc.2 自身の manifest に engines はありませんが、CLI の commander 15.0.0 は Node `>=22.12.0`、pnpm 11.17.0 は `>=22.13` を宣言しています。最低版での実行は未検証であり、M3E が Node 22 の初期版から動くとは案内しません。
M3E の根拠が不足していた `engines.node: >=22` は削除しました。M3E の実行時 dependencies は空で、依存側の宣言と M3E の検証済み下限は別です。根拠は隔離 DSH の `node_modules/commander/package.json:60` と通常 CLI の pnpm の `package.json:63`、`bin/pnpm.mjs:6-18`。tmp の依存無し tgz に試験用 `engines.node: >=99` を付けて Node 26.7.0 の pnpm 11.17.0 で追加すると、既定では警告無し・exit 0、`--config.engine-strict=true` では `ERR_PNPM_UNSUPPORTED_ENGINE`・exit 1 でした。engines を書くだけで動作確認の代わりにはならず、未確認の下限で利用を拒むことも避けるため、今回は新しい下限を宣言していません。
`prepack` が build → check:pack の順に実行します。`pnpm pack` と `npm pack` の両方で実際に発火を確認しました。`npm publish` も同じ prepack を使うことは [npm の lifecycle の説明](https://docs.npmjs.com/cli/v11/using-npm/scripts/#life-cycle-operation-order)に基づきます。公開コマンド自体は実行していません。公開はソースのルートから行い、`--ignore-scripts` でこの仕組みを無効化しないでください。作成済み tgz をそのまま publish する経路には、再ビルドの保証はありません。
`check:pack` は実際に `npm pack --ignore-scripts --json` で `tmp/pack-check/run-*/` に tgz を作り、tar の一覧と許可したテキストの内容を調べます。内側だけ scripts を止めて prepack の再帰を避けています。引数で既存 tgz を渡せば、pnpm が作った実物にも同じ検査を行えます。判定は副作用のない `scripts/pack-contract.ts` の `validatePack` です。外側の pack は必ずビルドし、内側の check 単独は現在のビルド成果物を検査します。
検査対象は、package.json、Host/client の JS、HTML、JS/CSS assets、patch、LICENSE、3 言語 README の存在です。配布先の main/exports、package と patch の名前、bundle 宣言、実行時依存が空であること、利用者側の preinstall/install/postinstall/prepare が無いことも調べます。許可するファイルの一覧にないソース・試験・docs・tmp・設定・マップなどは拒否します。JS/CSS の sourceMappingURL と、配布テキストの利用者ホームの絶対パスも拒否します。
Vite 7.3.6 の既存成果物は `index-BL75o79i.js`、`index-2YhsQp64.css`、`material-symbols-outlined-DzyP15l8.woff2` でした。`vite.config.ts` に `[name]-[hash:8]` の出力形式を明記し、assets の許可条件も名前+8文字ハッシュと小文字の js/css/woff2 に限定しました。開発用のドット入りの名前、サブディレクトリ、想定外の拡張子は拒否します。出力設定を変える際は正常系の fixture と許可条件も見直し、実 tgz の `check:pack` を通してください。tgz の生の名前は純粋関数 `parsePackEntries` で先に検査し、`package/` 外のファイル・ディレクトリを、内容を読まずに `invalid archive root` で拒否します。
偽データの起動は `web/src/main.tsx:12-13` の DEV 分岐から行い、`web/src/dsh/mock/index.ts` が feature の mock を登録します。Vite の `productionModuleGuard` は、生成チャンクの `modules` に偽データ用モジュールが 1 つでもあればビルドを止めます。対象は `web/src/dsh/mock/` と、features 内の `mock*.ts(x)`、`*-mock*.ts(x)`、`*-fixtures.ts(x)`。現在の 30 ファイル全件を `tests/18-pack.test.ts` の一覧で照合し、Windows の区切りと query 付き id も検査します。
唯一の例外は `web/src/dsh/mock/observable.ts` です。このファイルは通知・購読・スナップショットの汎用処理だけを持ち、偽 workspace・controller・記録は生成しません。本番の `completion-status.ts:2` と `conversation-selection.ts:2` が利用しているため、ファイル名の完全一致だけを許可しています。同じ場所の別名ファイルは拒否します。
tgz 内でも `readme-review`、`approval-sheet`、`__m3eTest`、`createMockContext`、`mock/open-failed`、`/mock/` を JS の目印として検査します。これはモジュール検査を補うもので、目印だけに依存しません。本番の案内にもある `?mock` の文字だけは許可します。Vite と build-plugin はソースマップを有効にしておらず、実際の 19 ファイルにもマップはありません。`tests/18-pack.test.ts` の 28 件が正常系と具体的なエラー文字列を確認し、通常の `pnpm test` に含まれます。
`e2e-dsh` の明示的 build と prepack の build が重なる状態です。解消パッチが拒否されたため、作業の指示のとおり二重ビルドを残しました。結果は変わらず、実行時間だけが増えます。`M3E_SKIP_BUILD=1` は既存の明示的 build を省く指定で、prepack のビルドと検査は省かれません。
### 版の上げ方
M3E の公開版は `package.json.version` だけを上げます。`src/shared/dsh-compat.ts` は対応 DSH と同梱ライブラリの宣言であり、M3E の公開版とは別です。DSH 自体を移行する場合だけ、[互換性文書の手順](dsh-compatibility.md#版を上げるときの手順)で共有ライブラリと宣言を揃えます。今回その関係、依存、ロックファイルは変更していません。公開済みの同じ版に別の成果物を載せる運用はしません。
### DSH CLI 0.2.0-rc.2 で確認したこと
確認日:2026-10-05。macOS 27.2(26B5086k)、Node.js 26.7.0、pnpm 11.17.0、npm 11.19.0。使用した実物は `tmp/dsh-integration/dsh-0.2.0-rc.2/node_modules/.bin/dsh` です。HOME と DSH_HOME は `tmp/issue18-cli/` 以下、Host を起動した全体試験は既存の `e2e-dsh` の隔離環境です。利用者の DSH と 3081/3199 は使っていません。
表の `dsh` は上の実物の絶対パス、実行環境は次の形です。
```bash
env HOME="$PWD/tmp/issue18-cli/home" DSH_HOME="$PWD/tmp/issue18-cli/dsh-home" tmp/dsh-integration/dsh-0.2.0-rc.2/node_modules/.bin/dsh plugin --profile web --help
```
| コマンド・確認 | 実際の結果 |
|---|---|
| `dsh plugin --help` | exit 1、required option `--profile ` not specified |
| `dsh plugin --profile web --help` | exit 0、pnpm 11.17.0 の help。初回は web profile も初期化する |
| 同じ接頭辞で `add --help`、`remove --help`、`update --help` | pnpm の引数を表示。追加 add、削除 remove、更新 update/up。更新先を固定する案内には `add name@version` を使用 |
| `list dsh-webui-m3e --depth 0 --json` | dependencies の version と解決先を表示。導入前・削除後は dependencies が無い |
| `add file:` → `add file:` → baseline を再追加 | すべて exit 0。一覧の version は 0.0.7 → 0.0.8 → 0.0.7。profile の直接参照が新しい tgz を向く |
| `remove dsh-webui-m3e` | exit 0。dependency と dsh.profile.bundles の M3E が消え、base と web-app は残る |
| `add dsh-webui-m3e` / `add dsh-webui-m3e@0.0.8` | exit 1、`ERR_PNPM_FETCH_404`、registry.npmjs.org の Not Found。成功は、将来 npm に公開した場合に確認 |
| `--profile web-typo add file:` | exit 0。base と M3E だけの別 profile が作られる。web-app は入らない。打ち間違いは自動検出されない |
| `--profile ../wrong list` | exit 1、invalid profile name とスタックトレース |
| 1 コマンドだけ `env PATH=/usr/bin:/bin … /absolute/node …/dsh/lib/bin.js plugin --profile web list` | exit 127、`pnpm was not found; install pnpm and make it available on PATH.`。環境設定は変更していない |
失敗時には `$DSH_HOME/profiles//.plugin-manager/logs/operation-*/pnpm.log` の場所が表示されます。通常 CLI が選んだ pnpm は今回 `/opt/homebrew/bin/pnpm` でした。導入先は `$DSH_HOME/profiles//node_modules`、書き換えるのは profile の package.json(dependencies と dsh.profile.bundles)、pnpm-lock.yaml、node_modules および処理記録です。共有ストアに古い成果物が物理的に残るかは調べておらず、直接参照が新しい版になることと区別します。
作業用 0.0.7 は main の DSH 0.2.0-rc.2 対応済みコードから作ったものです。以前配布した 0.0.7 の検証ではありません。0.0.8 の比較用 tgz は `tmp/issue18-cli/copy/package/` のコピーだけを編集して作り、リポジトリの版を試験のために往復させていません。
実物の根拠(以下は `node_modules/@deepseek-ai/` 内の相対パス):
| 場所 | 確認した契約 |
|---|---|
| `dsh/lib/bin.js:115-125,234-236` | profile 必須、引数転送、runPlugin の exit code |
| `dsh/lib/plugin-BGnVfe_D.js:76-96` | CLI options、管理関数、pnpm 不在と診断先の表示 |
| `dsh-plugin-manager/lib/types/operations.d.ts:13-26` | 実行コマンド・引数・環境を渡せる型。既定は PATH の pnpm |
| 同 `operations.js:43-72,246-275,325-331,444-536` | bundle の追加・削除、profile cwd、同梱コードの書換えではなく profile の依存を管理 |
| `dsh-app-boot/lib/index.js:524-587` | profile 名の検査、テンプレート、新規 profile の初期化 |
| `dsh-hmr/lib/index.js:353-376` | manifest は bundle の名前の列が変わらないと refresh を省く。手動確認でも、版だけの更新・復帰では Host コードの反映に再起動が必要だった |
[上流 CLI reference](https://github.com/deepseek-ai/deepseek-harness/blob/master/apps/cli/reference/README.md#plugin-management)の PATH 上の pnpm・引数転送は実物と一致します。ただし master の「update で bundle 宣言を得た依存も有効になる」という説明と、実物の reconcile が既存の beforeDeps を skip する点は一致しません。M3E は最初から bundle 宣言を持ちます。[配布ガイド](https://github.com/deepseek-ai/deepseek-harness/blob/master/docs/user/develop/basic/publish.md)の省略形 `dsh plugin add` には実物で必須の `--profile` を補います。Issue の 0.1.5 の記述は今回 0.2.0-rc.2 に読み替えています。Desktop の同梱経路は[上流の説明](https://github.com/deepseek-ai/deepseek-harness/blob/master/apps/desktop/README.md#bundled-command-runtime)への参照だけで、実物は未検証です。
### 保持の確認範囲と残る検証
隔離 CLI で、home の cordis.patch.yml に別の組込みプラグイン `time-context` の `timeZone: Asia/Tokyo`、profile の cordis.patch.yml に確認用内容、profile の package.json に無関係な独自フィールドを置きました。追加 → 新版 tgz への入れ直し → 削除の各直後に、両 patch の完全一致と独自フィールドの保持を確認しました。これはファイル保持の確認で、当該設定を使う Host の実行や別の第三者プラグインの動作確認ではありません。
追加の手動確認では、既存の `startDsh` と偽 LLM を使い、新しい HOME と DSH_HOME(`tmp/dsh-integration/run-1791174147190/` 以下)で Host を loopback 起動しました。画面から workspace `home`、会話「Issue18 データ保持の確認」と偽 LLM の返答を作り、別の組込みプラグイン `@deepseek-ai/dsh-bash-sandbox` の `timeoutMs` を 61000 に変更して保存・再読み込みしました。利用者の環境は使っていません。
0.0.8 の実 tgz を基に、`tmp/issue18-lifecycle/next/package/` に版だけ 0.0.9 と HTML の目印を付けたコピー、`next10/package/` に版 0.0.10 と HTML の目印、Host の応答ヘッダー `x-issue18-host-version: 0.0.10` を付けたコピーを作りました。コピー内で `npm pack --ignore-scripts` を行い、リポジトリの版は 0.0.8 のままです。HTML の変化だけでは Host コードの更新を証明できないため、判定には後者のヘッダーを使いました。
| 操作(Host を動かしたまま実行) | 再起動前 | 再起動後 |
|---|---|---|
| 新版の tgz を `add file:` で再追加 | `/m3e/` 200、HTML は新版、Host の識別ヘッダーは無し(旧コード) | 200、HTML・Host のヘッダーとも新版 |
| 元の 0.0.8 tgz を再追加 | 200、HTML は元に戻るが、新版の Host ヘッダーが残る | 200、新版ヘッダーが消え、元の Host コードへ戻る |
| `remove dsh-webui-m3e` | `/m3e/` 404、標準画面 200。標準画面で同じ会話と返答を表示 | 404、標準画面 200。同じ会話と返答を表示 |
| 削除して再起動した未導入状態から新版 tgz を追加 | 200、新版 HTML と Host ヘッダー。会話と設定を表示 | 200、同じ内容を表示 |
全段階で標準画面の HTTP 200 を確認しました。更新・復帰の前後と再起動後に同じ会話の本文・返答を表示でき、シェルの待機時間 61000 も保持しました。削除中は profile の patch に同じ値が残り、再追加後も設定画面で 61000 を確認しています。workspace も標準画面の一覧と M3E に残りました。**この環境では追加・削除は再起動なしで反映され、更新・旧版復帰は Host コードを切り替えるため再起動が必要でした。** 判定記録は `tmp/issue18-lifecycle/evidence.json`、削除後の標準画面は `removed-classic.png` です。手動操作中のセレクター不一致と URL の `#` 重複は修正して再確認し、失敗した操作を成功には数えていません。
この導入ライフサイクルの自動の試験はありません。第三者プラグイン、長期利用の既存データ、本物の LLM、Desktop は未検証です。GitHub URL 経由の保持は今回のリリース後確認にも含めておらず、下の手順で別途確認します。npm の経路は将来 npm に公開する場合に別途確認します。
#### 比較用 tgz を再現する
リポジトリのルートで、未使用の `tmp/issue18-compare/` を使います。元の版は 0.0.8 のまま、コピーだけを試験用の 0.0.10 にします。既存のフォルダがある場合は全コマンドで別の名前に揃えてください。このコピーは公開しません。
```bash
mkdir -p tmp/issue18-compare/next
pnpm pack --pack-destination tmp/issue18-compare/base
tar -xzf tmp/issue18-compare/base/dsh-webui-m3e-0.0.8.tgz -C tmp/issue18-compare/next
```
`tmp/issue18-compare/make-copy.mjs` に次を保存します。HTML の `` と Host の 2 箇所の 200 応答だけに印を付け、想定した差分を当てられない場合は停止します。
```js
import { readFileSync, writeFileSync } from 'node:fs'
import assert from 'node:assert/strict'
const dir = new URL('./next/package/', import.meta.url)
const read = path => readFileSync(new URL(path, dir), 'utf8')
const write = (path, text) => writeFileSync(new URL(path, dir), text)
const manifest = JSON.parse(read('package.json'))
assert.equal(manifest.version, '0.0.8')
const html = read('dist/index.html')
assert.equal(html.split('').length, 2)
const host = read('lib/index.js')
const response = 'res.writeHead(200, {'
assert.equal(host.split(response).length - 1, 2)
manifest.version = '0.0.10'
write('package.json', JSON.stringify(manifest, null, 2) + '\n')
write('dist/index.html', html.replace('', ''))
write('lib/index.js', host.replaceAll(response, response + ' "x-issue18-host-version": "0.0.10",'))
```
```bash
node tmp/issue18-compare/make-copy.mjs
cd tmp/issue18-compare/next/package
npm pack --ignore-scripts
cd ../../../..
```
ここだけ `--ignore-scripts` を使うのは、ビルド済みの比較用コピーの印を保持するためです。公開用の pack/publish では使いません。元の tgz は `base/dsh-webui-m3e-0.0.8.tgz`、比較用は `next/package/dsh-webui-m3e-0.0.10.tgz` です。この作成手順はレビュー対応時にそのまま実行し、コピーの版・HTML の印・Host の 2 箇所の印と、リポジトリの版が 0.0.8 のままであることを確認しました。前回の Host 実測では、新版への更新直後は HTML の meta だけが変わり、再起動後に Host ヘッダーも `0.0.10` になりました。元へ戻すときは逆に、再起動まで新版のヘッダーが残りました。
#### 同じ環境で追加・更新・復帰・削除する
既存補助を Node REPL で起動し、表示された隔離 HOME を別端末の `read` に入力します(全体 e2e とは同時に動かさないでください。全体 e2e は古い `run-*` を片付けます)。
```bash
node --experimental-repl-await
```
```js
var { startFakeLlm } = await import('./e2e-dsh/fake-llm.ts')
var { startDsh } = await import('./e2e-dsh/dsh-host.ts')
var llm = await startFakeLlm()
var host = await startDsh(llm.url)
console.log(host.workspace)
var { chromium, expect } = await import('@playwright/test')
var { openM3e } = await import('./e2e-dsh/fixtures.ts')
var browser = await chromium.launch({ headless: false })
var page = await browser.newPage({ viewport: { width: 390, height: 844 } })
await openM3e(page, host)
```
画面から上記の workspace・会話・設定を作ります。次は直前の手順で作った比較用 tgz を使います。コマンドを一段階ずつ実行し、上の結果表どおりに再起動の前後を確認してください。
```bash
read M3E_TEST_HOME
dsh_lifecycle() {
env HOME="$M3E_TEST_HOME" DSH_HOME="$M3E_TEST_HOME/../dsh-home" "$PWD/tmp/dsh-integration/dsh-0.2.0-rc.2/node_modules/.bin/dsh" "$@"
}
dsh_lifecycle plugin --profile web add "file:$PWD/tmp/issue18-compare/next/package/dsh-webui-m3e-0.0.10.tgz"
dsh_lifecycle plugin --profile web list dsh-webui-m3e --depth 0 --json
# 画面とヘッダーを確認し、REPL の await host.restart() 後にも確認
dsh_lifecycle plugin --profile web add "file:$PWD/tmp/issue18-compare/base/dsh-webui-m3e-0.0.8.tgz"
dsh_lifecycle plugin --profile web list dsh-webui-m3e --depth 0 --json
# 前後を確認。削除前には標準画面 /?ui=classic を開く
dsh_lifecycle plugin --profile web remove dsh-webui-m3e
dsh_lifecycle plugin --profile web list dsh-webui-m3e --depth 0 --json
# 削除後と再起動後を確認してから、未導入状態へ再追加
dsh_lifecycle plugin --profile web add "file:$PWD/tmp/issue18-compare/next/package/dsh-webui-m3e-0.0.10.tgz"
```
REPL で HTTP の結果を確認する例です。標準画面は削除前後に実際に開き、workspace と会話を選んで本文を確かめます。再起動は各段階の確認後に `await host.restart()`、M3E が入っている段階の認証と再表示は `await openM3e(page, host)` で行います。削除中は M3E を開かず、`await page.goto(host.loginUrl)` の後に `await page.goto(host.origin + '/?ui=classic')` で標準画面へ入ります。
```js
var response = await page.request.get(host.origin + '/m3e/')
console.log(response.status(), response.headers()['x-issue18-host-version'] ?? null)
await page.goto(host.origin + '/?ui=classic')
// すべて確認した後、既存補助の終了処理を待つ
await browser.close()
await host.stop()
await llm.close()
```
### URL 配布の実測(2026-10-05)
DSH 0.2.0-rc.2、Node 26.7.0、pnpm 11.17.0、macOS 27.2 で、`tmp/issue18-url/home` と `dsh-home` に隔離しました。一時 HTTP サーバーは `127.0.0.1:56252`、偽 LLM は `127.0.0.1:56251`。HTTP と偽 LLM は 15 分で自動終了する補助を使い、確認後は補助の終了処理で先に終了しました。Host は空き loopback ポートで起動し、再起動・終了は端末の Ctrl+C で行いました。kill コマンドは使っていません。
基準 tgz は `pnpm pack --pack-destination tmp/issue18-url/base` で作りました。比較用は上の「比較用 tgz を再現する」と同じ手順を `tmp/issue18-url/` で実行し、コピーだけを 0.0.10 にして HTML meta と Host ヘッダーを付けています。リポジトリの版は 0.0.8 のままです。次の関数と同じ環境で実物の CLI を呼びました(ポートは今回の記録値です)。
```bash
dsh_url() {
env HOME="$PWD/tmp/issue18-url/home" DSH_HOME="$PWD/tmp/issue18-url/dsh-home" DEEPSEEK_API_KEY=fake-key-for-m3e-integration DEEPSEEK_BASE_URL=http://127.0.0.1:56251 SSH_CONNECTION=m3e-integration "$PWD/tmp/dsh-integration/dsh-0.2.0-rc.2/node_modules/.bin/dsh" "$@"
}
dsh_url plugin --profile web add http://127.0.0.1:56252/v0.0.8.tgz
dsh_url plugin --profile web list dsh-webui-m3e --depth 0 --json
dsh_url web --no-open --host 127.0.0.1 --port 0
```
標準画面のログイン URL を開いて認証した Chromium から `/m3e/` と `/?ui=classic` を確認しました。次の操作は別端末から、上と同じ関数で 1 段階ずつ実行しています。各段階の `list dsh-webui-m3e --depth 0 --json` で版と `resolved` を読みました。
```bash
dsh_url plugin --profile web add http://127.0.0.1:56252/v0.0.10.tgz
# 画面を確認し、Host を Ctrl+C → 同じ web コマンドで再起動して再確認
dsh_url plugin --profile web add http://127.0.0.1:56252/v0.0.8.tgz
# 同様に再起動の前後を確認
cp tmp/issue18-url/base/dsh-webui-m3e-0.0.8.tgz tmp/issue18-url/same.tgz
dsh_url plugin --profile web add http://127.0.0.1:56252/same.tgz
cp tmp/issue18-url/next/package/dsh-webui-m3e-0.0.10.tgz tmp/issue18-url/same.tgz
dsh_url plugin --profile web add http://127.0.0.1:56252/same.tgz
dsh_url plugin --profile web add http://127.0.0.1:56252/redirect.tgz
dsh_url plugin --profile web add http://127.0.0.1:56252/missing.tgz
# 標準画面へ切り替えてから削除
dsh_url plugin --profile web remove dsh-webui-m3e
```
サーバーは版別の URL を各 tgz へ、`same.tgz` を差し替え対象のファイルへ対応させました。`redirect.tgz` は HTTP 302 と `Location: /v0.0.10.tgz`、`missing.tgz` は 404 を返します。応答に Cache-Control・ETag は付けていません。サーバー・CLI 呼出し・画面確認の補助と、全コマンドのログは `tmp/issue18-url/` に保存しています。公開用の補助や自動試験として追加したものではありません。
| 確認 | 結果 |
|---|---|
| 0.0.8 の URL を追加 | exit 0。list の version は 0.0.8、resolved と profile の dependencies は同じ URL。M3E の bundle 登録あり。利用者側の build は無し |
| Host を起動 | `/m3e/` 200、標準画面 200。M3E の見出しをブラウザで確認 |
| 別 URL の 0.0.10 へ更新 | exit 0、list は 0.0.10。再起動前は HTML のみ新版、再起動後に Host ヘッダーも 0.0.10 |
| 前の URL の 0.0.8 へ戻す | exit 0、list は 0.0.8。再起動前は新版 Host ヘッダーが残り、再起動後に消える。標準画面も 200 |
| 同じ URL の中身を 0.0.8 → 0.0.10 に差し替えて再追加 | exit 0、`downloaded 1`、list は 0.0.10、HTML も新版。サーバーの GET と応答 SHA-256 の変化を確認。今回の環境では古いキャッシュを使わず新しい中身を取得した |
| 302 を返す URL を追加 | exit 0、list は 0.0.10、resolved は転送元 URL。サーバーに 302 と転送先 200 の記録。再起動後に新版 HTML・Host ヘッダーと `/m3e/` 200 を確認 |
| 存在しない URL を追加 | exit 1、`ERR_PNPM_FETCH_404`、`GET …/missing.tgz: Not Found - 404`、`dsh: plugin command failed; diagnostics: …/pnpm.log`。既存の転送元 URL の依存・bundle 登録は残った |
| 削除 | exit 0、list から M3E が消える。稼働中の `/m3e/` は 404、標準画面は 200 |
**同じ URL が同じ版・内容を表す保証を失うため、リリースの添付ファイルを同じ名前で差し替えません。** この実測は「常に古いキャッシュが使われる」という根拠ではありません。GitHub/CDN での差し替え・別の pnpm・別利用者のキャッシュ挙動は未検証です。直すときは版を上げ、別のタグ・リリース・ファイル名・URL にします。1 つのパッケージ版に対してタグ `v<版>` とリリースを 1 つずつ対応させます。
上表はリリース前のローカル HTTP の実測です。GitHub 上の HTTPS・転送・取得・導入は、下記の [v0.0.8 リリース後の実行記録](#v008-リリース後の実行記録2026-10-05) で別に確認しました。設定・会話・別プラグインの保持はローカル URL と file: の確認範囲に限り、GitHub URL 経由では未検証です。
### 今回の検証結果
指示役が 2026-10-05、PR #20 マージ後の main `d00515e0776f46608a5c3ffe70eb529974ecf744` でリリース前の確認列を実行し、すべて成功しました。
| リリース前の確認 | 結果 |
|---|---|
| `pnpm install --frozen-lockfile`、`pnpm typecheck` | 成功 |
| `pnpm test` | 1,039 件成功 |
| `pnpm build`、`pnpm run check:pack` | 成功。配布物は 19 ファイル |
| 偽データ e2e | 159 件成功。既定の 5191 が使用中だったため、下記の一時設定をポート 5230 に揃えて実行 |
| 実 DSH 0.2.0-rc.2 の統合試験を 2 回 | 各回、通常 33 件成功と既知の差の期待失敗 1 件 |
| `pnpm pack` と `npm pack`、各 tgz の `node scripts/check-pack.ts ` | 両方成功。リリースには pnpm の tgz を添付 |
リリース後の GitHub URL 導入・画面・削除・file: からの移行も指示役が確認しました。詳細と未検証項目は [実行記録](#v008-リリース後の実行記録2026-10-05) に分けています。以下は実装時の履歴です。URL 配布への切り替えでも typecheck・test・build・check:pack を再実行し、以下の e2e と pnpm/npm の prepack の行は前回(`868487b`)時点で確認しました。
| 確かめ方 | 結果 |
|---|---|
| `pnpm typecheck` | 成功 |
| `pnpm test` | 1,039 件成功、skip なし。配布検査 28 件を含む |
| `pnpm build` | 成功。既存のチャンクサイズ警告は残る |
| `pnpm exec playwright test -c tmp/e2e-alt-port.config.ts --global-timeout=600000` | この worktree 指定の 5218 で偽データ 159 件成功(約 5.6 分)、skip なし。公開者用の既定手順は下記の管理済み設定を使う |
| `env M3E_DSH_VERSION=0.2.0-rc.2 pnpm exec playwright test -c e2e-dsh/playwright.config.ts --global-timeout=600000` を 2 回 | 各 34 件が想定どおり。各回とも通常成功 33 件+既知の差の期待失敗 1 件、予期しない失敗・skip なし(約 3.1 分/3.0 分) |
| `pnpm run check:pack`、pnpm と npm の実 tgz に `node scripts/check-pack.ts ` | 19 ファイルで成功 |
| `pnpm pack` と `npm pack` | 修正後も両方で prepack の build → check を実行し、外側の tgz も各 19 ファイルで成功。初回作業では古い印を付けた `lib/index.js` が再ビルドされることも確認 |
| `npm pack` の tgz を隔離 DSH に file: で追加(初回作業) | exit 0、追加されたパッケージは M3E 1 個。利用側のビルドは走らない |
| 検査スクリプト 2 ファイルへの追加の TypeScript 検査(初回作業) | 成功。`scripts` と `tests` は `tsconfig.host.json` の include にあり、通常の `pnpm typecheck` の対象にも含まれる |
実 DSH の初回作業の結果は `tmp/issue18-cli/real-run-1.json` と `real-run-2.json`、手動確認を追加した時点は `tmp/issue18-lifecycle/real-run-1.json` と `real-run-2.json`、レビュー修正後の今回の結果は `tmp/issue18-review/real-run-1.json` と `real-run-2.json` に保存しています。`e2e-dsh` の成功は file: 導入後の `/m3e/` と通信の根拠です。npm レジストリの名前だけの成功や、導入ライフサイクルのデータ保持まで含めません。後者は上記の手動確認と区別しています。
次は初回作業で **tmp 内の実 tgz を壊して配布検査を直接実行した結果**です。すべて exit 1 で該当する理由を表示しました。これとは別に、追加した単体試験ごとの本体変異も下表のとおり確認しました。
| 壊したコピーの場所・内容 | 検出結果 |
|---|---|
| `dist/index.html` を tgz から外す | missing: dist/index.html |
| `vite.config.ts` を混ぜる | unexpected: vite.config.ts |
| package.json の name だけ変える | package/patch name mismatch |
| `lib/index.js` に `readme-review` を足す | mock code: lib/index.js |
| `lib/index.js` に利用者ホームの絶対パスを足す | absolute home path: lib/index.js |
| package.json に dependencies を足す | runtime dependencies: dependencies |
| package.json に install script を足す | consumer lifecycle: install |
| client の exports を存在しない JS に向ける | missing target: exports../client.default |
| package.json を private にする | private package |
| dsh.bundle を外す | missing bundle patch declaration |
破損コピーと結果は `tmp/issue18-cli/mutant-*/` に残しました。本体への破損変更は残していません。
以下は初回追加確認(`2ade474`)時点の 16 件の記録です。1 件ずつ `scripts/pack-contract.ts` の該当判定を一時的に外し、`node --experimental-strip-types --test --test-name-pattern='<対象試験名>' tests/18-pack.test.ts` が **AssertionError、exit 1** になることを確認しました。各回に本体を復元し、最後に元の内容との完全一致と 16 件すべての成功を確認しています。この後の独立監査で入力の境界不足が見つかったため、レビュー対応で下記の確認を追加しました。行番号は初回時点のものです。
| 試験 | 一時的に壊した判定(pack-contract.ts の行) | 結果 |
|---|---|---|
| 正常な配布物と公開者側 prepack を許可 | 13: 必須ファイルを許可する分岐を無効化 | 落ちた・復元済み |
| 必須ファイル・3 言語 README | 25: 必須ファイルの欠落検査を外す | 落ちた・復元済み |
| JS と CSS assets | 27: assets の存在検査を外す | 落ちた・復元済み |
| 開発用ファイル・設定・マップ・不正パス | 30: 許可一覧の検査を外す | 落ちた・復元済み |
| sourceMappingURL | 33: マップ参照検査を外す | 落ちた・復元済み |
| 偽データの目印 | 34: mock 検査を外す | 落ちた・復元済み |
| 利用者の絶対パス | 32: ホームパス検査を外す | 落ちた・復元済み |
| 重複 | 37: 重複検査を外す | 落ちた・復元済み |
| 不正な manifest | 43: parse エラーの記録を外す | 落ちた・復元済み |
| private | 45: private 検査を外す | 落ちた・復元済み |
| package と patch の名前 | 47: 名前の照合を外す | 落ちた・復元済み |
| 実行時の依存 | 49: 依存検査を外す | 落ちた・復元済み |
| 利用者側 lifecycle | 52: lifecycle 検査を外す | 落ちた・復元済み |
| main | 56: main の参照先検査を外す | 落ちた・復元済み |
| exports | 61: exports の参照先検査を外す | 落ちた・復元済み |
| bundle の patch 宣言 | 64: bundle 宣言検査を外す | 落ちた・復元済み |
結果と各回のログは `tmp/issue18-mutations/report.json` と `1.txt`〜`16.txt` にあります。単体試験だけのパッチは通りました。Host 補助の単独パッチは `M3E_SKIP_BUILD` を含む環境変数参照行、代替補助は環境変数を展開する行が保護対象文字列に見えた可能性があります。フックは理由を「protected path or sensitive filename pattern」としか示さず、原因文字列は特定できていません。変異確認の最初の長いシェル呼び出しも形式を理由に拒否されましたが、共通の決まりで許可されたコマンド形式の修正だけを行い、tmp の作業スクリプトを直接実行しました。
#### レビューと独立監査の指摘への対応
監査で生存した M04/M05/M07/M14/M22/M35 を含め、35 変異を修正後の本体に当て直し、すべて検出しました。追加の 11 変異も検出し、計 46/46 が AssertionError・exit 1 です。M05/M07 は新しいハッシュ付きの名前でもサブディレクトリ/txt を許可する同じ変更、M35 は同一行の空白を必須から任意にする同じ変更を適用しました。1 変異ごとに `git restore` し、修正済みの本体との完全一致を確認しています。
以下は 26 試験それぞれを実際に落とした変更です。未変更の試験も含めて当て直しました。場所は原則 `scripts/pack-contract.ts`、最後の 2 件は `scripts/production-modules.ts` です。
| 試験 | 本体を一時的に壊した箇所・内容 | 検出 |
|---|---|---|
| 正常系・prepack・固定公開ファイル | M04: 許可一覧から sw.js を除く | exit 1 |
| 必須ファイル | M02: 必須ファイル検査を除く | exit 1 |
| JS/CSS 必須 | M08: assets の存在検査を除く | exit 1 |
| 開発用ファイル・設定・map | M10: 許可パス検査を除く | exit 1 |
| 空白・タブを含む sourceMappingURL(JS/CSS) | M14: 空白を扱わない式にする。M15: CSS を検査しない | exit 1 |
| 偽データの目印 | M16: JS を検査しない | exit 1 |
| 利用者の絶対パス | M12: Windows の検査を除く | exit 1 |
| 重複 | M18: 重複検査を除く | exit 1 |
| 不正 manifest | M20: parse エラーの記録を除く | exit 1 |
| private | M21: true/false を反転 | exit 1 |
| package/patch の名前 | M23: 名前の一致検査を除く | exit 1 |
| 実行時依存 | M25: 非空の依存を許可 | exit 1 |
| 利用者側 lifecycle | M27: prepare を許可 | exit 1 |
| main の参照先 | M28: ファイル存在検査を除く | exit 1 |
| exports の参照先 | M31: 先頭の export だけを検査 | exit 1 |
| bundle 宣言 | M34: patch 宣言検査を除く | exit 1 |
| assets 内の開発用の名前 | N01: ハッシュを要求せずドットも許可 | exit 1 |
| assets の下の階層・拡張子 | M05: スラッシュを許可。M07: txt を許可 | exit 1 |
| file URL のホームパス | N04: file URL の検査を除く | exit 1 |
| Windows の大小文字 | N05: 大小文字を区別する式にする | exit 1 |
| patch の name が 2 件 | M22: name の件数検査を除く | exit 1 |
| 字下げのない name(先頭改行も確認) | M35: 字下げを任意にする | exit 1 |
| 正常な生の tar 名と directory record | N03: directory record をファイル扱いにする | exit 1 |
| package/ 外の生の tar 名 | N02: package/ 必須検査を除く | exit 1 |
| 偽モジュール 30 ファイル | N06/N07: shared/feature の判定を除く。N09: Windows 正規化を除く | exit 1 |
| 本番モジュールと observable の例外 | N08: 例外を除く。N11: 例外を別名ファイルにも広げる | exit 1 |
記録は `tmp/issue18-review/mutations/report.json` と各 ID のログです。実 tgz も、監査で通った A12 と、新しく作った package/ 外のハッシュ付き JS、ルートの lib/index.js への置換、assets 内の example.test.js の計 4 件をすべて exit 1 で拒否しました。ルート違反は `invalid archive root`、開発用 JS は `unexpected` です。
モジュール検査の実物確認として、本番の `web/src/main.tsx` に `applyMockOperations` の import と `Object.assign(globalThis, { issue18InjectedMock: applyMockOperations })` を一時追加しました。`pnpm run build:web` は `mock module in production: …/features/settings/mock-mutations.ts` を示して exit 1 になりました。単なる未使用 import ではなく、出力に残る参照を作って確認しています。直後に `git restore web/src/main.tsx` で戻し、通常の build 成功を確認しました。
受け直しでは次の 2 試験を追加し、再監査と同じ変異を 1 件ずつ適用しました。いずれも対象試験が AssertionError・exit 1 になり、毎回 `git restore scripts/pack-contract.ts` の後に元の本体とのバイト一致を確認しました。本体の動作は変更していません。記録は `tmp/issue18-review2/mutations.json` と `N02.txt`、`N13.txt` です。
| 追加した試験 | 一時的な変更 | 結果 |
|---|---|---|
| JS/CSS/woff2 の 7・9・10 文字ハッシュを `unexpected` で拒否 | 再監査 N02: pack-contract.ts:14 の `[\w-]{8}` を `[\w-]{8,10}` にする | 対象試験が失敗、復元済み |
| patch の `name:` と値の間の改行を名前不一致で拒否 | 再監査 N13: pack-contract.ts:58 の `name:[ \t]*` を `name:\s*` にする | 対象試験が失敗、復元済み |
#### 受け入れ条件の判定
Issue の主経路は npm レジストリですが、利用者の指示により GitHub Releases の代替経路で v0.0.8 をリリースしました。npm で満たしたという判定にはせず、同じ目的を URL で満たした範囲と、未検証の範囲を分けます。以下は 2026-10-05 のリリース後確認を反映した判定です。
| 項目 | npm の主経路 | GitHub Releases の代替経路 |
|---|---|---|
| 1. 名前・権限・対応版・配布検査 | 未充足。npm アカウントが無く、公開権限未確認。公開用の設定は維持 | 満たした。名前・0.0.8・DSH 0.2.0-rc.2 と配布検査 28 件を確認。指示役が確認済み main に v0.0.8 を作成し、検査済み tgz を公開 |
| 2. クリーン環境からビルド無しで導入 | 未充足。名前だけでは取得できない | 満たした。新しい隔離環境で GitHub URL だけの add が exit 0、版は 0.0.8、ログイン後 /m3e/ は 200。ソース取得・ビルド・version の手編集は不要 |
| 3. pnpm・Desktop・環境の案内 | 配布先に依存しない案内は充足 | 充足。通常 CLI の PATH 上の pnpm、Node の宣言と実測環境、Desktop 未検証を維持 |
| 4. ローカル tgz 移行・公開版同士の更新 | 未充足。registry への移行・更新は未実施 | 移行は満たした。file: → GitHub URL の add は両方 exit 0、依存参照が URL に変わり再起動後 /m3e/ は 200。公開版同士の更新は 2 つ目の互換版がリリースされてから確認。ローカルの別 URL での版切替は成功 |
| 5. 設定・他プラグイン・会話の保持 | registry 経由は未検証 | ローカル URL と file: の確認範囲に限る。1 workspace・1 会話と返答・別の組込みプラグイン設定の保持を確認。GitHub URL 導入時の保持、第三者プラグイン、長期データは未検証 |
| 6. 前提不足・誤 profile・取得失敗の案内 | 未公開の名前追加が 404 になることを案内 | 確認した範囲で満たした。PATH 不足、誤 profile と別 profile 作成、URL の 404 と診断ログを案内。実際の GitHub URL の 302 → 200 と導入も成功 |
| 7. 旧版復帰・削除・標準画面 | 公開 npm 版での確認は未実施 | 削除は満たした。GitHub URL 導入後、稼働中と再起動後とも /m3e/ は 404、標準画面は 200、一覧と依存から消える。旧版復帰はローカル URL では成功、公開版同士では 2 つ目の互換版のリリース後に確認 |
### リリースする人の手順
次の版でも、順番は **PR をマージ → main の先頭で下記の確認 → pnpm pack と tgz の検査 → main の確認済みコミットにタグを付けて GitHub Release と添付 tgz を公開 → リリース後の確認 → 結果を文書へ記録**です。マージとリリースは続けて行います。v0.0.8 では指示役がこの順に実行しました。以下のコマンドでは `read M3E_RELEASE_VERSION` に package.json と一致する新しい版を入力し、説明の版・URL も揃えます。既存の 0.0.8 は再利用しません。
GitHub の対象リポジトリへリリースを作成できるアカウントと権限を実行者が確認します。この経路に npm アカウントは要りません。リリースとタグ `v<版>`、package.json の版を 1 対 1 に対応させ、添付名は `dsh-webui-m3e-<版>.tgz` にします。既存のタグ・リリース・添付は使い直さず、修正時は新しい版にします。
PR をマージした後、新しく clone した main または更新済み main のルートで実行します。`git branch --show-current` が main、`git status --short` が空であることを確認し、`git rev-parse HEAD` と `git ls-remote origin refs/heads/main` のハッシュを照合します。異なる場合は公開せず、main の先頭を用意し直します。
```bash
read M3E_RELEASE_VERSION
pnpm install --frozen-lockfile
pnpm exec playwright install chromium
pnpm typecheck
pnpm test
pnpm build
pnpm run check:pack
pnpm exec playwright test -c e2e/playwright.config.ts --global-timeout=600000
env M3E_DSH_VERSION=0.2.0-rc.2 pnpm exec playwright test -c e2e-dsh/playwright.config.ts --global-timeout=600000
env M3E_DSH_VERSION=0.2.0-rc.2 pnpm exec playwright test -c e2e-dsh/playwright.config.ts --global-timeout=600000
pnpm pack --pack-destination tmp/release
node scripts/check-pack.ts "tmp/release/dsh-webui-m3e-$M3E_RELEASE_VERSION.tgz"
mkdir -p tmp/release-npm
npm pack --pack-destination tmp/release-npm
node scripts/check-pack.ts "tmp/release-npm/dsh-webui-m3e-$M3E_RELEASE_VERSION.tgz"
```
既定の 5191 が使用中の場合だけ、空きポートを選んで一時設定を用意します。たとえば 5218 が空いていれば `mkdir -p tmp` を行い、`tmp/e2e-alt-port.config.ts` に次を保存します。baseURL・command・port の数値はすべて同じ値に揃え、既存サーバーは流用しません。
```ts
import base from '../e2e/playwright.config.ts'
import { fileURLToPath } from 'node:url'
export default {
...base,
testDir: fileURLToPath(new URL('../e2e/', import.meta.url)),
use: { ...base.use, baseURL: 'http://localhost:5218' },
webServer: { ...base.webServer, command: 'pnpm exec vite --host 127.0.0.1 --port 5218 --strictPort', port: 5218 },
}
```
この場合だけ、確認の列の偽データ e2e を `pnpm exec playwright test -c tmp/e2e-alt-port.config.ts --global-timeout=600000` に置き換えます。新規 clone の既定手順は管理済みの設定だけで実行できます。
すべての確認後、題名は `M3E v<版>` とします。説明は `tmp/release-notes.md` に次の形で保存します。版と URL は対象の版に揃え、作者名・メール・ローカルパス・操作ログなどの個人情報は書きません。
```markdown
DSH 0.2.0-rc.2 向けのビルド済み Web UI プラグインです。
- ソース取得・ビルド・version の手編集は不要です。
- 導入: `dsh plugin --profile web add https://github.com/AliceSaikawa/dsh-webui-m3e/releases/download/v0.0.8/dsh-webui-m3e-0.0.8.tgz`
- 更新・旧版への復帰後は、対象プロファイルの Host を再起動してください。
- 本物の LLM と iPhone Safari・ホーム画面の Web アプリ、Desktop での動作は未確認です。
- 詳しい手順と確認範囲: https://github.com/AliceSaikawa/dsh-webui-m3e/blob/v0.0.8/README.md
```
公開する人は `read M3E_RELEASE_COMMIT` に、検査した main の先頭の完全な SHA を入力します。リリース直前にも HEAD とリモート main が同じであることを照合し、違う場合は停止して更新後の main で確認し直します。対象版のタグが既に表示された場合も停止し、再利用しません。
```bash
git branch --show-current
git status --short
git rev-parse HEAD
git ls-remote origin refs/heads/main
git ls-remote --tags origin "refs/tags/v$M3E_RELEASE_VERSION"
read M3E_RELEASE_COMMIT
node scripts/check-pack.ts "tmp/release/dsh-webui-m3e-$M3E_RELEASE_VERSION.tgz"
gh release create "v$M3E_RELEASE_VERSION" "tmp/release/dsh-webui-m3e-$M3E_RELEASE_VERSION.tgz" --repo AliceSaikawa/dsh-webui-m3e --target "$M3E_RELEASE_COMMIT" --title "M3E v$M3E_RELEASE_VERSION" --notes-file tmp/release-notes.md
```
`--target` にコミットを指定すると、存在しないタグをそのコミットに作る契約は [GitHub CLI の説明](https://cli.github.com/manual/gh_release_create)と実物の `gh release create --help` で確認しました。v0.0.8 は指示役が次のコマンドで作成しました(実行済みの記録であり、再実行しません)。添付するのは検査済みの pnpm の tgz であり、GitHub が自動生成する Source code の archive ではありません。
```bash
gh release create v0.0.8 tmp/release/dsh-webui-m3e-0.0.8.tgz --repo AliceSaikawa/dsh-webui-m3e --target d00515e0776f46608a5c3ffe70eb529974ecf744 --title 'M3E v0.0.8' --notes-file tmp/release-notes.md
```
続けて次節の GitHub URL だけでの追加・版確認・`/m3e/`・削除と標準画面を確認します。完了したら README 以外の次の場所へ、リリース日、版、タグ、main のハッシュ、実行環境、コマンド、結果、残る未検証事項を記録します。
| 更新する場所 | 更新する内容 |
|---|---|
| この文書の「配布」「今回の検証結果」「受け入れ条件の判定」「リリースのあとに確かめること」 | 公開した版の情報、GitHub URL の結果と判定を更新。npm 未公開は別の状態として残す |
| `docs/dsh-compatibility.md` の「GitHub Releases 配布版の記録」 | GitHub URL の導入・更新・復帰で確認できた範囲と残件を更新 |
| `docs/handoff.md` の「GitHub Releases 配布の記録」 | リリース日・版・検証結果と次に渡すことを更新 |
### リリースのあとに確かめること
以下は次のリリースでも使う確認手順です。例は公開済みの 0.0.8 なので、次の版では URL・期待する version・tgz 名を対象版に揃えます。今回の実行結果は末尾の記録に分け、手順に含まれる保持確認をすべて実行済みとは扱いません。DSH 0.2.0-rc.2 が上記 tmp にある開発環境で、未使用の `tmp/release-smoke/` を用意します。既に存在する場合は新しい名前を選びます。既存の利用者環境をコピーしません。
```bash
mkdir -p tmp/release-smoke/home tmp/release-smoke/dsh-home
```
一つ目の端末で偽 LLM を起動し、表示された loopback URL を控えます。完了後は Ctrl+C で終了します。
```bash
node --input-type=module -e 'const {startFakeLlm}=await import("./e2e-dsh/fake-llm.ts"); const llm=await startFakeLlm(); console.log(llm.url); process.on("SIGINT",async()=>{await llm.close(); process.exit(0)})'
```
二つ目の端末で `read` にその URL を入力し、隔離した CLI を定義します。初回追加はローカル tgz を経由せず、GitHub の添付 URL だけで行います。
```bash
read M3E_FAKE_URL
dsh_smoke() {
env HOME="$PWD/tmp/release-smoke/home" DSH_HOME="$PWD/tmp/release-smoke/dsh-home" DEEPSEEK_API_KEY=fake-key-for-m3e-integration DEEPSEEK_BASE_URL="$M3E_FAKE_URL" SSH_CONNECTION=m3e-integration "$PWD/tmp/dsh-integration/dsh-0.2.0-rc.2/node_modules/.bin/dsh" "$@"
}
dsh_smoke plugin --profile web add https://github.com/AliceSaikawa/dsh-webui-m3e/releases/download/v0.0.8/dsh-webui-m3e-0.0.8.tgz
dsh_smoke plugin --profile web list dsh-webui-m3e --depth 0 --json
dsh_smoke web --no-open --host 127.0.0.1 --port 0
```
list の `version` が 0.0.8 であることと、プロファイルの `package.json` の `dependencies["dsh-webui-m3e"]` が指定した GitHub の URL であることを確認します。list の `resolved` は、pnpm が転送先の URL を記録する場合があるので、指定した URL と違っていても失敗とは扱わず、実際の値を記録します。表示された標準画面のログイン URL をブラウザで開いてから同じ origin の `/m3e/` を開きます。workspace を一つ追加し、会話を一つ作って偽 LLM の返答まで保存します。別プラグインの設定も一つ保存し、その値・workspace・会話の本文を控えます。
初回のリリースでも、まず削除まで確かめます。標準画面 `/?ui=classic` へ切り替え、Host を Ctrl+C で正常終了してから実行します。
```bash
dsh_smoke plugin --profile web remove dsh-webui-m3e
dsh_smoke plugin --profile web list dsh-webui-m3e --depth 0 --json
dsh_smoke web --no-open --host 127.0.0.1 --port 0
```
標準画面にログインして、同じ workspace・会話・設定が残ることと `/m3e/` が 404 になることを確認し、Host を Ctrl+C で終了します。続いて次の移行を 1 段階ずつ行い、その都度上の web コマンドで再起動して同じ内容を確認します。初回リリースでは file: → GitHub URL の移行までを確認し、2 版間の更新・復帰は互換版が 2 つリリースされてから行います。
```bash
# ローカル tgz から GitHub URL への移行:手元の互換版 tgz を使う
dsh_smoke plugin --profile web add "file:$PWD/tmp/release/dsh-webui-m3e-0.0.8.tgz"
dsh_smoke plugin --profile web add https://github.com/AliceSaikawa/dsh-webui-m3e/releases/download/v0.0.8/dsh-webui-m3e-0.0.8.tgz
dsh_smoke plugin --profile web list dsh-webui-m3e --depth 0 --json
# 2 つ以上の互換版がリリースされた後、旧版と新版を入力する(先頭の v は付けない)
read M3E_PREVIOUS_VERSION
read M3E_NEW_VERSION
dsh_smoke plugin --profile web add "https://github.com/AliceSaikawa/dsh-webui-m3e/releases/download/v$M3E_PREVIOUS_VERSION/dsh-webui-m3e-$M3E_PREVIOUS_VERSION.tgz"
dsh_smoke plugin --profile web add "https://github.com/AliceSaikawa/dsh-webui-m3e/releases/download/v$M3E_NEW_VERSION/dsh-webui-m3e-$M3E_NEW_VERSION.tgz"
dsh_smoke plugin --profile web list dsh-webui-m3e --depth 0 --json
# 旧版へ戻す
dsh_smoke plugin --profile web add "https://github.com/AliceSaikawa/dsh-webui-m3e/releases/download/v$M3E_PREVIOUS_VERSION/dsh-webui-m3e-$M3E_PREVIOUS_VERSION.tgz"
dsh_smoke plugin --profile web list dsh-webui-m3e --depth 0 --json
# 標準画面へ切り替えてから終了・削除
dsh_smoke plugin --profile web remove dsh-webui-m3e
dsh_smoke plugin --profile web list dsh-webui-m3e --depth 0 --json
dsh_smoke web --no-open --host 127.0.0.1 --port 0
```
削除後は新しく表示された URL で標準画面にログインし、`/?ui=classic` で同じ workspace・会話・設定を確認します。第三者プラグインの依存と動作、長期利用データも別途検証してください。初回の 0.0.8 リリースだけでは「公開された旧版から新版」の検証は完了しません。以前配布した別内容の 0.0.7 を、0.2.0-rc.2 の互換旧版と見なしてはいけません。
#### v0.0.8 リリース後の実行記録(2026-10-05)
以下は指示役による実測です。新しい隔離環境を使い、HOME と DSH_HOME は `tmp/release-smoke-*` 以下、Host は 127.0.0.1 のみ、LLM のキーは無しでした。環境は macOS 27.2、Node.js 26.7.0、pnpm 11.17.0、DSH 0.2.0-rc.2 です。表の `` は `https://github.com/AliceSaikawa/dsh-webui-m3e/releases/download/v0.0.8/dsh-webui-m3e-0.0.8.tgz`、`` はローカルの互換版 tgz です。
| 実行・確認 | 結果 |
|---|---|
| 添付 URL の取得と SHA-256 照合 | 302 の転送 1 回の後に 200。取得したファイルは添付の SHA-256 `1d77d0ac8cc85333c1937927f879d5c83774c00ee3519080f584f7120b9bae24` と一致。転送応答の Cache-Control は `no-cache` |
| `dsh plugin --profile web add ` | exit 0。利用者側のソース取得・ビルド無しで導入 |
| `dsh plugin --profile web list dsh-webui-m3e --depth 0 --json` と profile の package.json | version は 0.0.8。resolved と dependencies は指定した GitHub URL のままで、転送先には置き換わらなかった。これは今回の実測で、すべての転送の保証ではない |
| Host 起動、標準画面でログイン後の `/m3e/` と `/?ui=classic` | 両方 200。M3E の HTML に `` と `/m3e/assets/index-…` の参照を確認 |
| Host 稼働中に `dsh plugin --profile web remove dsh-webui-m3e` | exit 0。`/m3e/` は 404、標準画面は 200。一覧と profile の依存から M3E が消えた |
| 削除後の Host 再起動 | `/m3e/` は 404、標準画面は 200 のまま |
| `dsh plugin --profile web add file:` → `dsh plugin --profile web add ` | 両方 exit 0。依存参照が file: から GitHub URL に変わり、再起動後 `/m3e/` は 200 |
**未検証**:GitHub URL で導入した状態での会話・workspace・設定の保持(ローカル URL と file: の確認のみ)、公開版同士の更新・旧版復帰(2 つ目の版がまだ無い)、Desktop、本物の LLM、iPhone Safari とホーム画面に追加した Web アプリ、ほかの作者のプラグイン、長期利用データ。npm レジストリはアカウントが無いため未公開です。上の偽 LLM を使う保持確認手順は、残る検証のために維持しています。
### あとで npm にも出す場合
**npm アカウントとパッケージの公開権限が必要です。** 現在は npm に公開していません。公開者が [npm の公開手順](https://docs.npmjs.com/creating-and-publishing-unscoped-public-packages/)と [2 要素認証](https://docs.npmjs.com/requiring-2fa-for-package-publishing-and-settings-modification/)に沿って、自分の環境で準備します。`npm view dsh-webui-m3e versions --json` で名前・版を確認し、404 を権限の証明にはしません。
npm に公開する版では、**公開前に** 3 言語 README を npm の名前による導入案内へ更新し、npm 未公開の文を外します。配布内容が変わるので新しい版に上げ、同じ版の GitHub tgz と内容を揃えます。上の main の確認列を通し、公開者がソースのルートから `npm publish` を実行します。`prepack` の build・検査は止めません。公開後は新しい隔離 HOME / DSH_HOME を用意し、上の `dsh_smoke` をその環境に向けて、次を確認します。
```bash
dsh_smoke plugin --profile web add dsh-webui-m3e
dsh_smoke plugin --profile web list dsh-webui-m3e --depth 0 --json
dsh_smoke web --no-open --host 127.0.0.1 --port 0
# /m3e/ を確認して Host を正常終了し、公開された互換版を指定して更新・復帰も確認
read M3E_NEW_VERSION
read M3E_PREVIOUS_VERSION
dsh_smoke plugin --profile web add "dsh-webui-m3e@$M3E_NEW_VERSION"
dsh_smoke plugin --profile web add "dsh-webui-m3e@$M3E_PREVIOUS_VERSION"
dsh_smoke plugin --profile web remove dsh-webui-m3e
```
版の変数は公開済みの互換版を `read` で入力します。file:/GitHub URL から npm への移行も、同じ profile で名前と版を指定して追加し直し、各段階で Host の再起動、設定・会話保持、削除後の標準画面を確認します。成功後に development.md・dsh-compatibility.md・handoff.md の公開状況と判定を更新します。
問題があれば原則として新しい版で修正します。[npm の取り消しポリシー](https://docs.npmjs.com/policies/unpublish/)では、新規公開から 72 時間以内でも他の公開パッケージから依存されていないことが条件です。それ以後は、依存無しに加え、直近 1 週間のダウンロード数が 300 未満、所有者/管理者が 1 人という条件があります。取り消しても同名同版は再利用できず、全版を取り消すと新しい版も 24 時間は公開できません。公開者が公式の条件を確認して判断します。