brownfield_orchestrator.py God file 分割 実装計画
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: tools/brownfield_orchestrator.py(485行 God file)を tools/brownfield/ パッケージ(6モジュール)へ分割し、振る舞いを完全保存して python tools/brownfield_orchestrator.py --ui の後方互換を維持する。
Architecture: パッケージ化 + importlib によらない sys.path 挿入型 thin shim(相対import問題を回避)。依存方向 utils ← core ← ui ← __main__(循環なし)。振る舞い保存は baseline 前後比較(stream_run を mock 境界にした golden input のイベント列 diff)で保証。
Tech Stack: Python 3(from __future__ import annotations)・Gradio・pytest・monkeypatch。リポジトリ: NexusCore。
前提spec: docs/superpowers/specs/2026-07-01-brownfield-orchestrator-split-design.md
転記の原則: 各モジュールは旧 tools/brownfield_orchestrator.py(commit f7bb6100 時点)から該当行を転記する。本計画は転記元行番号 + 調整点(import 文・定数の REPO_ROOT 化)を明示する。転記時に関数本体のロジックは一切変更しない(振る舞い保存)。
File Structure
| ファイル | 責務 | 由来(旧ファイル行) |
|---|---|---|
tools/brownfield/__init__.py | lazy __getattr__(main/build_ui)・gradio を引かない | 新規 |
tools/brownfield/__main__.py | parse_args + main(argv) エントリ | L445-485 |
tools/brownfield/utils.py | 定数 + IO/helper + stream_run + detect_latest_snapshot + orchestrator adapter | L47-128, L239-253, L255-317 |
tools/brownfield/core.py | run_brownfield_stream(業務コア) | L131-233 |
tools/brownfield/ui.py | build_ui, auto_launch_with_increment, try_build_dir_picker | L235-238, L318-443 |
tools/brownfield_orchestrator.py | thin shim(後方互換・上書き) | 新規(旧ファイルと置換) |
tests/tools/brownfield/test_*.py | smoke + baseline比較テスト群 | 新規 |
Task 1: baseline イベント列取得(旧コードで正解記録)
Files:
- Create:
tests/tools/brownfield/__init__.py(空) - Create:
tests/tools/brownfield/_baseline_capture.py(一時・旧コードから取得) -
Create:
tests/tools/brownfield/_baseline_events.json(生成物) - Step 1: テストパッケージ雛形 + baseline 取得スクリプト作成
tests/tools/brownfield/__init__.py を空ファイルで作成。
tests/tools/brownfield/_baseline_capture.py:
"""旧 brownfield_orchestrator.py から baseline イベント列を取得(リファクタ前)。"""
import json, sys
from pathlib import Path
from typing import List, Tuple, Optional, Generator
# 旧ファイルを import するため sys.path に tools/ を追加
_TOOLS = Path(__file__).resolve().parents[3] / "tools"
sys.path.insert(0, str(_TOOLS))
import brownfield_orchestrator as old # 旧485行ファイル(まだ shim 化前)
def fake_stream_run(cmd, cwd) -> Generator[str, None, Tuple[bool, str]]:
"""決定論的 fake。stream_run と同じ形状(yield 行 → return (ok, out))。"""
yield "--- [fake] phase line 1\n"
yield "--- [fake] phase line 2\n"
return (True, "fake output")
def capture(tmp_path: Path) -> List[Tuple[str, str, Optional[str]]]:
events = []
# stream_run を fake に置換(旧モジュール内の参照)
old.stream_run = fake_stream_run
gen = old.run_brownfield_stream(
project_root=str(tmp_path / "proj"),
out_root=str(tmp_path / "out"),
profiles=["gemini-single-file"],
selected_phases=["structure", "snapshot"],
policy_profile_ui="", policy_version_ui="", policy_icon_ui="",
richness_mode="Light (fast)",
include_full_archive=False,
)
if gen:
for ev in gen:
events.append(list(ev)) # (log_text, summary, zip_path) → JSON 用に list 化
return events
if __name__ == "__main__":
import tempfile
with tempfile.TemporaryDirectory() as td:
events = capture(Path(td))
out = Path(__file__).parent / "_baseline_events.json"
out.write_text(json.dumps(events, ensure_ascii=False), encoding="utf-8")
print(f"saved {len(events)} events -> {out}")
- Step 2: baseline 取得を実行
Run: cd ~/projects/NexusCore && python3 tests/tools/brownfield/_baseline_capture.py Expected: saved N events -> .../_baseline_events.json(N>=1)
注意: 旧ファイル実行時、
run_brownfield_stream内でload_policy_meta等のファイルIOが走る可能性。tmp_path配下で完結しない場合は、Step1 のcapture内でold.load_policy_meta = lambda *a, **k: {}等の追加 mock を差し込み、イベント列が安定するようにする。その場合、Task 4 の新コード側にも同一 mock を適用すること。
- Step 3: baseline JSON が生成されたことを確認
Run: head -c 200 tests/tools/brownfield/_baseline_events.json Expected: JSON 配列(例: [["...log text...", "", null], ...])
- Step 4: commit
git add tests/tools/brownfield/__init__.py tests/tools/brownfield/_baseline_capture.py tests/tools/brownfield/_baseline_events.json
git commit -m "test(brownfield): baseline イベント列取得(旧コード・リファクタ前)"
Task 2: brownfield パッケージ雛形 + __init__.py(lazy __getattr__)
Files:
- Create:
tools/brownfield/__init__.py -
Test:
tests/tools/brownfield/test_imports.py - Step 1: 失敗テストを書く
tests/tools/brownfield/test_imports.py:
"""パッケージ import と lazy __getattr__ の検証。"""
import sys
def test_import_brownfield_does_not_load_gradio():
"""import brownfield だけでは gradio を読み込まない(lazy __getattr__)。"""
sys.modules.pop("gradio", None)
sys.modules.pop("brownfield", None)
import brownfield # noqa: F401
assert "gradio" not in sys.modules, "lazy __getattr__ 失敗: gradio が読み込まれた"
def test_getattr_main():
sys.modules.pop("brownfield", None)
import brownfield
assert callable(brownfield.main)
def test_getattr_build_ui():
sys.modules.pop("brownfield", None)
import brownfield
assert callable(brownfield.build_ui)
def test_getattr_unknown_raises():
sys.modules.pop("brownfield", None)
import brownfield
import pytest
with pytest.raises(AttributeError):
brownfield.nonexistent_attr
- Step 2: テストを実行して失敗を確認
Run: cd ~/projects/NexusCore && python3 -m pytest tests/tools/brownfield/test_imports.py -v Expected: FAIL(ModuleNotFoundError: No module named 'brownfield')
- Step 3:
__init__.pyを実装
tools/brownfield/__init__.py:
"""brownfield orchestrator パッケージ。
import 時に gradio を引かないよう lazy 読み込みする。
"""
from typing import TYPE_CHECKING
__all__ = ["main", "build_ui"]
def __getattr__(name):
if name == "main":
from .__main__ import main
return main
if name == "build_ui":
from .ui import build_ui
return build_ui
raise AttributeError(f"module 'brownfield' has no attribute {name!r}")
if TYPE_CHECKING:
from .__main__ import main as main # noqa: F401
from .ui import build_ui as build_ui # noqa: F401
- Step 4: main/build_ui 用のダミーモジュールでテストを通す
__init__.pyは.__main__と.uiに依存するが、これらは後続タスクで作成。__getattr__の遅延性テスト(gradio 非読込)はtest_import_brownfield_does_not_load_gradioのみ。test_getattr_main/test_getattr_build_uiは Task 4/5 完了後に通るため、本タスクではtest_import_brownfield_does_not_load_gradioとtest_getattr_unknown_raisesのみ検証。
Run: python3 -m pytest tests/tools/brownfield/test_imports.py::test_import_brownfield_does_not_load_gradio tests/tools/brownfield/test_imports.py::test_getattr_unknown_raises -v Expected: 2 PASS
注:
__getattr__("main")が.ui(未作成)経由で gradio を引く可能性があるため、test_import_brownfield_does_not_load_gradioはimport brownfieldのみ(属性アクセスなし)で検証する。属性アクセス系テストは Task 6 で全件再実行する。
- Step 5: commit
git add tools/brownfield/__init__.py tests/tools/brownfield/test_imports.py
git commit -m "feat(brownfield): パッケージ雛形 + __init__.py lazy __getattr__"
Task 3: utils.py(定数 + IO/helper + stream_run + detect_latest_snapshot + orchestrator adapter)
Files:
- Create:
tools/brownfield/utils.py -
Test:
tests/tools/brownfield/test_utils.py - Step 1: 失敗テストを書く
tests/tools/brownfield/test_utils.py:
"""utils.py の純粋関数テスト。"""
from pathlib import Path
from brownfield import utils as U
def test_repo_root_resolves_to_nexuscore_root():
"""REPO_ROOT は NexusCore 仓库根(.git があるディレクトリ)。"""
assert (U.REPO_ROOT / ".git").exists() or (U.REPO_ROOT / "pyproject.toml").exists()
assert U.REPO_ROOT.name == "NexusCore"
def test_package_dir_is_brownfield():
assert U.PACKAGE_DIR.name == "brownfield"
assert U.PACKAGE_DIR.parent.name == "tools"
def test_detect_latest_snapshot_empty(tmp_path):
"""存在しない/空ディレクトリは空文字列。"""
assert U.detect_latest_snapshot(str(tmp_path / "noexist")) == ""
assert U.detect_latest_snapshot(str(tmp_path)) == "" # 空ディレクトリ
def test_detect_latest_snapshot_picks_newest(tmp_path):
(tmp_path / "20260701_120000").mkdir()
(tmp_path / "20260701_180000").mkdir()
result = U.detect_latest_snapshot(str(tmp_path))
assert result.endswith("20260701_180000")
def test_constants_values():
assert "structure" in U.PHASE_KEYS
assert "ai_export" in U.PHASE_KEYS
assert "gemini-single-file" in U.DEFAULT_PROFILES
assert U.DEFAULT_OUT == U.REPO_ROOT / "brownfield_snapshots"
assert U.ORCHESTRATOR_MODULE_NAME == "nexuscore.core.orchestrator"
- Step 2: テストを実行して失敗を確認
Run: python3 -m pytest tests/tools/brownfield/test_utils.py -v Expected: FAIL(ModuleNotFoundError: No module named 'brownfield.utils')
- Step 3:
utils.pyを実装(旧ファイルから転記 + 定数 REPO_ROOT 化)
tools/brownfield/utils.py を作成。以下を記述:
- import 文(旧ファイル L41-42 と同じ):
from __future__ import annotations import os, sys, json, shlex, shutil, argparse, subprocess, zipfile import threading, queue, time, traceback, importlib, importlib.util from pathlib import Path from datetime import datetime, timezone, timedelta from typing import Dict, List, Tuple, Callable, Generator, Optional, Union - 定数(旧 L48-62 を REPO_ROOT 化して転記): ```python JST = timezone(timedelta(hours=9))
本ファイル物理位置基準。symlink/zipapp では要再考。
PACKAGE_DIR = Path(file).resolve().parent # brownfield/ REPO_ROOT = PACKAGE_DIR.parents[1] # brownfield -> tools -> repo root PROJECT_TOP = REPO_ROOT SRC_DIR = REPO_ROOT / “src” TOOLS_DIR = REPO_ROOT / “tools” ORCHESTRATOR_MODULE_NAME = “nexuscore.core.orchestrator”
PICKER_ROOT = Path(os.getenv(“NEXUS_BROWNFIELD_PICKER_ROOT”, str(PROJECT_TOP.parent))).resolve() PHASE_KEYS = [“structure”, “snapshot”, “unified”, “graphs”, “history”, “quality”, “ai_export”] DEFAULT_PROFILES = [“gemini-single-file”, “gpt5-zip”] DEFAULT_OUT = REPO_ROOT / “brownfield_snapshots”
3. **共通ヘルパー(旧 L47-128 を転記・ロジック不改)**: `now_tag`, `candidate_paths`, `phase_cmd`, `_read_json_safe`, `load_policy_meta`, `inject_policy_meta_to_manifest`, `stream_run` を旧ファイル L49-128 から**そのまま転記**。`stream_run` が `SRC_DIR`/`PROJECT_TOP` を参照することを確認(上記定数で解決)。
4. **detect_latest_snapshot(旧 L239-243 を転記)**:
```python
def detect_latest_snapshot(out_root: str) -> str:
root = Path(out_root)
if not root.exists(): return ""
dirs = [p for p in root.iterdir() if p.is_dir()]
return str(sorted(dirs, key=lambda p: p.name, reverse=True)[0]) if dirs else ""
- orchestrator adapter 関数(旧 L245-317 を転記・docstring で adapter 明記):
is_orchestrator_importable(L245-253),run_orchestrator_cli(L255-280),run_orchestrator_func(L281-317)を旧ファイルからそのまま転記。各関数の直前に docstring 追記:def is_orchestrator_importable() -> bool: """adapter: 外部モジュル nexuscore.core.orchestrator の importability を検証。""" ...
- Step 4: テストを実行して成功を確認
Run: python3 -m pytest tests/tools/brownfield/test_utils.py -v Expected: 5 PASS
- Step 5: commit
git add tools/brownfield/utils.py tests/tools/brownfield/test_utils.py
git commit -m "feat(brownfield): utils.py(定数+IO/helper+stream_run+detect_latest_snapshot+orchestrator adapter)"
Task 4: core.py(run_brownfield_stream)+ baseline 前後比較
Files:
- Create:
tools/brownfield/core.py -
Test:
tests/tools/brownfield/test_core_stream.py - Step 1: 失敗テストを書く(baseline 比較)
tests/tools/brownfield/test_core_stream.py:
"""run_brownfield_stream の baseline 前後比較テスト(振る舞い保存の正体)。"""
import json, sys
from pathlib import Path
from typing import List, Tuple, Optional, Generator
_BASELINE = Path(__file__).parent / "_baseline_events.json"
def _fake_stream_run(cmd, cwd) -> Generator[str, None, Tuple[bool, str]]:
"""Task1 の _baseline_capture.py の fake と**完全一致**させること。"""
yield "--- [fake] phase line 1\n"
yield "--- [fake] phase line 2\n"
return (True, "fake output")
def test_run_brownfield_stream_matches_baseline(tmp_path, monkeypatch):
"""新 core.run_brownfield_stream のイベント列が baseline と完全一致。"""
from brownfield import core
# core モジュール内の stream_run 参照を置換(core.py は 'from .utils import stream_run')
monkeypatch.setattr(core, "stream_run", _fake_stream_run)
events = []
gen = core.run_brownfield_stream(
project_root=str(tmp_path / "proj"),
out_root=str(tmp_path / "out"),
profiles=["gemini-single-file"],
selected_phases=["structure", "snapshot"],
policy_profile_ui="", policy_version_ui="", policy_icon_ui="",
richness_mode="Light (fast)",
include_full_archive=False,
)
for ev in gen:
events.append(list(ev))
expected = json.loads(_BASELINE.read_text(encoding="utf-8"))
assert events == expected, f"baseline 不一致: got {len(events)} events, expected {len(expected)}"
重要: Task 1 Step2 で
load_policy_meta等の追加 mock を入れた場合、本テストでもmonkeypatch.setattr(core, "load_policy_meta", lambda *a, **k: {})等、同一 mock を適用すること。
- Step 2: テストを実行して失敗を確認
Run: python3 -m pytest tests/tools/brownfield/test_core_stream.py -v Expected: FAIL(ModuleNotFoundError: No module named 'brownfield.core')
- Step 3:
core.pyを実装(旧 L131-233 を転記)
tools/brownfield/core.py:
- import 文: ```python from future import annotations from typing import Dict, List, Tuple, Generator, Optional from pathlib import Path
from .utils import ( stream_run, # ★ from .utils import(mock 境界・N1対策) load_policy_meta, inject_policy_meta_to_manifest, candidate_paths, phase_cmd, now_tag, PROJECT_TOP, )
2. **`run_brownfield_stream`(旧 L131-233 をそのまま転記)**: 関数シグネチャ `def run_brownfield_stream(project_root: str, out_root: str, profiles: List[str], selected_phases: List[str], policy_profile_ui: str, policy_version_ui: str, policy_icon_ui: str, richness_mode: str, include_full_archive: bool) -> Generator[Tuple[str, str, Optional[str]], None, None]:` と本体(emit クロージャ含む `yield from emit(...)` 構造)を旧ファイル L131-233 から**ロジック不改で転記**。docstring に streaming 契約(spec 3.6)を追記:
```python
"""Brownfield スナップショットをストリーミング実行。
Yields: (log_text: str, summary: str, zip_path: str | None)
※ log_text は「それまでの全行を結合した文字列」
Returns: なし(yield のみ)
"""
転記時、
stream_run(cmd_list, cwd=PROJECT_TOP)の呼び出しはfrom .utils importで束縛したstream_runを参照する(mock が効く)。
- Step 4: テストを実行して成功(baseline 一致)を確認
Run: python3 -m pytest tests/tools/brownfield/test_core_stream.py -v Expected: PASS(イベント列が baseline JSON と完全一致)
もし不一致(FAIL): 転記漏れ・import 参照違いが原因。旧 L131-233 との diff を取り、
yield from emitの位置・stream_run呼出・StopIteration.value取得が一致するか確認。ロジック変更は禁止(一致しない場合は転記ミス)。
- Step 5: commit
git add tools/brownfield/core.py tests/tools/brownfield/test_core_stream.py
git commit -m "feat(brownfield): core.py(run_brownfield_stream) + baseline 前後比較テスト PASS"
Task 5: ui.py(Gradio UI)
Files:
- Create:
tools/brownfield/ui.py -
Test:
tests/tools/brownfield/test_ui_build.py - Step 1: 失敗テストを書く(launch monkeypatch・S3対策)
tests/tools/brownfield/test_ui_build.py:
"""build_ui 構築テスト(launch は monkeypatch で潰す・port bind なし)。"""
def test_build_ui_returns_blocks_and_launch_args(monkeypatch):
from brownfield import ui
captured = {}
def fake_launch(self, *args, **kwargs):
captured["kwargs"] = kwargs
# gradio.Blocks.launch を潰す(実起動回避・B104 踏まない)
import gradio
monkeypatch.setattr(gradio.Blocks, "launch", fake_launch)
demo = ui.build_ui()
assert demo is not None # gr.Blocks インスタンス
demo.launch(prevent_thread_lock=True)
assert captured["kwargs"].get("prevent_thread_lock") is True
- Step 2: テストを実行して失敗を確認
Run: python3 -m pytest tests/tools/brownfield/test_ui_build.py -v Expected: FAIL(ModuleNotFoundError: No module named 'brownfield.ui')
- Step 3:
ui.pyを実装(旧 L235-238, L318-443 を転記)
tools/brownfield/ui.py:
- import 文(旧ファイル冒頭の標準 import のうち UI が必要なもの + 相対 import): ```python from future import annotations import os, shutil, zipfile, threading, time from pathlib import Path from typing import Optional
from .core import run_brownfield_stream from .utils import ( detect_latest_snapshot, is_orchestrator_importable, run_orchestrator_cli, run_orchestrator_func, PICKER_ROOT, PHASE_KEYS, DEFAULT_PROFILES, DEFAULT_OUT, )
2. **`try_build_dir_picker`(旧 L235-238 を転記)**: 関数内 `from gradio import FileExplorer` は**関数内 import のまま保持**(gradio を module-level に上げない・spec m6)。
3. **`build_ui`(旧 L318-431 を転記)**: `import gradio as gr` は**関数内 import のまま**保持。旧 L318-431 の本体をロジック不改で転記。`build_ui` 内で参照する `PICKER_ROOT`, `PHASE_KEYS`, `DEFAULT_PROFILES`, `run_brownfield_stream`, `detect_latest_snapshot`, `is_orchestrator_importable`, `run_orchestrator_cli`, `run_orchestrator_func` は上記 import で解決。
4. **`auto_launch_with_increment`(旧 L432-443 を転記)**: 旧 L432-443 をそのまま転記。
- [ ] **Step 4: テストを実行して成功を確認**
Run: `python3 -m pytest tests/tools/brownfield/test_ui_build.py -v`
Expected: PASS(demo が gr.Blocks・launch の prevent_thread_lock=True を捕捉)
- [ ] **Step 5: commit**
```bash
git add tools/brownfield/ui.py tests/tools/brownfield/test_ui_build.py
git commit -m "feat(brownfield): ui.py(build_ui+auto_launch+try_build_dir_picker)"
Task 6: __main__.py + shim 上書き
Files:
- Create:
tools/brownfield/__main__.py - Modify:
tools/brownfield_orchestrator.py(旧485行 → shim に上書き) -
Test:
tests/tools/brownfield/test_shim_loader.py - Step 1: 失敗テストを書く(shim + main(argv))
tests/tools/brownfield/test_shim_loader.py:
"""shim 経由起動と main(argv) のテスト。"""
import sys
import pytest
def test_main_help_exits_zero(capsys):
"""main(['--help']) は SystemExit(0)。"""
from brownfield.__main__ import main
with pytest.raises(SystemExit) as exc:
main(["--help"])
assert exc.value.code == 0
assert "--ui" in capsys.readouterr().out
def test_shim_module_loads_main():
"""shim ファイル経由で main が解決できる(sys.path 注入経路)。"""
sys.path.pop(0) if str(__import__("pathlib").Path(__file__).resolve().parents[3] / "tools") in sys.path else None
tools_dir = str(__import__("pathlib").Path(__file__).resolve().parents[3] / "tools")
if tools_dir not in sys.path:
sys.path.insert(0, tools_dir)
# brownfield.__main__ を正規 import(shim と同じ経路)
import importlib
mod = importlib.import_module("brownfield.__main__")
assert callable(mod.main)
- Step 2: テストを実行して失敗を確認
Run: python3 -m pytest tests/tools/brownfield/test_shim_loader.py -v Expected: FAIL(ModuleNotFoundError: No module named 'brownfield.__main__')
- Step 3:
__main__.pyを実装(旧 L445-485 を転記・main(argv=None)->int化)
tools/brownfield/__main__.py:
- import 文: ```python from future import annotations import os, argparse from pathlib import Path from typing import Optional
from .ui import build_ui, auto_launch_with_increment from .core import run_brownfield_stream from .utils import PICKER_ROOT, PHASE_KEYS, DEFAULT_PROFILES, DEFAULT_OUT, load_policy_meta
2. **`parse_args`(旧 L445-458 を転記)**: シグネチャを `def parse_args(argv: Optional[list] = None) -> argparse.Namespace:` に変更し、最後を `return p.parse_args(argv)` にする(それ以外は旧 L446-457 のまま)。
3. **`main`(旧 L459-481 を転記・シグネチャ変更)**:
```python
def main(argv: Optional[list] = None) -> int:
args = parse_args(argv)
if args.ui:
base_port = int(os.getenv("NEXUS_BROWNFIELD_UI_PORT", "7862"))
share = os.getenv("NEXUS_BROWNFIELD_UI_SHARE", "0") == "1"
demo = build_ui(); auto_launch_with_increment(demo, base_port, share)
return 0
else:
target = Path(args.project_root).resolve()
if target.is_file(): target = target.parent
out_root = Path(args.out).resolve(); out_root.mkdir(parents=True, exist_ok=True)
profiles = [s.strip() for s in (args.profiles or "").split(",") if s.strip()] or DEFAULT_PROFILES
skip = [s.strip() for s in (args.skip or "").split(",") if s.strip()]
selected_phases = [p for p in PHASE_KEYS if p not in set(skip)]
meta = load_policy_meta()
if args.policy_profile: meta["policy_profile"] = args.policy_profile
if args.policy_version: meta["policy_version"] = args.policy_version
if args.policy_icon: meta["policy_icon"] = args.policy_icon
summary, zip_path = "", None
gen = run_brownfield_stream(str(target), str(out_root), profiles, selected_phases,
meta.get("policy_profile",""), meta.get("policy_version",""), meta.get("policy_icon",""),
args.richness, args.full_archive)
if gen:
for _, summary, zip_path in gen: pass
print(summary)
if zip_path: print(f"[ZIP] {zip_path}")
return 0
if __name__ == "__main__":
import sys
sys.exit(main())
- Step 4: テストを実行して成功を確認
Run: python3 -m pytest tests/tools/brownfield/test_shim_loader.py -v Expected: 2 PASS
- Step 5: shim で
tools/brownfield_orchestrator.pyを上書き
旧485行の tools/brownfield_orchestrator.py を以下の shim で完全上書き(ファイル全体を置換):
"""後方互換エントリ。実体は tools/brownfield/ パッケージ(リファクタ v3 P1)。
起動方法(変更なし): python tools/brownfield_orchestrator.py --ui
tools/ は __init__.py を持たないフラットなスクリプト集のため、brownfield/ パッケージを
解決するには tools/ 自身を sys.path に含める必要がある(shim ローカルで一時挿入)。
"""
import sys
from pathlib import Path
_TOOLS_DIR = Path(__file__).resolve().parent
if str(_TOOLS_DIR) not in sys.path:
sys.path.insert(0, str(_TOOLS_DIR))
from brownfield.__main__ import main # 正規パッケージロード(相対import解決の前提)
if __name__ == "__main__":
sys.exit(main())
- Step 6: shim 経由の実起動を確認
Run: cd ~/projects/NexusCore && python3 tools/brownfield_orchestrator.py --help Expected: argparse のヘルプが表示(--ui を含む)・exit 0
- Step 7: commit
git add tools/brownfield/__main__.py tools/brownfield_orchestrator.py tests/tools/brownfield/test_shim_loader.py
git commit -m "feat(brownfield): __main__.py(main(argv)->int) + shim上書き(後方互換)"
Task 7: 旧コード重複除去 + 全テスト + 最終検証
Files:
-
Verify:
tools/brownfield_orchestrator.py(shim のみ・485行のコードは削除済) -
Step 1: shim ファイルが旧コードを含まないことを確認
Run: wc -l tools/brownfield_orchestrator.py Expected: 20行前後(shim のみ)。485行のコードは残っていない。
Run: grep -c 'def run_brownfield_stream' tools/brownfield_orchestrator.py Expected: 0(旧コードの関数定義は shim に無い)
- Step 2: brownfield 全テストを実行
Run: cd ~/projects/NexusCore && python3 -m pytest tests/tools/brownfield/ -v Expected: 全 PASS(test_imports 4件 + test_utils 5件 + test_core_stream 1件 + test_ui_build 1件 + test_shim_loader 2件 = 13件)
- Step 3: 既存テスト全体の回帰確認
Run: cd ~/projects/NexusCore && python3 -m pytest tests/ -q --ignore=tests/tools/brownfield 2>&1 | tail -5 Expected: 既存テストが brownfield リファクタ前と同等に PASS(既存の失敗がある場合は、それが brownfield 由来でないことを確認)
- Step 4: docs/CI 内の参照にパス変更が不要か確認
Run: cd ~/projects/NexusCore && rg "brownfield_orchestrator" docs/ .github/ 2>/dev/null | head Expected: 監査レポート等の言及のみ(python tools/brownfield_orchestrator.py パスは不変・追記不要)。パス変更を示唆する参照がなければ OK。
- Step 5: baseline 一時ファイルの整理検討
tests/tools/brownfield/_baseline_capture.py は baseline 取得後も再実行可能なため残置可、または _baseline_events.json のみ残して capture スクリプトは削除。チーム判断だが、_baseline_events.json は必ず残す(test_core_stream が依存)。
Run: ls tests/tools/brownfield/_baseline_events.json Expected: 存在
- Step 6: 最終 commit
git add -A
git commit -m "chore(brownfield): 旧コード重複除去 + 全テスト PASS(13件) + 最終検証完了
God file分割完了: 485行→パッケージ6モジュール + shim
振る舞い保存: baseline前後比較 PASS
後方互換: python tools/brownfield_orchestrator.py --ui 動作確認"
完了条件(spec 成功基準との対応)
| spec 成功基準 | 検証タスク |
|---|---|
1. python tools/brownfield_orchestrator.py --ui が変更なしで動作 | Task 6 Step6(–help)+ 実機で –ui 起動確認 |
| 2. 各モジュール単独 import・依存単方向 | Task 2/3/4/5 の各テスト |
| 3. baseline 前後比較でイベント列一致 | Task 1 + Task 4(test_core_stream PASS) |
| 4. build_ui() が gr.Blocks 返し launch 引数捕捉 | Task 5(test_ui_build PASS) |
フォローアップ(別Issue・本計画の scope 外)
- B602 subprocess shell=True 対策(stream_run の cmd 出所検証)
- B104 0.0.0.0 バインド切替機構
- brownfield 単体テスト拡充(本最小テストを土台に)