# EAI 组织架构一次性 seed

把 demo（`/.reference/org-dark-grid-demo/`）里的 21 个 T1 部门 + 46 T2 + 21 T3 + ~84
成员引用导入到 `corp_hr.organizations` / `corp_hr.departments` / `corp_hr.user_departments`。

设计成**一次性、可重入、幂等**：用同一脚本在 dev、UAT、prod 各跑一次即可。

---

## 文件

| 文件 | 作用 |
| --- | --- |
| [`eai-orgchart-data.json`](./eai-orgchart-data.json) | 从 demo HTML 抽出来的部门树结构（含 code/name/leader/cat/t2/t3/members/scope）|
| [`eai-orgchart-aliases.json`](./eai-orgchart-aliases.json) | 手工别名表：demo 名字 → 用户 email。`--scan` 报"未匹配"时往这加 |
| [`eai-orgchart-seed.ts`](./eai-orgchart-seed.ts) | 执行脚本 |
| [`tsconfig.json`](./tsconfig.json) | TS 配置（继承 `backend/tsconfig.json`） |

数据与脚本分离的好处：审查 PR 时**只看 JSON 是否对**就够了，不用读代码。

---

## 模式

```bash
# 在仓库根目录运行
cd backend

# 1. 仅扫描（默认）—— 只读，输出 plan 摘要 + 未匹配清单
npm run seed:eai-orgchart -- --scan

# 2. 完整 dry-run —— 模拟全流程，不写库
npm run seed:eai-orgchart -- --dry-run

# 3. 真实执行 —— 写库
npm run seed:eai-orgchart -- --apply

# 4. 真实执行 + 清空旧数据 —— **仅 dev**
npm run seed:eai-orgchart -- --apply --reset-dev

# 5. 强制重跑（即便已 seed）—— 谨慎，会 upsert 覆盖
npm run seed:eai-orgchart -- --apply --force
```

---

## 安全边界

| 行为 | dev | UAT | prod |
| --- | :-: | :-: | :-: |
| `--scan` | ✅ | ✅ | ✅ |
| `--dry-run` | ✅ | ✅ | ✅ |
| `--apply`（首次）| ✅ | ✅ | ✅ |
| `--apply --reset-dev` | ✅ | ❌（脚本检查 `NODE_ENV=production` 强制禁用）| ❌ |
| `--apply --force` | ✅ | ✅ | ✅（不推荐）|

**幂等性**：所有 ID 由 `sha1(seedVersion + 'dept' + code)` 推导（v5 UUID 风格）。
重复跑 `--apply` 不会复制，只会 upsert 已有记录。

**已 seed 检测**：脚本启动会检查 `organization.metadata.seededBy === 'eai-orgchart-v1'`。
已 seed 则 abort（除非 `--force` 或 `--reset-dev`）。

---

## 人名匹配策略（从严到宽，第一个命中即用）

1. **别名表精确匹配**（`eai-orgchart-aliases.json` 里的 demo-name → email）
2. **`displayName` 精确等于** （demo 名字 strip 括号后小写 normalize，跟 `User.displayName` 比对）
3. **Last-First 倒置**（`Johnson, Larry E.` → `Larry Johnson`，drop 中间名 initial）
4. **占位符**（`TBD` / `TBH` / `—` / `Various` / `None` / `n/a`）→ 直接 skip
5. **role 检测**（`Skilled Product Manager` / `VP Accounting` / `Foundation Model Manager` 等）→ skip
6. 都没命中 → 报告里列出，`head_id=null`，`metadata.unresolvedLead` 存原始字符串

**多人共主管**（`A / B / C`）：按 issue 决策，`head_id=null` + `metadata.coLeaders=[{name, userId, acting}]`。

---

## 上 prod 的标准流程

铁律：**绝不在本地从 AI session 直接连 prod 执行**。

1. 本机：`feature/eai-orgchart-seed` 分支 → push → PR → review
2. 在 dev `--scan` 没新 miss → `--dry-run` plan 数字符合预期 → `--apply --reset-dev` 落到 dev DB → MCP 验证页面
3. 合并 develop → CI 把 develop 合到 test 环境的 deploy 分支 → SSH 上 test 服务器
4. test 服务器：
   ```bash
   cd /path/to/workspace
   git pull
   cd backend
   npm run seed:eai-orgchart -- --scan          # 检查 prod-like users 表对得上
   npm run seed:eai-orgchart -- --dry-run
   npm run seed:eai-orgchart -- --apply         # 真跑
   ```
5. 在 test 验证 UI + 数据库
6. 走部署上 prod（按 `.agents/skills/deploy-ops`）
7. prod 服务器（**手动 SSH 进去，不通过 AI**）：
   ```bash
   ssh <prod-host>
   cd /path/to/workspace
   git log --oneline -3                          # 确认代码到位
   cd backend
   NODE_ENV=production npm run seed:eai-orgchart -- --scan
   NODE_ENV=production npm run seed:eai-orgchart -- --dry-run
   NODE_ENV=production npm run seed:eai-orgchart -- --apply
   ```
8. 在生产前端验证页面（[/organization/structure/grid](/organization/structure/grid)）

---

## --scan 输出范例

```
🌱 EAI Orgchart Seed — mode=scan, reset-dev=false, NODE_ENV=development
   Demo data: 21 T1 departments
   Users in DB: 378

📋 Plan summary:
   Organization: EAI Robotics (<deterministic uuid>)
   Departments: 88
   User-Dept memberships: 59
   Unmatched leaders: 1
   Unmatched members: 3

⚠️  Unmatched LEADER names (head_id will be NULL; metadata.unresolvedLead set):
   - "Xu Huo (CN)" → appears in: Supply Chain / SQE

⚠️  Unmatched MEMBER names (skipped from user_departments):
   - "Edward Tian" → appears in: EAI Brain / Cloud
   - "Hank Wang" → appears in: UES / User Acquisition / ...
   - "Josh / Julian S" → appears in: Product & Design / Design
```

未匹配的人**不要 panic** —— 都已经记到 metadata 里。三种处理：
- 真有这个人但邮箱不一样 → 在 `eai-orgchart-aliases.json` 加 `"DemoName": "email@ff.com"`，重跑 scan
- 真没这个人（系统里就是没建账号）→ 留 null，等账号建好后人工 set head/membership
- 数据本身有问题（如 `Josh / Julian S` 那种 demo 写法奇怪）→ 改 `eai-orgchart-data.json` 把它拆成两个 member 或干脆删掉

---

## 重跑、回滚

- **重跑（覆盖式）**：`npm run seed:eai-orgchart -- --apply --force` —— upsert 所有记录
- **回滚 dev**：`npm run seed:eai-orgchart -- --apply --reset-dev` —— 删了再插
- **回滚 prod**：脚本不提供。生产数据安全起见，需要回滚则手动写 reverse SQL，PR 走部署链路

---

## demo 与真数据的差异（已知）

- demo 把"职位"字符串当 leader（`'Jevy Luo / VP Accounting'`），脚本 role 检测会跳过职位字符串
- demo 部分 member 是纯职位（`'Skilled Product Manager'`），脚本 skip
- demo 用 `O0/O1/Q7` + `1.1/3.1.5` 这种顺序编号；脚本以 demo 的 `code` 作为 `Department.code`
  入库（与前端"展示编号"分离，前端按位置生成 `O{i}` / `i.j` / `i.j.k`，不依赖 DB code）
- 多人共主管的部门 `head_id=null`，但 UI 仍可从 `metadata.coLeaders` 渲染多个名字
