"""FR-170 P2 拆卡底表（搬 scan、netdiag、files；console 退聚合；刪舊 core/）。
前提：P1（CM-3324，批 12～15）全部合回主線後才放行批 16；P2 母卡獨立（P1 母卡結案要求子卡全 Done）。
AST 掃出的事實（2026-10-30）：scan 25 支 7,605 行、netdiag 16 支 2,091 行、files 8 支 820 行、console 20 支 3,486 行。
scan → sync 13 條全是 allowed_targets／tool_schemas／outbox／heartbeat_status 四個 → 抽三個 port（P2-1）；
scan → console 7 條全是 ops（重啟旗標，歸 platform）與 secret_form（表單驗證，歸 scan）；
console 出邊 84 條（identity／sync 53 條 P1-5 已處理，scan／netdiag／files 31 條 P2-4 處理）＝退聚合不是剪線：只准 import 對方 app/ 與 ports/。
"""

CARDS = [
dict(id="P2-1", stage=16, repo="agent", model="opus", effort="high",
  title="scan 的三個 port 與歸位：TargetAuthorizer／ToolSchemaCatalog／ResultOutbox 由 sync 註冊 platform；ops 重啟旗標歸 platform、secret_form 歸 scan；migrations 拆給各模組申報",
  ren="new：`guidant_agent/sync/ports/{target_authorizer,tool_schema_catalog,result_outbox}.py`（Protocol）＋ sync 的 adapters 實作＝P1 搬進去的 `allowed_targets`／`tool_schemas`／`outbox` 本體，`sync.register()` 把三個實例掛 `container.sync.*`；scan 的 13 條 `→ sync` 邊改從 container 取 port（`task_executor`、`local_runs`、`probe_runner`、`credential_handover`、`credential_resolver`、`task_sinks`、`inspec.py`）。`heartbeat_status` 四處改讀 P1-4 的 `platform/health.py`。歸位：`core/console/ops.py` 的重啟旗標與 uptime 那段 → `guidant_agent/platform/ops.py`（`local_ops`、`restart_flag`、`task_executor`、`local_runs` 七條邊消失）；`core/console/secret_form.py` → `guidant_agent/scan/app/secret_form.py`（只有 scan 的工具帳密與 LDAP 自設表單用，LDAP 那支 P1 已改走 identity 的 `CredentialRepo`）。migrations：legacy 的 v1（`local_credentials`、`offline_operators`→identity；`local_runs`→scan；`outbox`→sync；`upload_files` heal→files）、v2 console tables→console、v3／v4 local_runs→scan、v5→identity、v6 system_logs→platform、v7→files，**版本號不變、步驟函式 `git mv` 到各模組 `migrations.py`**，`registry.migrations()` 已會攤平排序與重號檢查；legacy ModuleSpec 的 migrations 變空清單",
  edit=["core/task_executor.py、core/local_runs.py、core/probe_runner.py、core/credential_handover.py、core/credential_resolver.py、core/task_sinks.py、core/task_executor_connectors/inspec.py（import 改 port）", "guidant_agent/sync/__init__.py（register 掛三個 port）", "guidant_agent/platform/container.py", "core/console/ops.py（退成殼）", "core/console/secret_form.py（退成殼）", "core/local_ops.py、core/restart_flag.py（import 改 platform/ops）", "guidant_agent/platform/local_db/migrations.py（拆空，只留 add_missing_columns 工具）", "guidant_agent/identity/migrations.py、sync/migrations.py（接收各自步驟）", "test/test_module_boundaries.py（ALLOWLIST 刪 ≥20 條）", "test/test_no_module_globals.py（ops._lock 列）"],
  new=["guidant_agent/sync/ports/target_authorizer.py、tool_schema_catalog.py、result_outbox.py", "guidant_agent/platform/ops.py", "guidant_agent/scan/__init__.py、scan/app/secret_form.py、scan/migrations.py", "guidant_agent/files/migrations.py、console/migrations.py、platform/migrations.py（各接收步驟）", "test/test_scan_ports.py", "test/test_migrations_split.py（攤平後順序與 P1 結束時 MIGRATIONS 完全相同、重號拒絕）"],
  deps=[], why="跨四個模組的 port 設計與 migration 拆分，順序錯一步 agent.db 升級就壞；需 opus",
  accept="`registry.migrations()` 攤平後的 `(version, 函式名)` 清單與拆分前逐項相同（測試斷言）；舊 agent.db v7 起服務不跑任何 migration（log 無 migrated）、空庫起服務到 v7；ALLOWLIST 少 ≥20 條且 scan→sync 歸零；scan 對 sync 的 import 只剩 `guidant_agent.sync.ports`；`pytest test/` 全綠；突變：①`TargetAuthorizer` 實作拿掉 `is_allowed` 判斷 → `test_fr166_guards::test_every_entry_checks_allowed_targets` 紅；②兩個模組申報同號 → `test_migrations_split` 紅"),

dict(id="P2-2", stage=17, repo="agent", model="opus", effort="high",
  title="scan 模組搬遷：25 支進 guidant_agent/scan/，connectors 進 adapters/connectors/，local_runs SQL 進 adapters、八個全域收類別、api/detection 進 scan/api；超標兩檔只搬不塞",
  ren="new：`guidant_agent/scan/{domain,app,ports,adapters,api}` 補齊（`__init__`、`migrations.py` P2-1 已建）。搬法 `git mv`：`task_executor.py`→`scan/app/task_executor.py`（`dispatch_tasks`、`_run_one`）；`task_executor_connectors/*`→`scan/adapters/connectors/*`（11 支，`base.py` 進 `scan/ports/connector.py` 當 Protocol＋`adapters/connectors/base.py` 共用實作）；`local_runs.py`→`scan/app/local_runs.py`＋`scan/adapters/local_runs_repo.py`（SQL）；`running_tasks.py`→`scan/app/running_tasks.py`（四個全域 `LOCK`／`REGISTRY`／`_meta`／`_targets` 收成 `RunningTasks` 類別，platform 給單例——P0 的 `update_sync` 已在用它的 `snapshot()`，介面不變）；`scan_input.py`→`scan/app/scan_input.py`（三個全域收類別）；`credential_store.py`→`scan/adapters/credential_store.py`（Fernet 檔案存取；`_lock` 收實例）；`credential_resolver.py`、`credential_handover.py`、`probe_runner.py`、`task_sinks.py`→`scan/app/`；`api/detection/`→`scan/api/`（blueprint 由 `scan.register()` 掛，legacy 刪那行）。`sonarqube.py` 1,711 行、`inspec.py` 1,052 行**只搬不改**，白名單路徑改新、數字不變（拆它們是獨立的卡，不在 P2）。`ssh_host_keys._LOCK`、`sbom_scan._running_lock` 收實例",
  edit=["25 支 scan 檔退成殼", "guidant_agent/platform/registry.py（MODULES 加 scan；legacy 刪 detection blueprint 那行）", "guidant_agent/platform/container.py（scan 子容器）", "test/test_module_boundaries.py（LEGACY_MODULE_OF 刪 25 列）", "test/test_no_module_globals.py（刪八列）", "test/test_sql_only_in_adapters.py（刪 local_runs）", "test/test_file_and_function_size.py（12 列改路徑，數字不得變大）", "docs/ARCHITECTURE.md §1 scan 列路徑改新、docs/oversized-files.md 路徑改新"],
  new=["guidant_agent/scan/ 五層與 25 支本體", "test/test_scan_module_register.py"],
  deps=["P2-1"], why="25 支 7,600 行、11 支 connector 各有 subprocess 與帳密處理，切層要對且資安守衛（無 shell、帳密不進 argv）全程綠；需 opus",
  accept="`guidant_agent/scan/` 五層齊、`domain/` 零框架 import；SQL 只在 adapters；LEGACY_MODULE_OF 裡 scan 列為 0；`test_fr166_guards`（無 shell、入口過清冊、scope 表）、`test_netdiag_guards` 全綠；11 支 connector 測試斷言不改；demo 模式 `python main_app.py` health 200；用 Flask test client 走 `/detection/probe` 與主控台手動掃描（nmap 掃 127.0.0.1，既有測試有 fixture）各一次 200 並落 local_runs；**真機派工單留測試期首腦驗**；突變：①`RunningTasks.snapshot` 回空 → `test_update_sync::test_observe_with_update_starts_background_sync` 的「有工作在跑不下載」case 紅；②`credential_store` adapter 拿掉 0600 → 測試紅"),

dict(id="P2-3", stage=17, repo="agent", model="opus", effort="medium",
  title="netdiag ＋ files 搬遷：netdiag 16 支進 guidant_agent/netdiag/（probes 進 adapters）；files 8 支進 guidant_agent/files/（blob API、browser_plane 8444、storage、file_protect）",
  ren="**netdiag**：`core/netdiag/__init__.py`→`netdiag/app/runner.py`（`_RUN_LOCK` 收類別）；`probes/*`→`netdiag/adapters/probes/*`（11 支，`probes/__init__.py` 的 `run_cmd`／`call_with_timeout` 本體進 `shared/timeouts.py`，P1-4 已搬 `call_with_timeout`，`run_cmd` 這次跟上）；`dashboard.py`、`verdict.py`、`report.py`、`limits.py`→`netdiag/app/`（`limits._SESSION_LOCK` 收類別）；4 條出邊：`→ allowed_targets`／`heartbeat_status`改 port 與 health、`→ cloud_trust` 改 sync port `CloudTrust`（P2-1 補或本卡補）、`→ evidence_sha256` 改 shared。**files**：`api/blob/`→`files/api/`；`browser_plane_server.py`→`files/api/browser_plane.py`（8444 監聽；`→ console_allow_cidr` 改 identity port `SourceCidrGuard`、`→ agent_auth.verify_upload_ticket` 改 sync port `UploadTicketVerifier`、`→ scan_input` 改 scan port `ScanInputSlots`、`→ console.routes` 那條是為了掛 console blueprint 到 8444，改由 platform bootstrap 掛）；`storage_settings.py`→`files/app/storage_settings.py`（`→ credential_store` 改 scan port `SecretStore`：design D4 說加密保管底層抽成 `platform.SecretStore`，本卡落實——Fernet 檔案存取從 scan 的 credential_store 下沉 `platform/secret_store.py`，scan 與 files 都用它）；`storage_trust.py`、`evidence_sha256.py`→`files/app/`；`core/console/file_protect.py`→`files/app/file_protect.py`。files `migrations.py` 接 v1 upload_files heal 與 v7（P2-1 已拆）。兩模組同批不撞檔",
  edit=["16 支 netdiag 檔退成殼、8 支 files 檔退成殼", "guidant_agent/platform/registry.py（MODULES 加 netdiag、files；legacy 刪 blob blueprint 那行）", "guidant_agent/platform/bootstrap.py（8444 由 files 模組起、console blueprint 掛法）", "guidant_agent/platform/secret_store.py（新，Fernet 下沉）＋ guidant_agent/scan/adapters/credential_store.py 改用它", "test/test_module_boundaries.py（LEGACY_MODULE_OF 刪 24 列；ALLOWLIST 刪 netdiag／files 相關 ≥15 條）", "test/test_no_module_globals.py（刪 netdiag 兩列）", "test/test_file_and_function_size.py（三列改路徑）", "docs/ARCHITECTURE.md §1 兩列"],
  new=["guidant_agent/netdiag/ 五層與 16 支本體", "guidant_agent/files/ 五層與 8 支本體", "guidant_agent/platform/secret_store.py", "test/test_netdiag_module_register.py、test/test_files_module_register.py、test/test_secret_store.py"],
  deps=["P2-1"], why="兩個模組都小但 files 的 8444 監聽與 CIDR／票驗證／上傳槽三個 port 要跨三模組接；SecretStore 下沉動到 Fernet 金鑰檔，錯了既有帳密全解不開；需 opus",
  accept="兩模組五層齊；`test_netdiag_guards` 五支全綠（無 shell、subprocess 單一出口、固定逾時、無 CERT_NONE、無 nmap）；`test_fr166_guards::test_no_capture_or_bandwidth_tools_and_no_nmap_in_netdiag` 綠；舊 agent.db 的 `<data>/secrets/tool-credentials.enc` 用 SecretStore 仍解得開（測試：P1 結束時的金鑰與密文 fixture）；本機 Agent 連 DEV：8444 瀏覽器直傳一個源碼包、主控台網路除錯一筆、檔案預覽與加密下載各一（首腦驗）；突變：①`SecretStore` 改金鑰路徑 → 既有密文測試紅；②browser_plane 拿掉 `SourceCidrGuard` → CIDR 測試紅"),

dict(id="P2-4", stage=18, repo="agent", model="opus", effort="high",
  title="console 退聚合：20 支搬 guidant_agent/console/，31 條對 scan／netdiag／files 的 import 改 app／ports 層；routes.py 297 行拆成登記表＋各頁 routes；console 不碰 DB 守衛",
  ren="new：`guidant_agent/console/{api,app}`（console 只有兩層：`api/` 是 blueprint 與路由、`app/` 是聚合——規則 §1 說它不碰 DB、不保狀態，所以沒有 domain／adapters）。搬法：`routes.py` 拆成 `console/api/__init__.py`（`console_bp`、`require_session`、`register(app, container)`）＋`console/api/pages.py`（總覽與檔案頁那幾支 route）；`*_routes.py` 八支→`console/api/`；`overview.py`、`stats.py`、`files.py`、`tools.py`、`heartbeat.py`、`inline_policy.py`、`body_limit.py`→`console/app/`；`audit_import.py`→`platform/`（它是把主系統稽核匯進本機 system_logs，屬宿主）；`audit.py` P1-1 已進 shared、`file_protect.py` P2-3 已進 files、`ops.py`／`secret_form.py` P2-1 已歸位、`session.py`／`local_*.py` P1-2 已進 identity。31 條出邊逐條改：只准 `from guidant_agent.<模組>.app import ...` 或 `.ports`，不准 `.adapters`（守衛 `test_api_layer_does_not_import_adapters` 已擋）；`overview`／`stats`／`files` 三支有 SQL（白名單）→ 改呼叫各模組 app 的查詢方法，SQL 白名單刪三列。`console/stats._cache` 全域收類別。`heartbeat.py` 的 `fields()`／`observe()` 改實作 P1-3 的 `HeartbeatObserver`、由 console ModuleSpec 申報。console blueprint 由 `console.register()` 掛（8010 主 app 與 8444 browser app 都要，P2-3 定的掛法）",
  edit=["20 支 console 檔退成殼", "guidant_agent/platform/registry.py（MODULES 加 console，排最後）", "guidant_agent/platform/bootstrap.py", "test/test_module_boundaries.py（LEGACY_MODULE_OF 刪 console/ 前綴列；ALLOWLIST 刪 console→* 全部）", "test/test_sql_only_in_adapters.py（刪 console 四列）", "test/test_no_module_globals.py（刪 stats 兩列、audit._once_lock 若已隨 P1-1 走則確認）", "docs/ARCHITECTURE.md §1 console 列"],
  new=["guidant_agent/console/api/、app/ 與 20 支本體", "test/test_console_module_register.py", "test/test_console_no_db.py（守衛：console/ 下零 `text(`／`.execute(`／`sqlalchemy` import）"],
  deps=["P2-2", "P2-3"], why="31 條 import 改層、三支有 SQL 的聚合要改成呼叫各模組 app 方法（不存在的要在對方 app 補薄方法）、routes.py 拆法；需 opus",
  accept="`guidant_agent/console/` 零 SQL、零 adapters import（兩支守衛證）；ALLOWLIST 裡 `console →` 邊為 0；42 條路由與 P0 時完全相同（`git show d2fe93f` 對照）；`test_console_*` 全部測試斷言不改；本機 Agent 連 DEV：主控台總覽、檔案、檢測工具、執行紀錄、網路除錯、日誌、維運七頁各開一次＋四種登入（首腦照 FR-166 SPEC 驗）；突變：①`require_session` 拿掉 scope 判斷 → `test_ops_scope_table` 紅；②console 直接 import 某 adapters → 守衛紅"),

dict(id="P2-5", stage=19, repo="agent", model="sonnet", effort="medium",
  title="刪舊 core/：殼全刪、api/ 與 di_containers/ 清空、FROZEN_FILES／LEGACY_MODULE_OF／ALLOWLIST 歸零、守衛改成「core/ 不存在」；install 腳本與 bats 內的舊路徑改新；全量＋真機完整一輪",
  ren="extend：對 `core/`、`api/`、`di_containers/` 剩下的每支殼 `grep -rn '<舊模組路徑>' --include='*.py' --include='*.sh' --include='*.bats' --include='*.md' --include='*.toml' --include='*.txt' .`（含 `scripts/build/`、`scripts/install/`、`deploy/`、`test/`、`docs/`、`pyproject.toml`、Nuitka 的 include 清單、`tools-manifest`）；命中的**改成新路徑**（這次不是「有命中就留」，是全改），零命中才刪；`core/__init__.py`、`api/__init__.py`、`di_containers/` 最後刪。`test/test_no_new_flat_core.py` 改成斷言 `core/` 目錄不存在；`LEGACY_MODULE_OF` 刪到只剩 `main_app.py`、`config/`、`common/`（這三個 P3 或之後再議）與 `guidant_agent/` 前綴；`ALLOWLIST` 歸零（還有邊就是沒剪乾淨，回寫列出、停下）；`test_sql_only_in_adapters` 白名單只剩 `local_state_cli`／`support_bundle`（platform 維運，之後再議）。`scripts/build/build_agent_bundle.sh` 的編譯清單、`common/integrity` 的 manifest 檔案清單若列舊路徑一併改。`docs/ARCHITECTURE.md` §1 表「例子」欄全改新路徑、§4 白名單數字歸零、刪「過渡期」字句；Agent `CLAUDE.md`「程式放哪裡」改成終局敘述",
  edit=["core/、api/、di_containers/ 下全部殼（刪）", "scripts/build/、scripts/install/、deploy/、test/ 內舊路徑字串", "test/test_no_new_flat_core.py、test_module_boundaries.py、test_sql_only_in_adapters.py、test_no_module_globals.py、test_file_and_function_size.py（白名單歸零或只剩 platform 維運列）", "docs/ARCHITECTURE.md、docs/oversized-files.md、CLAUDE.md"],
  new=["test/test_no_legacy_tree.py（守衛：`core/`、`api/`、`di_containers/` 不存在；`ALLOWLIST` 為空）"],
  deps=["P2-4"], why="機械性刪除＋全 repo 字串改路徑＋對帳；判斷點只有「grep 命中要改成什麼」，卡上規則寫死",
  accept="新守衛綠；`pytest test/` 全綠；`scripts/build/build_all.sh --dry-run`（若無此旗標，至少 `grep` 編譯清單與 manifest 清單無 `core/`、`api/`、`di_containers/` 字樣）；`AGENT_INTEGRITY_FORCE=1` 源碼模式起服務走完整驗章流程不炸；**真機 Nuitka 出包與 FR-166 手測清單留測試期首腦在 188 做**；`find . -path ./.worktrees -prune -o -name '*.py' -print | xargs grep -l 'from core\\.\\|import core\\.' ` 為 0；42 條路由與 P0 時相同"),
]
