diff --git a/docs/specifications/base-image-chain-build.md b/docs/specifications/base-image-chain-build.md
index a792d0ff..0e080ee5 100644
--- a/docs/specifications/base-image-chain-build.md
+++ b/docs/specifications/base-image-chain-build.md
@@ -12,7 +12,7 @@ Dockerfile から直の親イメージを読む規則(直の親の読み方)
(`bin/devbase`)と `--expires` の判定(`lib/devbase`)が同じ Dockerfile から同じ直の親を読む。
読む Dockerfile(プロジェクトの Dockerfile)は、`docker compose build` が建てる開発サービス
-(`DEV_SERVICE_NAME`、既定 `dev`)の `build` から決める。compose の構成は `docker compose config` で読み、
+(`DEV_SERVICE_NAME`。未設定か空なら `dev`)の `build` から決める。compose の構成は `docker compose config` で読み、
通常のビルドと `--expires` の判定が同じ決め方(Dockerfile の場所の決め方)を使う。
利用者向けの説明は
@@ -431,10 +431,11 @@ compose の理由(`required variable MISSING is missing a value: need it` な
### 決定 6: `docker compose build` が建てるサービスと同じサービスを見るため、開発サービス名は `bin/devbase` が `--service` で渡す
-`bin/devbase` は `${DEV_SERVICE_NAME:-dev}`(空なら `dev`)を `docker compose build` に渡すが、Python の
-`get_dev_service_name()` は空の値をそのまま返す。入口が自分で名前を決めると、`DEV_SERVICE_NAME=` と空で書いた
-プロジェクトで、建てるサービスと場所を決めるサービスが分かれる。`--expires` の判定が空の値で開発サービスを
-見失う件は #425 にある。
+`bin/devbase` は `${DEV_SERVICE_NAME:-dev}`(未設定か空なら `dev`)を `docker compose build` に渡し、同じ値を
+`--service` で入口へ渡す。建てるサービスと場所を決めるサービスが、1 つの値から決まる。入口が自分で名前を決めると、
+2 つが揃うかは Bash と Python の 2 つの読み方が同じ規則であることに頼る。Python の `get_dev_service_name()` も
+未設定か空なら `dev` と読み、今は 2 つの読み方は揃っている([開発サービス名の読み方](dev-service-name.md)、#425)が、
+読み方が将来分かれても建てるサービスと場所を決めるサービスが分かれないよう、`--service` で渡す形を保つ。
### 決定 7: 実行時用の `.env` がまだ無いプロジェクトでも建てられるよう、入口は `--no-env-resolution` で構成を読む
@@ -447,6 +448,7 @@ compose の理由(`required variable MISSING is missing a value: need it` な
- [base イメージの中身の絞り込み](base-image-contents.md)(派生イメージの一覧)
- [base イメージの文字の描画と、文書を扱う道具](base-image-rendering.md)(ブラウザの派生イメージ)
- [位置引数の解決(プロジェクト名・イメージ名)](cli-argument-resolution.md)(名前の形 `is_single_segment_name`)
+- [開発サービス名の読み方(`DEV_SERVICE_NAME`)](dev-service-name.md)(`--service` で渡す名前の決め方)
- [用語集: イメージの継承(`image-lineage`)](../glossary.md#イメージの継承image-lineage)
- [コンテナ操作ガイド](../user/container-operations.md)
- 実装 PR: devbasex/devbase#427(#415)・devbasex/devbase#432(`--no-env-resolution`)
diff --git a/docs/specifications/dev-service-name.md b/docs/specifications/dev-service-name.md
new file mode 100644
index 00000000..c9e1ddb0
--- /dev/null
+++ b/docs/specifications/dev-service-name.md
@@ -0,0 +1,135 @@
+# 開発サービス名の読み方(`DEV_SERVICE_NAME`)
+
+## 概要
+
+プロジェクトの compose の構成で、どのサービスを開発サービスとして扱うかは、プロジェクトの `env` に書く
+環境変数 `DEV_SERVICE_NAME` で決める。**未設定か空(長さ 0 の文字列)なら `dev`、それ以外は値をそのまま使う。**
+
+この名前は 2 つの実行の単位がそれぞれ決める。`bin/devbase`(Bash)は `${DEV_SERVICE_NAME:-dev}` で、
+`lib/devbase`(Python)は `lib/devbase/volume/compose.py` の `get_dev_service_name()` で決め、2 つは同じ値から
+同じ名前を得る。`DEV_SERVICE_NAME=` と空で書いたプロジェクトでも、`build` が建てるサービスと、`up`・`login`・
+`open`・`post-start`・`scale`・`--expires` の判定・token の配布が扱うサービスは同じ `dev` になる。
+
+## 用語
+
+開発サービス名の定義は [用語集: Compose の構成(`compose`)](../glossary.md#compose-の構成compose) にある。
+
+## コンテキスト
+
+変更は Compose の構成(`compose`)のコンテキストに閉じる。開発サービス名を使う側(コマンドの入口 `cli`・
+イメージの継承 `image-lineage`)は名前を受け取るだけで、読み方を自分では持たない。
+
+## 背景
+
+`bin/devbase` は以前から空の値を `dev` と読んでいたが、Python の `get_dev_service_name()` は空の値をそのまま
+返していた。`DEV_SERVICE_NAME=` と書いたプロジェクトでは、`build` は `dev` を建てるのに、Python の経路は空の名前の
+サービスを探して開発サービスを見失った(#425)。Python の側を `bin/devbase` の読み方へ揃え、`bin/devbase` は
+変えていない。
+
+## 対象範囲
+
+- `lib/devbase/volume/compose.py` の `get_dev_service_name()` が、`DEV_SERVICE_NAME` から開発サービス名を決める規則
+- `bin/devbase` の `${DEV_SERVICE_NAME:-dev}` と同じ値を決めること
+
+含まない:
+
+- 空白だけの値・サービス名に使えない文字を含む値の検証(名前の形の検証)
+- `DEV_SERVICE_NAME` 以外の環境変数の空の値の扱い
+- 空の値を書いたプロジェクトへの警告
+
+## 構成要素
+
+| 要素 | 置き場所 | 責務 |
+| --- | --- | --- |
+| Python の読み方 | `lib/devbase/volume/compose.py` の `get_dev_service_name()` | 環境変数から開発サービス名を決める。Python で `DEV_SERVICE_NAME` を読む唯一の場所 |
+| Bash の読み方 | `bin/devbase` の `cmd_build` の `${DEV_SERVICE_NAME:-dev}` | 名前を決め、`docker compose build` と `project_dockerfile --service` へ渡す([派生イメージの継承の連なりのビルド](base-image-chain-build.md)の決定 6) |
+| 呼び出し側 | `lib/devbase/commands/container.py` の `cmd_up`・`cmd_login`・`cmd_open`・`cmd_post_start`・`cmd_scale`・`_resolve_dev_service`・`_ensure_images`・`_dev_instance_indices`・`_maybe_open_editor`・`_open_editor_at`、`lib/devbase/commands/env.py` の `_push_token_to_running`、`lib/devbase/volume/compose.py` の `generate_scaled_compose` | 名前を受け取り、compose の構成から開発サービスを引く。空の値を自分では確かめない |
+
+`DEV_SERVICE_NAME` は `bin/devbase` が `env` を `set -a` で読んで環境へ載せるほか、Python の `_load_project_env`
+(`lib/devbase/commands/container.py`)も `env` を読み直して載せる。どちらの経路でも空の値は環境変数として
+Python へ渡り、`get_dev_service_name()` が `dev` と読む。
+
+```mermaid
+graph LR
+ ENV["プロジェクトの env
DEV_SERVICE_NAME="]
+ B["bin/devbase の cmd_build
${DEV_SERVICE_NAME:-dev}"]
+ G["get_dev_service_name()"]
+ C[呼び出し側]
+ PD[project_dockerfile]
+ DC[docker compose]
+ ENV -->|set -a で読む| B
+ ENV -->|set -a / _load_project_env| G
+ B -->|--service dev| PD
+ B -->|build dev| DC
+ G -->|dev| C
+ C -->|config / up / exec| DC
+```
+
+## 仕様
+
+### 読み方
+
+| `DEV_SERVICE_NAME` の値 | 開発サービス名 |
+| --- | --- |
+| 未設定 | `dev` |
+| 空(`''`) | `dev` |
+| 空白だけ(`' '` など) | 値のまま(`' '`)。前後の空白を削らない |
+| それ以外(`app` など) | 値のまま |
+
+失敗の形は無く、名前は必ず決まる。構成にその名前のサービスが無いときの扱いは呼び出し側が持つ。
+呼び出しのたびに環境変数から決め直し、保存しない。
+
+### 常に成り立つ条件
+
+| # | 条件 | 破れたときの扱い |
+| --- | --- | --- |
+| I1 | `DEV_SERVICE_NAME` が未設定か空(長さ 0)なら、名前は `dev` である | 単体のテストが落ちる |
+| I2 | `DEV_SERVICE_NAME` が空でなければ、名前はその値を変えずに使う(空白だけの値も前後の空白を削らない) | 単体のテストが落ちる |
+| I3 | 同じ `DEV_SERVICE_NAME` の値から、`bin/devbase` の `${DEV_SERVICE_NAME:-dev}` と `get_dev_service_name()` は同じ名前を決める | I1・I2 の単体のテストが、Bash の `${VAR:-word}` の規則(未設定か空のときだけ置き換え、空白だけの値は置き換えない)と同じ表で縛る |
+| I4 | `lib/devbase/` で `DEV_SERVICE_NAME` を読むのは `volume/compose.py` の 1 か所だけである | 環境変数の読み取りを集める検査のテストが落ちる |
+
+### 設計の判断
+
+- **空の値を `dev` と読む規則は `get_dev_service_name()` の中だけに置く。** 呼び出し側は 12 か所あり、どれもこの関数から
+ 名前を受け取るため、関数の中に置けば呼び出し側を変えずにすべての経路が揃う。`bin/devbase` で空の値を消してから
+ Python へ渡す形では、`_load_project_env` が `env` を読み直す経路で空の値が戻る
+- **空の判定は長さ 0 だけにし、前後の空白を削らない。** `${DEV_SERVICE_NAME:-dev}` は空白だけの値を置き換えない。
+ Python だけが空白を削ると、空白だけの値で `build` が建てるサービスと Python が探すサービスが分かれる(I3 が破れる)
+- **読み取りは `os.environ.get('DEV_SERVICE_NAME') or 'dev'` の形で関数の中に置く。** 環境変数の読み取りを集める検査
+ ([テストの環境の隔離](test-environment-isolation.md#読み取りの集合の集め方))は `os.environ.get(N, ...)` の形を
+ `ast` で拾う。この形を保てば、検査が読み取りを拾う場所は `volume/compose.py` のままになる
+
+## データ・設定
+
+| 名前 | 向き | 意味 |
+| --- | --- | --- |
+| `DEV_SERVICE_NAME` | 入力(プロジェクトの `env`、任意) | 開発サービス名。未設定か空なら `dev` |
+
+## 運用
+
+- `DEV_SERVICE_NAME=` と空で書いたプロジェクトは、書かないプロジェクトと同じく `dev` を開発サービスとして扱う
+- 空白だけの値は空として扱わない。サービス名として使えない値を書いたときの扱いは呼び出し側(構成にそのサービスが無い)に従う
+
+## テスト観点
+
+`tests/volume/test_compose_dev_service_name.py`:
+
+- `get_dev_service_name()` が、未設定で `'dev'`、空で `'dev'`、`app` で `'app'`、空白 1 文字で `' '` を返すこと(I1・I2。
+ 関数を `os.environ.get('DEV_SERVICE_NAME', 'dev')` に戻すと空の値の場合が落ちる)
+- 構成に `dev` と `redis` があり `DEV_SERVICE_NAME` が空のとき、`generate_scaled_compose(scale=2)` が `dev` を
+ `dev-1`・`dev-2` へ複製し、`dev` を残さないこと(呼び出し側の経路が `get_dev_service_name()` を差し替えずに通る)
+
+`tests/test_env_isolation.py`:
+
+- `test_collector_finds_known_reads` が `DEV_SERVICE_NAME` の読み取りを `volume/compose.py` に見つけること(I4)
+- `test_test_side_setenv_wins` が `monkeypatch.setenv('DEV_SERVICE_NAME', 'x')` の後に `get_dev_service_name()` から
+ `'x'` を得ること
+
+手動の確認: 作業用のプロジェクトの `env` に `DEV_SERVICE_NAME=` と書き、`devbase login` が `dev` のコンテナへ入ること。
+
+## 関連リンク
+
+- [付随サービス群の後からの起動・停止(Compose の profiles)](compose-profiles.md)(開発サービス名を生成物で `<開発サービス名>-1`..`-N` へ複製する)
+- [派生イメージの継承の連なりのビルド](base-image-chain-build.md)(`build` が開発サービス名を `--service` で渡す、決定 6)
+- [テストの環境の隔離(環境変数と HOME)](test-environment-isolation.md)(環境変数の読み取りを集める検査)
+- 実装 PR: devbasex/devbase#438(#425)
diff --git a/docs/specifications/snapshot-series.md b/docs/specifications/snapshot-series.md
index 37547550..8b76ef02 100644
--- a/docs/specifications/snapshot-series.md
+++ b/docs/specifications/snapshot-series.md
@@ -82,7 +82,7 @@ classDiagram
+series_latest(volumes) dict|None
+auto_snapshot_target(max_incrementals) str|None
+last_snapshot_time(volumes) datetime|None
- +rotate(keep, max_total) int
+ +rotate(keep, max_total) RotateResult
-_rotation_plan(snapshots, keep, max_total) list
-_safe_snap_dir(name) Path
-_series_entries(volumes) list
diff --git a/docs/specifications/test-environment-isolation.md b/docs/specifications/test-environment-isolation.md
index 4e935e14..df0a67f4 100644
--- a/docs/specifications/test-environment-isolation.md
+++ b/docs/specifications/test-environment-isolation.md
@@ -290,7 +290,7 @@ I4 が保証するのは session の段が終わった時点までである。
```text
隔離の一覧に無い環境変数が 1 個ある
-DEV_SERVICE_NAME: volume/compose.py:37
+DEV_SERVICE_NAME: volume/compose.py:41
tests/conftest.py の ISOLATED_ENV か NOT_ISOLATED_ENV(理由つき)に足す
```
diff --git a/issues/issue-425-design.md b/issues/issue-425-design.md
deleted file mode 100644
index 4db48e9e..00000000
--- a/issues/issue-425-design.md
+++ /dev/null
@@ -1,221 +0,0 @@
-# devbase: `DEV_SERVICE_NAME=` と空で書いたプロジェクトで、`build` は `dev` を建てるのに `up`・`login`・`--expires` の判定などは空の名前のサービスを探して開発サービスを見失う → 空の値は未設定と同じく `dev` と読み、どの経路も同じ開発サービスを使う(#425)
-
-## 目的
-
-- **何が壊れているか**: `bin/devbase` は `${DEV_SERVICE_NAME:-dev}` で空の値を `dev` と読むが、Python の `get_dev_service_name()` は空の値をそのまま返す。同じ `env` から 2 つの開発サービス名が生まれる
-- **誰が困るか**: プロジェクトの `env` に `DEV_SERVICE_NAME=` と空で書いた利用者。`build` は通るのに、`up` のイメージの確かめ・`login`・`open`・`post-start`・`scale`・`--expires` の判定・トークンの配布が開発サービスを見つけられない
-- **直すと何が成り立つか**: 空の値は未設定と同じく `dev` になり、`bin/devbase` と Python が同じ `env` から同じ開発サービス名を得る
-
-## 適用範囲
-
-- **働く範囲**: このリポジトリの `lib/devbase/`。配布先は `install.sh` で `main` を取る利用者の端末。`bin/devbase` とプラグインのリポジトリ(`projects/*`)は変えない
-- **プロジェクトごとに違うもの**: 開発サービス名はプロジェクトの `env` の `DEV_SERVICE_NAME` から受ける(今と同じ)。新しい設定と引数は足さない
-- **当たるモード**: `standard`
-
-## あるべき姿の根拠
-
-| 根拠 | 種類 | 何を示すか |
-| --- | --- | --- |
-| `bin/devbase` の `cmd_build` は `docker compose build "${DEV_SERVICE_NAME:-dev}"` と `--service "${DEV_SERVICE_NAME:-dev}"` で名前を決める(`bin/devbase` の 245・328・333・337 行) | 既存の形 | 空の値を `dev` と読む規則がすでに配布物にあり、Python をこちらへ揃えれば読み方が 1 つになる |
-| Bash の `${VAR:-word}` は、変数が未設定か空のときに `word` へ置き換え、空白だけの値は置き換えない(`bash -c 'X=" "; printf "[%s]" "${X:-dev}"'` は `[ ]`、`X=` では `[dev]`。macOS の bash 5.3、2026-10-07) | 実測 | 「空」は長さ 0 の文字列だけを指す。前提 2(空白だけの値は空に含めない)と一致する |
-| 課題の本文の前提 1「`bin/devbase` の `${DEV_SERVICE_NAME:-dev}` の読み方に Python を揃える(`bin/devbase` の側は変えない)」 | 利用者の指示の原文 | 直す側を Python に決める |
-
-要求と受け入れ条件は #425 の本文にある(コピーは `issues/issue-425-requirements.md`)。この文書は「どう作るか」だけを扱う。
-
-## ドメインモデル
-
-### コンテキスト
-
-| コンテキスト | 何の語が 1 つの意味に決まるか |
-| --- | --- |
-| Compose の構成(`compose`) | 開発サービス名(どのサービスを開発サービスとして扱うか) |
-
-変更はこの 1 つのコンテキストに閉じる。開発サービス名を使う側(コマンドの入口 `cli`・イメージの継承 `image-lineage`)は
-名前を受け取るだけで、読み方を自分では持たない。その関係は変えない。
-
-### 集約
-
-| 集約 | 持ち主(書き換えてよいもの) | 根 | エンティティ | 値オブジェクト |
-| --- | --- | --- | --- | --- |
-| 開発サービス名 | Python 側は `lib/devbase/volume/compose.py` の `get_dev_service_name()`、Bash 側は `bin/devbase` の `${DEV_SERVICE_NAME:-dev}`(この変更では変えない) | 環境変数 `DEV_SERVICE_NAME` の値 | — | 決めた名前(文字列) |
-
-呼び出しのたびに環境変数から決め直し、保存しない。持ち主が 2 つあるのは、Bash と Python という別の実行の単位が
-それぞれ名前を必要とするためである。2 つが同じ規則で読むことを I3 で縛る。
-
-### 不変条件
-
-| # | 集約 | 条件 | 破れたときの扱い |
-| --- | --- | --- | --- |
-| I1 | 開発サービス名 | `DEV_SERVICE_NAME` が未設定か空(長さ 0)なら、名前は `dev` である | 単体のテストが落ちる |
-| I2 | 開発サービス名 | `DEV_SERVICE_NAME` が空でなければ、名前はその値を変えずに使う(空白だけの値も前後の空白を削らない) | 単体のテストが落ちる |
-| I3 | 開発サービス名 | 同じ `DEV_SERVICE_NAME` の値(未設定・空・`app`・空白 1 文字)から、`bin/devbase` の `${DEV_SERVICE_NAME:-dev}` と `get_dev_service_name()` は同じ名前を決める | 2 つを並べて比べるテストが落ちる |
-| I4 | 開発サービス名 | `lib/devbase/` で `DEV_SERVICE_NAME` を読むのは `volume/compose.py` の 1 か所だけである | 環境変数の読み取りを集める検査のテストが落ちる |
-
-### ドメインイベント
-
-| # | イベント | 発生元 | 受け手 |
-| --- | --- | --- | --- |
-| E1 | 利用者がプロジェクトの `env` に `DEV_SERVICE_NAME=` と書いた | 利用者 | `bin/devbase`(E2)・`_load_project_env`(E2 の Python 側の読み込み) |
-| E2 | 空の `DEV_SERVICE_NAME` が環境変数として Python へ渡った | `bin/devbase` の `set -a` での読み込み、または `_load_project_env` | `get_dev_service_name()`(E3) |
-| E3 | Python が開発サービス名を求め、`dev` を得た | `get_dev_service_name()` | 呼び出し側(E4) |
-| E4 | 呼び出し側が compose の構成から開発サービス `dev` を引いた | `_resolve_dev_service`・`generate_scaled_compose` など(変えない) | `up`・`login`・`open`・`post-start`・`scale`・`--expires` の判定・トークンの配布 |
-| E5 | `build` が `dev` を建て、同じ `dev` を `--service` で Dockerfile の場所を決める入口へ渡した | `bin/devbase` の `cmd_build`(変えない) | `docker compose build`・`project_dockerfile` |
-
-E3 と E5 が同じ `dev` になることが、この変更で揃える点である(I3)。
-
-### 用語
-
-| 用語 | 意味 | 用語集への反映 |
-| --- | --- | --- |
-| 開発サービス名 | `get_dev_service_name()` が返す名前(`DEV_SERVICE_NAME`。未設定か空なら `dev`)。生成物では `<開発サービス名>-1`..`-N` へ複製される | 意味の変更(`compose`。この設計の変更で反映済み) |
-
-## 機能一覧
-
-| # | 機能 | 誰が使うか |
-| --- | --- | --- |
-| F1 | `DEV_SERVICE_NAME=` と空で書いたプロジェクトで、`up`・`login`・`open`・`post-start`・`scale`・`--expires` の判定・トークンの配布が `dev` を開発サービスとして使う | プロジェクトの利用者 |
-| F2 | `bin/devbase` と Python が、同じ `env` から同じ開発サービス名を決める | devbase の開発者 |
-
-## 構成要素
-
-| 要素 | 責務 | この変更 |
-| --- | --- | --- |
-| `get_dev_service_name()`(`lib/devbase/volume/compose.py`) | 環境変数から開発サービス名を決める。Python で `DEV_SERVICE_NAME` を読む唯一の場所 | 変える。空の値を `dev` と読む |
-| `cmd_build`(`bin/devbase`) | `${DEV_SERVICE_NAME:-dev}` で名前を決め、`docker compose build` と `project_dockerfile --service` へ渡す | 変えない |
-| 呼び出し側(`container.py` の `cmd_up`・`cmd_login`・`cmd_open`・`cmd_post_start`・`cmd_scale`・`_resolve_dev_service`・`_ensure_images`・`_dev_instance_indices`・`_maybe_open_editor`・`_open_editor_at`、`env.py` の `_push_token_to_running`、`compose.py` の `generate_scaled_compose`) | 名前を受け取り、compose の構成から開発サービスを引く | 変えない |
-| 用語集の「開発サービス名」(`docs/glossary/glossary.json` と、作り直す `docs/glossary.md`) | 語の意味を定める | 変える(この設計の変更で反映) |
-| 用語集の意味を写した確定仕様の文(`docs/specifications/editor-open.md` と `docs/specifications/compose-profiles.md` の用語の表) | 仕様の中で語の意味を示す | 変える。用語集と同じ文にする |
-| `CHANGELOG.md` の `[Unreleased]` | 利用者に見える振る舞いの変更を記す | 足す |
-
-型・クラスは足さず変えない。`get_dev_service_name()` の引数と戻り値の型(`str`)も変えないため、クラス図と構造の節は置かない。
-
-既存の規則のうち、`DEV_SERVICE_NAME` の値を前提にしたものを、名前の検索(`DEV_SERVICE_NAME`・`get_dev_service_name`・`既定 dev`)と、
-空の値が通る経路(E1〜E5)の読みで集めた。空の値を `dev` と読んだ後に当てはまらなくなる規則は、上の表の用語集と 2 つの確定仕様の文だけである。
-`generate_scaled_compose` の `dev_service_name is None` のときの既定・`_dev_instance_indices` の `<名前>-<番号>` の照合・
-`tests/conftest.py` の隔離の一覧は、受け取る名前が `dev` になるだけで、そのまま当てはまる。
-
-### 文脈と配置
-
-```mermaid
-graph LR
- U[利用者] -->|書く| ENV["プロジェクトの env
DEV_SERVICE_NAME="]
- subgraph HOST[利用者の端末]
- subgraph SH["bin/devbase(Bash)"]
- B["cmd_build
${DEV_SERVICE_NAME:-dev}"]
- end
- subgraph PY["lib/devbase(Python)"]
- G["get_dev_service_name()"]
- C[呼び出し側]
- PD[project_dockerfile]
- end
- end
- ENV -->|set -a で読む| B
- ENV -->|set -a / _load_project_env| G
- B -->|--service dev| PD
- B -->|build dev| DC[docker compose]
- G -->|dev| C
- C -->|config / up / exec| DC
-```
-
-変更は `G` の 1 か所に閉じる。`B` と `PD` と `C` は変えない。
-
-## 入出力の契約
-
-変えるのは、プロジェクトの `env` に書く `DEV_SERVICE_NAME` の読み方だけである。
-
-| 項目 | 書くこと |
-| --- | --- |
-| 名前 | 環境変数 `DEV_SERVICE_NAME`(Python の入口は `get_dev_service_name()`) |
-| 入力 | 文字列。任意 |
-| 出力 | 未設定 → `dev`、空(`''`)→ `dev`(**変わる**。今は `''`)、それ以外 → 値をそのまま(空白だけの値も含む) |
-| 失敗の形 | 無い。名前は必ず決まる。構成にその名前のサービスが無いときの扱いは呼び出し側が持ち、変えない |
-| 互換性 | 空で書いたプロジェクトだけが変わる。今の振る舞いは開発サービスを見失うだけで、頼りにする利用者は無いと置く(要求の前提 4)。未設定と空でない値のプロジェクトは変わらない |
-
-## 処理の流れ
-
-```mermaid
-sequenceDiagram
- participant W as bin/devbase
- participant G as get_dev_service_name()
- participant C as 呼び出し側
- participant D as docker compose
- W->>W: env を set -a で読む(DEV_SERVICE_NAME='')
- alt build
- W->>D: build ${DEV_SERVICE_NAME:-dev} → dev
- else up・login・open・post-start・scale・--expires の判定・トークンの配布
- W->>C: Python の入口を起動
- C->>G: 開発サービス名を求める
- G->>G: 値を読む。未設定か空なら dev
- G-->>C: dev
- C->>D: config / up / exec(サービス dev)
- end
-```
-
-構成要素の表のうち、用語集・確定仕様の文・`CHANGELOG.md` は実行の流れに現れないため、この図に描かない。
-
-## 決定の記録
-
-### 決定 1: どの呼び出し側も同じ名前を得るため、空の値を `dev` と読む規則を `get_dev_service_name()` の中だけに置く
-
-呼び出し側は 12 か所あり、どれも `get_dev_service_name()` から名前を受け取る(要求の前提 3)。規則を関数の中に置けば、
-呼び出し側を変えずにすべての経路が揃う。Python の読み込み(`_load_project_env`)も空の値を環境へ載せるため、
-`bin/devbase` を通らない経路も同じ関数で揃う。
-
-`bin/devbase` で空の `DEV_SERVICE_NAME` を消してから Python へ渡す形は採らない。`bin/devbase` を変えないという要求の
-前提 1 に反し、`_load_project_env` が `env` を読み直す経路(トークンの配布など)では空の値が戻る。呼び出し側ごとに
-空の値を確かめる形も採らない。12 か所に同じ規則が散り、1 か所の書き漏れで経路が分かれる。
-
-根拠: Value 1 / Mission(MVV 版 1)
-
-### 決定 2: `bin/devbase` と同じ値を空と読むため、空の判定は長さ 0 だけにし、前後の空白を削らない
-
-`${DEV_SERVICE_NAME:-dev}` は空白だけの値を置き換えない(あるべき姿の根拠の実測)。Python が空白を削って `dev` と読むと、
-空白だけの値で `build` が建てるサービスと Python が探すサービスがまた分かれる。空白だけの値やサービス名に使えない文字の
-検証は、名前の形の検証として別の課題で扱う(要求の「含まない」)。
-
-前後の空白を削ってから空かを見る形は採らない。上の理由で I3 が破れる。
-
-根拠: Value 1(MVV 版 1)
-
-### 決定 3: 環境変数の読み取りを集める検査が今と同じ場所を拾うよう、読み取りは `os.environ.get('DEV_SERVICE_NAME')` の形のまま関数の中に置く
-
-`test_collector_finds_known_reads` と `test_every_env_read_is_listed` は、`os.environ.get(N, ...)` の形を `ast` で拾う
-(`docs/specifications/test-environment-isolation.md` の「読み取りの集合の集め方」)。`os.environ.get('DEV_SERVICE_NAME') or 'dev'`
-はこの形を保ち、読む場所は `volume/compose.py` のままになる。確定仕様のその節の例(`os.environ.get('DEV_SERVICE_NAME', 'dev')`)は
-拾う形の例示で、実際のコードの写しではないため、書き換えない。
-
-空の値を別の関数(環境変数を一般に読む helper)へ寄せる形は採らない。`DEV_SERVICE_NAME` 以外の変数の空の値は範囲の外で
-(要求の「含まない」)、helper の中で読むと検査が拾う場所も移る。
-
-根拠: Value 1(MVV 版 1)
-
-## テスト設計
-
-| 受け入れ条件・不変条件 | どの振る舞いで縛るか | どう壊したら落ちるべきか |
-| --- | --- | --- |
-| 受け入れ条件 1・I1 | `DEV_SERVICE_NAME=''` で `get_dev_service_name()` が `'dev'` を返す | 関数を元の `os.environ.get('DEV_SERVICE_NAME', 'dev')` に戻すと落ちる |
-| 受け入れ条件 2・I1 | `DEV_SERVICE_NAME` を消した状態で `'dev'` を返す | 既定を `dev` 以外にすると落ちる |
-| 受け入れ条件 3・I2 | `DEV_SERVICE_NAME=app` で `'app'` を返す | 値を捨てて常に `dev` を返すと落ちる |
-| 受け入れ条件 4・I2 | `DEV_SERVICE_NAME=' '` で `' '` を返す | 空白を削ってから空かを見る形にすると落ちる |
-| I3 | 未設定・空・`app`・空白 1 文字のそれぞれで、Bash が `${DEV_SERVICE_NAME:-dev}` を展開した値と `get_dev_service_name()` が一致する | Python 側だけ空白を削る、または Python 側だけ空を `dev` と読まない形にすると落ちる |
-| 受け入れ条件 5 | 構成に `dev` があり `DEV_SERVICE_NAME=''` のとき、`_resolve_dev_service` が `dev` のサービスの定義を返す。`get_dev_service_name` を差し替えずに通す | 関数を元に戻すと、空の名前で引いて空の定義を返し落ちる |
-| 受け入れ条件 6・I4 | 読み取りを集める処理が `DEV_SERVICE_NAME` の読み取りを `volume/compose.py` に見つけ、ほかのファイルには見つけない | 呼び出し側に `os.environ.get('DEV_SERVICE_NAME')` を足すと落ちる。関数の読み取りを拾えない形へ書き換えると落ちる |
-| 受け入れ条件 7 | `glossary.py check` が設計文書と用語集の食い違いを出さず、`docs/glossary.md` が `glossary.json` から作り直してある | 用語集の意味を古い文に戻すと、`render` の差分が出る |
-| 受け入れ条件 8 | `CHANGELOG.md` の `[Unreleased]` を読んで確かめる(レビュー) | — |
-| 受け入れ条件 9 | 全体のテスト・lint・固有の語の検査(要求の「検証手段」のコマンド) | — |
-
-既存のテストの多くは `container.get_dev_service_name` を `lambda: 'dev'` に差し替えている。受け入れ条件 5 のテストは差し替えると
-関数を通らず、壊しても落ちない。
-
-## 設計の結果
-
-| 課題 | 扱い | 取り込み先 | 触るファイル |
-| --- | --- | --- | --- |
-| #425 | 実装する | — | `lib/devbase/volume/compose.py`、`tests/volume/`、`tests/commands/`、`tests/test_env_isolation.py`、`docs/specifications/editor-open.md`、`docs/specifications/compose-profiles.md`、`CHANGELOG.md` |
-
-## 未確認のまま残ること
-
-| 項目 | 内容 |
-| --- | --- |
-| 確定仕様 `base-image-chain-build.md` の決定 6 の文 | 「`get_dev_service_name()` は空の値をそのまま返す」は、直した後に事実でなくなる。決定の理由として残すか、#425 で揃ったと書き足すかは、確定仕様化(`plan-to-spec`)の担当が決める(要求の「未決」) |
-| 実機での確かめ | 空の値で `devbase login` が `dev` のコンテナへ入ることは、リリース後テストで作業用のプロジェクトを使って確かめる。実際の利用者の環境での `devbase up` は P3 のため人の承認を得てから行う |
diff --git a/issues/issue-425-requirements.md b/issues/issue-425-requirements.md
deleted file mode 100644
index 159420b7..00000000
--- a/issues/issue-425-requirements.md
+++ /dev/null
@@ -1,134 +0,0 @@
-# #425: DEV_SERVICE_NAME を空で書くと、bin/devbase は dev を使い Python は空の名前で開発サービスを探す
-
-正は課題の本文(#425)で、この文書はその写しである。`spec-copy.py write` で作り直す。手で直さない。
-
-## 依頼(原文)
-
-> ## 何を見つけたか
->
-> プロジェクトの `env` に `DEV_SERVICE_NAME=`(空の値)と書いたとき、2 つの読み方が別のサービス名になる(コードを読んだ結果。実機では未再現)。
->
-> - `bin/devbase` の `cmd_build` は `${DEV_SERVICE_NAME:-dev}` で、空なら `dev` を `docker compose build` に渡す。Dockerfile の場所を決める `python -m devbase.commands.project_dockerfile` にも同じ値を `--service` で渡す(#415)ため、通常のビルドの中では建てるサービスと場所を決めるサービスが揃う
-> - Python の `get_dev_service_name()`(`lib/devbase/volume/compose.py:35`)は `os.environ.get('DEV_SERVICE_NAME', 'dev')` で、空の値をそのまま返す。`--expires` の判定(`lib/devbase/commands/container.py` の `_get_base_image_ref`)・`up`・`login`・`open`・`post-start`・`scale` はこちらを使う
->
-> `bin/devbase` は `env` を `set -a` で読むため、空の値も環境変数として Python へ渡る。
->
-> ## どこで見つけたか
->
-> - `lib/devbase/volume/compose.py` の `get_dev_service_name`
-> - `bin/devbase` の `cmd_build`(`docker compose build "${DEV_SERVICE_NAME:-dev}"` と `project_dockerfile` の `--service "${DEV_SERVICE_NAME:-dev}"`)
->
-> ## なぜこの変更の範囲外なのか
->
-> issue #415 の受け入れ条件は、通常のビルドと `--expires` の判定が**同じ開発サービス名から**同じ Dockerfile を決めることを求め、開発サービス名の読み方そのものは前提 1(2 つが同じ値を使う)に置いている。`get_dev_service_name()` は `up`・`login`・`open`・`post-start` などでも使われ、直すと #415 の範囲(Dockerfile の場所の決め方)の外へ広がる。#415 は、通常のビルドが `${DEV_SERVICE_NAME:-dev}` の値を引数で渡し、建てるサービスと場所を決めるサービスを揃えた(PR #427)。根拠: Value 1(MVV 版 1)
->
-> ## 直さないと何が起きるか
->
-> `DEV_SERVICE_NAME=` と空で書いたプロジェクトで、`--expires` の判定・`devbase up` のイメージの確かめ・`login` などが、サービス名 `''` を探して開発サービスを見失う。今の手元のプラグインのプロジェクトで `DEV_SERVICE_NAME` を `env` に書いたものは 0 件で、すぐには表に出ない。
->
-> ## 由来
->
-> issue #415 の設計中に見つけた。
-
-## 目的
-
-- プロジェクトの `env` に `DEV_SERVICE_NAME=`(空の値)と書いても、`devbase` のどの経路も同じ開発サービス名(`dev`)を使う状態にする。`bin/devbase` の `build` と、Python の `up`・`login`・`open`・`post-start`・`scale`・`--expires` の判定とで、開発サービスを見失う経路を無くす
-
-## 前提
-
-- 前提 1: 空の値(長さ 0 の文字列)は「`DEV_SERVICE_NAME` を書いていない」と同じに扱い、開発サービス名は `dev` になる。`bin/devbase` の `${DEV_SERVICE_NAME:-dev}` の読み方に Python を揃える(`bin/devbase` の側は変えない)
-- 前提 2: 空白だけの値(`DEV_SERVICE_NAME=" "` など)は空の値に含めない。`bin/devbase` の `${DEV_SERVICE_NAME:-dev}` も空白だけの値をそのまま使うため、今も 2 つの読み方は揃っている
-- 前提 3: Python で `DEV_SERVICE_NAME` を読む箇所は `lib/devbase/volume/compose.py` の `get_dev_service_name()` 1 か所だけである(`lib/` を `DEV_SERVICE_NAME` で検索した結果。ほかの当たりは文字列の説明か `get_dev_service_name()` の呼び出し)。呼び出し側(`lib/devbase/commands/container.py` の `cmd_up`・`cmd_login`・`cmd_open`・`cmd_post_start`・`cmd_scale`・`_resolve_dev_service`・`_ensure_images`・`_dev_instance_indices`・`_maybe_open_editor`・`_open_editor_at`、`lib/devbase/commands/env.py` の `_push_token_to_running`、`lib/devbase/volume/compose.py` の `generate_scaled_compose`)は名前を受け取るだけで、直す場所は `get_dev_service_name()` に閉じる
-- 前提 4: 手元のプラグインのプロジェクト(`repos/`・`projects/*`)で `DEV_SERVICE_NAME` を書いたものは 0 件である(issue の起票時の調べ)。空の値を書いた利用者の環境があっても、変更後は `dev` を探すようになるだけで、`dev` 以外の名前を書いた利用者の振る舞いは変わらない
-
-## 対象範囲
-
-含む:
-- `get_dev_service_name()` が、`DEV_SERVICE_NAME` が空の値のとき `dev` を返すようにする
-- 空の値の扱いを確かめるテスト(関数の単体と、呼び出し側の経路 1 つ以上)
-- 開発サービス名の既定の書き方(用語集の「開発サービス名」と、それを写した確定仕様の文)を「未設定か空なら `dev`」へ揃える
-- `CHANGELOG.md` の `[Unreleased]` への記入(利用者に見える振る舞いの変更のため)
-
-含まない:
-- `bin/devbase` の `${DEV_SERVICE_NAME:-dev}` の読み方の変更(すでに空なら `dev`)
-- `build` が `--service` で開発サービス名を渡す形(#415 の決定 6)をやめること。Python 側が揃った後も、建てるサービスと場所を決めるサービスを 1 つの値で揃える役目は残る
-- 空白だけの値・サービス名として使えない文字を含む値の検証(前提 2。名前の形の検証は別の課題で扱う)
-- `DEV_SERVICE_NAME` 以外の環境変数の空の値の扱い
-- 空の値を書いたプロジェクトへの警告の表示
-
-## ドメインイベント
-
-| # | イベント | 引き金 | 失敗したとき | 順序の前提 |
-| --- | --- | --- | --- | --- |
-| E1 | 利用者がプロジェクトの `env` に `DEV_SERVICE_NAME=` と書いた | 利用者の編集 | — | — |
-| E2 | `bin/devbase` が `env` を `set -a` で読み、空の `DEV_SERVICE_NAME` を環境変数として Python へ渡した | `devbase` の各コマンドの起動 | `env` を読めなければ今の扱いのまま(この変更では変えない) | E1 |
-| E3 | Python が開発サービス名を求め、`dev` を得た | `up`・`login`・`open`・`post-start`・`scale`・`--expires` の判定・トークンの配布 | — (空の値でも未設定でも名前は必ず決まる) | E2 |
-| E4 | 呼び出し側が compose の構成から開発サービス `dev` を引いた | E3 の名前 | 構成に `dev` が無ければ、今の「開発サービスが見つからない」扱いのまま(この変更では変えない) | E3 |
-| E5 | `build` が `${DEV_SERVICE_NAME:-dev}` の `dev` を建て、同じ `dev` を `--service` で Dockerfile の場所を決める処理へ渡した | `devbase build` | 今の扱いのまま | E2 |
-
-E3 と E5 が同じ `dev` になることが、この変更で揃える点である。
-
-## 用語
-
-| 用語 | 意味 |
-| --- | --- |
-| 開発サービス名 | `get_dev_service_name()` が返す名前。`DEV_SERVICE_NAME` が未設定か空なら `dev` |
-
-## 受け入れ条件
-
-- [ ] `DEV_SERVICE_NAME` が空の値(`''`)のとき、`get_dev_service_name()` は `'dev'` を返す
-- [ ] `DEV_SERVICE_NAME` が未設定のとき、`get_dev_service_name()` は `'dev'` を返す(退行しない)
-- [ ] `DEV_SERVICE_NAME=app` のとき、`get_dev_service_name()` は `'app'` を返す(退行しない)
-- [ ] `DEV_SERVICE_NAME=" "`(空白 1 文字)のとき、`get_dev_service_name()` は `' '` を返す(前提 2。`bin/devbase` の `${DEV_SERVICE_NAME:-dev}` と同じ値)
-- [ ] 前提: compose の構成に `dev` サービスがあり、`DEV_SERVICE_NAME` が空の値である
- 操作: 呼び出し側の経路(`_resolve_dev_service` または `generate_scaled_compose`)を通す
- 結果: `dev` サービスを開発サービスとして扱う(空の名前のサービスを探して見失わない)
-- [ ] 環境変数の読み取りを集める検査(`tests/test_env_isolation.py` の `test_collector_finds_known_reads`)が、変更後も `DEV_SERVICE_NAME` の読み取りを `volume/compose.py` に見つける
-- [ ] 用語集の「開発サービス名」の意味が「未設定か空なら `dev`」と読め、`docs/glossary.md` は `glossary.json` から作り直してある
-- [ ] `CHANGELOG.md` の `[Unreleased]` に、空の `DEV_SERVICE_NAME` を `dev` として扱うようになったことが書いてある
-- [ ] 全体のテスト・lint・固有の語の検査が通る(下の「検証手段」)
-
-## 影響
-
-| 対象 | 影響 |
-| --- | --- |
-| 公開インタフェース | 変わる(互換あり)。`DEV_SERVICE_NAME=` と空で書いたときの開発サービス名が `''` から `dev` になる。今の振る舞いは開発サービスを見失うだけで、これを頼りにする利用者は無いと置く(前提 4) |
-| データ | 無し |
-| 既存の振る舞い | 空の `DEV_SERVICE_NAME` を書いたプロジェクトの `up`・`login`・`open`・`post-start`・`scale`・`--expires` の判定・トークンの配布が `dev` を開発サービスとして使う。未設定・空でない値のプロジェクトは変わらない |
-
-## 検証手段
-
-| 項目 | 手段 |
-| --- | --- |
-| テスト | `env -u DEVBASE_ROOT uv run --locked pytest tests/ -q` |
-| 静的解析 | `uvx ruff check --select=E9,F63,F7,F82 lib` |
-| 固有の語の検査 | `python3 .github/scripts/proper_term_check.py`(`語の一覧が無いため飛ばした` と出たら検査していないと記録する) |
-| 手動確認 | リリース後テストで、作業用のプロジェクトの `env` に `DEV_SERVICE_NAME=` と書き、`devbase login` が `dev` のコンテナへ入ることを確かめる。実際の利用者の環境での `devbase up` は P3 のため人の承認を得てから行う |
-
-## 前提とする取り決め
-
-| 項目 | 参照先 / 決めたこと |
-| --- | --- |
-| プロジェクト構造 | `AGENTS.md` の「このリポジトリ」。直すのは `lib/devbase/` の Python。`projects/*`・`repos/` は管理外のため触らない |
-| コーディング規約 | `AGENTS.md` の「書き方」と周りのコードに合わせる。lint は上の静的解析のコマンド |
-| テスト戦略 | 単体: `get_dev_service_name()` の 4 つの値(未設定・空・`app`・空白)。結合: 空の値で呼び出し側の経路が `dev` を引くこと。テストは `monkeypatch` で `DEV_SERVICE_NAME` を置き、実の docker に触らない(`docs/specifications/test-environment-isolation.md`) |
-
-## 境界
-
-| 区分 | 内容 |
-| --- | --- |
-| 常に行う | 上の 3 つの検査を push の前に手元で打つ。用語集を直したら `glossary.py render` で `docs/glossary.md` を作り直す |
-| 確認してから行う | `get_dev_service_name()` の戻り値の型や呼び出し方を変えること。名前の形の検証を足すこと |
-| 行わない | `bin/devbase` の読み方の変更。base イメージの変更(P1)。実際の利用者の環境での `devbase up`(P3) |
-
-## 未決
-
-| 項目 | 誰が決めるか | 期限 |
-| --- | --- | --- |
-| 確定仕様(`docs/specifications/base-image-chain-build.md` の決定 6)の「`get_dev_service_name()` は空の値をそのまま返す」の文を、直した後の事実へどう書き換えるか(決定の理由として残すか、#425 で揃ったと書き足すか) | 確定仕様化の担当 | 確定仕様化の工程 |
-
-## 根拠
-
-- Value 1(入った時点で使える): `env` の書き方の違いで開発サービスを見失わないようにする
-- Mission: どのプロジェクトでも同じ読み方で開発環境へ入れるようにする
-- P1・P3: 範囲に含めない(境界の「行わない」)