"""FR-170 P1 拆卡底表（搬 identity、sync）：design.md §5 P1 段、Notion spec、工作坊四欄從這張表產。

P1 前提（決策者 2026-10-30）：卡先建不放行；發版與線上更新用 P0 版（Agent 9044d95）；P0 若要修先合主線再放行批 12。
搬法照 design D9 絞殺式：一卡一模組、`git mv`、舊路徑留 re-export 殼（`sys.modules` 別名）、`LEGACY_MODULE_OF` 刪該列、各白名單跟著改。
三條要先剪的線（首腦 2026-10-30 分析，決策者「照建議」）：
  ① `core/console/audit.py` 八處跨模組用 → 搬 shared；② `LOCAL_ADMIN_SUB` 九處用 → 搬 shared；
  ③ 對主系統的 HTTP 通道（`task_executor._post／_get_stream／_build_file_downloader`）五支 sync 檔在用 → 抽成 sync 自己的 adapter，scan 反過來用 port。
批 12 兩張同時跑：1-1 改 shared 與三支殼，1-2 改 identity 八支；1-2 import audit 時寫新路徑，殼讓兩邊都通。
"""

CARDS = [
dict(id="P1-1", stage=12, repo="agent", model="sonnet", effort="medium",
  title="shared 收攏：audit()、LOCAL_ADMIN_SUB、heartbeat_wake 搬 guidant_agent/shared/，舊路徑留殼，白名單刪對應邊",
  ren="new：`guidant_agent/shared/__init__.py`、`shared/audit.py`（`git mv core/console/audit.py`，含 `audit`／`audit_once`／`page_viewed`，只依賴 logging 與 event_codes）、`shared/constants.py`（`LOCAL_ADMIN_SUB` 從 `core/console/local_login.py` 搬來，login 檔改 import 它）、`shared/heartbeat_wake.py`（`git mv core/heartbeat_wake.py`，全域 `_wake` 收成 `HeartbeatWake` 類別單例、模組層留 `wait()`／`kick()` 薄函式）。extend：三支舊路徑改成 re-export 殼（照 `core/app_factory.py` 的 `sys.modules[__name__] = _impl` 寫法，`patch` 才打得到本體）；`test/test_module_boundaries.py` 的 `LEGACY_MODULE_OF` 加 `guidant_agent/shared/`＝SHARED、刪 `core/console/audit.py`／`core/heartbeat_wake.py` 兩列，`ALLOWLIST` 刪所有「→ core/console/audit.py」與「→ core/heartbeat_wake.py」的邊（約 10 條）；`test/test_no_module_globals.py` 刪 `heartbeat_wake._wake` 列。**不改任何呼叫端的 import 行**（殼負責），呼叫端改路徑是各模組搬遷卡的事",
  edit=["core/console/audit.py（退成殼）", "core/heartbeat_wake.py（退成殼）", "core/console/local_login.py（LOCAL_ADMIN_SUB 改 import shared）", "test/test_module_boundaries.py（LEGACY_MODULE_OF、ALLOWLIST）", "test/test_no_module_globals.py", "docs/ARCHITECTURE.md §1 shared 列加三支"],
  new=["guidant_agent/shared/__init__.py", "guidant_agent/shared/audit.py", "guidant_agent/shared/constants.py", "guidant_agent/shared/heartbeat_wake.py", "test/test_shared_shells.py"],
  deps=[], why="純搬檔＋改白名單，形狀已定（P0 的 app_factory 殼是範本）；判斷點只有「殼是不是真的讓 patch 打得到本體」，用測試證",
  accept="六支守衛綠且 ALLOWLIST 比開工前少 ≥10 條（回寫前後數字）；`from core.console.audit import audit` 與 `from guidant_agent.shared.audit import audit` 是同一個物件（`is`）；`patch('core.console.audit._emit')` 能攔到 shared 本體的呼叫（test_shared_shells 斷言）；`pytest test/` 全綠；`python main_app.py` demo health 200、log 無 Traceback"),

dict(id="P1-2", stage=12, repo="agent", model="opus", effort="high",
  title="identity 模組：8 支搬 guidant_agent/identity/，local_credentials 的 SQL 進 adapters、密碼雜湊改接 jedi-password、三個全域收類別、ModuleSpec 登記",
  ren="new：`guidant_agent/identity/{__init__,domain,app,ports,adapters,api}`。搬法：`git mv` 八支——`core/local_credentials.py`→`identity/adapters/credential_repo.py`（SQL 全在這；雜湊改 `jedi_password.hashing.hash_password／verify_password／needs_rehash`，argon2 參數對齊套件；**舊雜湊必須仍可驗、驗過即 rehash**）＋`identity/domain/lockout.py`（失敗計數與鎖定規則，純 Python）；`core/ldap_source.py`→`identity/adapters/ldap_source.py`；`core/offline_operators.py`→`identity/app/offline_operators.py`（SQL 抽到 adapters）；`core/offline_login_window.py`→`identity/app/offline_login_window.py`；`core/console_allow_cidr.py`→`identity/domain/allow_cidr.py`（純解析與比對）；`core/console/session.py`→`identity/app/session.py`；`core/console/local_login.py`、`core/console/local_auth.py`→`identity/app/`。`ports/`：`CredentialRepo`、`Authenticator`（LDAP 與本機密碼兩個實作）、`HeartbeatObserver`（offline_operators／offline_login_window 的 `observe(data)` 改實作它，給 1-3 的 registry 掛）。全域收類別：`console_allow_cidr._INSTANCE_LOCK`、`session._stores`、`local_credentials._lock／_directory_inflight`，由 `identity/__init__.py` 的 `register(app, container)` 建單例掛 `app.extensions`。`ModuleSpec(name='identity', register=..., migrations=[])`——migrations 仍由 legacy 申報（v1 建 local_credentials／offline_operators 表的步驟不拆，P2 統一拆），本卡只登記 register。八支舊路徑留殼。**identity → sync 的四條邊**（`agent_auth.CONSOLE_SCOPES`、`heartbeat_status.offline_login_disabled`、`agent_name`、`allowed_targets.get().received`）本卡**不剪**，先保留在 ALLOWLIST（改路徑），1-4 搬 sync 時用 port 一起解",
  edit=["core/local_credentials.py、core/ldap_source.py、core/offline_operators.py、core/offline_login_window.py、core/console_allow_cidr.py、core/console/session.py、core/console/local_login.py、core/console/local_auth.py（八支退成殼）", "guidant_agent/platform/registry.py（MODULES 加 identity，排 legacy 之前）", "test/test_module_boundaries.py（LEGACY_MODULE_OF 刪八列；ALLOWLIST identity 相關邊改新路徑）", "test/test_no_module_globals.py（刪三列）", "test/test_sql_only_in_adapters.py（刪 local_credentials、offline_operators 兩列）", "pyproject.toml（加 jedi-password>=0.1.0,<0.2；argon2-cffi 若無人再用則移除）", "docs/ARCHITECTURE.md §1 identity 列路徑改新"],
  new=["guidant_agent/identity/ 五層骨架與八支本體", "test/test_identity_password_migration.py（舊 argon2 雜湊可驗且 rehash）", "test/test_identity_module_register.py"],
  deps=[], why="跨層設計：八支檔要切 domain／app／adapters，密碼雜湊換套件要保舊資料可登入，port 介面要給 1-4 用；需要 opus 判斷切法",
  accept="`guidant_agent/identity/` 五層齊、`domain/` 零 import SQLAlchemy／Flask／ldap3（守衛證）；SQL 只在 `adapters/`；三個全域守衛列已刪；舊 agent.db（188 複本或本機 v7）裡既有的 local-admin 密碼仍能登入主控台、登入後該列雜湊變成 jedi-password 格式（測試斷言＋手測 curl `/console/local-login`）；突變：①`verify_password` 改恆 True → 錯密碼登入測試紅；②拿掉 rehash → migration 測試紅；③`register` 漏掛 session 單例 → 主控台任何 API 401；`pytest test/` 全綠；`test_fr166_guards` 全綠（scope 表、無建帳號路由不變）"),

dict(id="P1-3", stage=13, repo="agent", model="opus", effort="high",
  title="sync 通道與 observer 掛點：_post／_get_stream／_build_file_downloader 從 task_executor 抽成 sync/adapters/cloud_channel.py；心跳 observer 改走 registry；scan 改用 port",
  ren="new：`guidant_agent/sync/{__init__,ports,adapters}`（本卡只開這兩層，其餘 1-4 搬）。`sync/ports/cloud_channel.py`：`CloudChannel` Protocol——`post(path, payload) -> dict`、`get_stream(path, dest, *, max_bytes, cancel_event, resume) -> int`、`file_downloader()`；`sync/adapters/cloud_channel.py`：實作，本體就是 `task_executor._post／_get_stream／_build_file_downloader` 三支 `git mv` 過來（信任錨 `cloud_trust.cloud_tls_verify`、通行證 `agent_auth`、串流上限、重導向規則一字不改）。`sync/ports/heartbeat_observer.py`：`HeartbeatObserver` Protocol（`fields() -> dict`、`observe(data) -> None`）＋ `platform/registry.py` 加 `observers: list` 欄位讓模組申報；`core/enroll.py` 的 `_observe_heartbeat` 改成「跑 registry 上登記的 observer」，點名呼叫的那串（offline_operators、offline_login_window、agent_name、restart_flag、console_heartbeat、tool_schemas、vulndb_sync、profile_sync、update_sync、allowed_targets、error_tracking）**先全部由 legacy ModuleSpec 原地申報**，行為與順序不變；1-2 做好的 identity observer 改由 identity 申報、legacy 刪那兩個。extend：`core/task_executor.py` 三支函式退成呼叫 `container.sync.cloud_channel`（DI 給）；`outbox`／`profile_sync`／`vulndb_sync`／`update_sync` 四支的 `from core.task_executor import _post／_get_stream／_build_file_downloader` 改 port；`ALLOWLIST` 刪 `outbox→task_executor`、`profile_sync→task_executor`、`vulndb_sync→task_executor`、`update_sync→task_executor`、`enroll→offline_*` 共 6 條（CM-3314 留的那兩條在內）。**enroll 本體與其餘 sync 檔本卡不搬**",
  edit=["core/task_executor.py（三支函式退成 port 呼叫）", "core/enroll.py（_observe_heartbeat 改跑 registry）", "core/outbox.py、core/profile_sync.py、core/vulndb_sync.py、core/update_sync.py（import 改 port）", "guidant_agent/platform/registry.py（ModuleSpec 加 observers）", "guidant_agent/platform/container.py（sync 子容器）", "guidant_agent/platform/bootstrap.py（legacy 申報既有 observer）", "test/test_module_boundaries.py（ALLOWLIST 刪 6 條）"],
  new=["guidant_agent/sync/__init__.py", "guidant_agent/sync/ports/cloud_channel.py", "guidant_agent/sync/ports/heartbeat_observer.py", "guidant_agent/sync/adapters/cloud_channel.py", "test/test_cloud_channel_port.py", "test/test_heartbeat_observer_registry.py"],
  deps=["P1-1", "P1-2"], why="本案最危險的一張：動心跳迴圈與下載通道，FR-167 線上更新、漏洞庫、profile、outbox 全走它；要在不改行為下換掛點，需 opus",
  accept="`test_heartbeat_loop_timing`、`test_update_sync` 24 支、`test_vulndb_sync`、`test_profile_sync`、`test_outbox`、`test_background_registry` 全綠且**斷言一字不改**；observer 執行順序與開工前 `_observe_heartbeat` 的點名順序相同（測試用 registry 順序斷言）；`grep -rn 'from core.task_executor import _post\\|_get_stream\\|_build_file_downloader' core guidant_agent` 只剩 task_executor 自己；ALLOWLIST 少 6 條；突變：①`cloud_channel.get_stream` 拿掉 `max_bytes` 判斷 → vulndb 超量測試紅；②registry 漏跑 identity observer → `test_fr166_guards::test_unsynced_refuses_every_scan_and_diag_entry` 紅；真機：本機 Agent 連 DEV BE 跑 10 分鐘，心跳 200、profile 同步、漏洞庫同步 log 與開工前同形（首腦驗）"),

dict(id="P1-4", stage=14, repo="agent", model="opus", effort="high",
  title="sync 模組搬遷：17 支搬 guidant_agent/sync/，outbox SQL 進 adapters、六個全域收類別、identity↔sync 四條邊用 port 解、資料面 8443 進 sync/api",
  ren="new：`guidant_agent/sync/{domain,app,api,migrations.py}` 補齊。搬法 `git mv`：`enroll.py`→`sync/app/enroll.py`（註冊、心跳迴圈、`HeartbeatComponent`）＋`sync/domain/registration.py`（CSR、指紋、重註冊規則）；`agent_auth.py`→`sync/domain/agent_auth.py`（票與 JWT 驗證純邏輯）＋`sync/adapters/cert_files.py`（讀寫憑證檔）；`data_plane_auth.py`、`data_plane_server.py`→`sync/api/`；`outbox.py`→`sync/app/outbox.py`＋`sync/adapters/outbox_repo.py`（SQL）；`profile_sync.py`、`profile_cache.py`、`vulndb_sync.py`、`vulndb_bundle.py`、`update_sync.py`、`tool_schemas.py`、`file_protect_sync.py`→`sync/app/`；`allowed_targets.py`→`sync/app/allowed_targets.py`（`→ netdiag.probes.call_with_timeout` 那條改 import `shared`，把 `call_with_timeout` 搬 shared）；`agent_name.py`、`heartbeat_status.py`、`cloud_trust.py`→`sync/app/`。**identity↔sync 四條邊**：`CONSOLE_SCOPES` 搬 `shared/constants.py`；`heartbeat_status.offline_login_disabled` 與 `offline_login_window.days` 的互依改成 identity 的 observer 讀 `platform` 提供的 `last_heartbeat_ok_at()`（heartbeat_status 本身歸 platform 的健康狀態，搬 `platform/health.py`）；`session → agent_name` 改 identity 從 `app.extensions` 讀顯示名；`offline_operators → allowed_targets.get().received` 改 port `TargetsReceived`。六個全域收類別：`agent_name._lock`、`allowed_targets._INSTANCE_LOCK`、`file_protect_sync._lock`、`heartbeat_status._lock`、`tool_schemas._fetch_lock`、`heartbeat_wake`（1-1 已收）。`ModuleSpec(name='sync', observers=[...], migrations=[])`，outbox 表仍由 legacy v1 建（P2 拆）。17 支舊路徑留殼；`core/console/heartbeat.py`（console 歸屬）本卡不動",
  edit=["17 支 sync 檔退成殼", "guidant_agent/platform/registry.py（MODULES 加 sync）", "guidant_agent/platform/health.py（新，heartbeat_status 搬入）", "guidant_agent/shared/constants.py（加 CONSOLE_SCOPES）、shared/timeouts.py（call_with_timeout）", "test/test_module_boundaries.py（LEGACY_MODULE_OF 刪 17 列；ALLOWLIST 刪 identity↔sync 四條＋sync→netdiag 一條）", "test/test_no_module_globals.py（刪五列）", "test/test_sql_only_in_adapters.py（刪 outbox）", "test/test_file_and_function_size.py（enroll／outbox／profile_cache 四列改新路徑，數字不得變大）", "docs/ARCHITECTURE.md §1 sync 列路徑改新"],
  new=["guidant_agent/sync/ 其餘層與 17 支本體", "guidant_agent/platform/health.py", "test/test_sync_module_register.py"],
  deps=["P1-3"], why="17 支、3,700 行、含心跳與資料面；切法要對（domain 不碰檔案系統與 Flask），需 opus",
  accept="`guidant_agent/sync/` 五層齊、`domain/` 零框架 import；SQL 只在 adapters；LEGACY_MODULE_OF 裡 sync 列為 0；ALLOWLIST 再少 5 條；`pytest test/` 全綠且 sync 相關測試斷言不改（只改 import 路徑與 patch 目標）；`test_fr166_guards`、`test_netdiag_guards` 全綠；本機 Agent 連 DEV BE：註冊狀態不變（agent_uid 相同）、心跳 200、主系統直推 8443 測試連線成功、主控台登入成功（首腦驗）；突變：①`domain/agent_auth.verify_data_plane_jwt` 拿掉 `bound_fp` 比對 → 指紋不符測試紅；②outbox adapter 拿掉 `sent_at` 更新 → 重送測試紅"),

dict(id="P1-5", stage=15, repo="agent", model="sonnet", effort="medium",
  title="console 退聚合＋P1 收尾：console 對 identity／sync 的 import 改走 app service；刪確定無人 import 的殼；LEGACY_MODULE_OF 與 ALLOWLIST 對帳；全量＋真機",
  ren="extend：`core/console/routes.py`、`overview.py`、`heartbeat.py`、`local_auth_routes.py`、`log_routes.py` 對 identity／sync 的 import 改成 `from guidant_agent.identity.app import ...`／`from guidant_agent.sync.app import ...`（只准 import `app/` 層與 `ports/`，不准 `adapters/`，守衛 `test_api_layer_does_not_import_adapters` 證）；`overview` 讀 `data_plane_port`、`local_credentials.has_password` 改走 app service 方法。殼清理：對 25 支殼逐支 `grep -rn 'from core.<舊路徑>\\|core\\.<舊名>' --include='*.py' --include='*.sh' .`（含 `scripts/`、`deploy/`、`test/`），**零命中的才刪**，有命中的留著並在回寫列清單（P2 刪）；`FROZEN_FILES` 同步刪已刪的殼。對帳：`LEGACY_MODULE_OF` 不得再有 identity／sync 列；`ALLOWLIST` 不得有任何一端在 `guidant_agent/identity/` 或 `guidant_agent/sync/` 的邊（搬進模組的檔若還有跨模組 import 就是沒剪乾淨，回寫列出、停下）。`docs/ARCHITECTURE.md` §1 兩列、§4 白名單數字更新；Agent `CLAUDE.md`「程式放哪裡」加一句「identity／sync 已在新位置，改這兩塊的程式去 `guidant_agent/`」",
  edit=["core/console/routes.py、overview.py、heartbeat.py、local_auth_routes.py、log_routes.py", "25 支殼（零命中者刪）", "test/test_no_new_flat_core.py（FROZEN_FILES）", "test/test_module_boundaries.py（對帳）", "docs/ARCHITECTURE.md", "CLAUDE.md"],
  new=["test/test_p1_no_legacy_identity_sync.py（守衛：LEGACY_MODULE_OF 與 ALLOWLIST 不含 identity／sync）"],
  deps=["P1-4"], why="機械性收尾＋對帳，判斷點只有「殼能不能刪」用 grep 定",
  accept="新守衛綠；`pytest test/` 全綠；回寫列「刪了哪些殼、留了哪些殼與各自的命中處」；本機 Agent 連 DEV BE 跑完整一輪：註冊、心跳、主控台四種登入（主系統票、AD／LDAP、離線密碼、本機管理員）、檔案預覽下載、手動掃描一筆、網路除錯一筆、主系統直推測試連線（首腦照 FR-166 SPEC 手測清單驗）；42 條路由與 P0 時相同"),
]
