修正CEO报销财务兜底与部署护栏(07-28-ceo-finance-fallback)
ci-e2e / e2e (push) Canceled after 0s
ci-minimal / check-minimal (push) Canceled after 0s

这个提交包含在:
2026-07-28 13:42:48 +08:00
父节点 37b625c5dc
当前提交 35948729cf
共修改 31 个文件,包含 980 行新增和 74 行删除
+49 -4
查看文件
@@ -608,9 +608,10 @@ curl -fsS "http://127.0.0.1:${SERVER_HOST_PORT}/ready"
### 1. Scope / Trigger
- Trigger: changing Compose identity, host data mounts, database publication,
deployment entrypoints or operational timers.
deployment entrypoints, deployment-time seeds or operational timers.
- Purpose: ensure one approved Compose project owns one verified DB/storage pair
without fixed global container names or accidental shared PGDATA.
without fixed global container names, source synchronization touching runtime
data, or deployment seeds overwriting approved production rules.
### 2. Signatures
@@ -618,7 +619,10 @@ curl -fsS "http://127.0.0.1:${SERVER_HOST_PORT}/ready"
- PostgreSQL mount: `${POSTGRES_DATA_DIR:-./data/postgres}`.
- Application storage mount: `${STORAGE_HOST_DIR:-./data/storage}`.
- Formal deploy inputs: absolute `POSTGRES_DATA_DIR`, absolute
`STORAGE_HOST_DIR`, `DEPLOY_DATA_MODE=existing|initialize`.
`STORAGE_HOST_DIR`, absolute `REMOTE_DIR`, absolute `REMOTE_ENV_FILE`,
`DEPLOY_DATA_MODE=existing|initialize`.
- Deployment init includes `npm run seed:config`; approval-rule defaults are
inserted only when `approval_rules` is empty.
- Timer install inputs: `COMPOSE_PROJECT_NAME`, absolute
`POSTGRES_DATA_DIR`, absolute `STORAGE_HOST_DIR`, full 40–64 hex
`BUILD_SHA` and its exact 12-hex `VITE_BUILD_SHA` prefix.
@@ -637,6 +641,15 @@ curl -fsS "http://127.0.0.1:${SERVER_HOST_PORT}/ready"
- Relative defaults preserve local trialrun compatibility, but production must
use explicit absolute command inputs and verify them against active container
mounts before any start, backup or migration.
- `REMOTE_DIR` must not equal, contain or be contained by either production
data path. Validate this locally before provenance, SSH or `rsync --delete`.
If `REMOTE_ENV_FILE` is inside the synchronization tree, derive its actual
root-relative path and add explicit rsync protect/exclude filters; a fixed
default `.env` exclusion is insufficient.
- Deployment-time `seed:config` may create the three default approval rules for
an empty database. If any approval rule exists, preserve all existing rules;
production rule changes require their own approved transaction, drift checks
and audit rows.
- `existing` is the normal migration mode and refuses a missing PGDATA marker
or storage directory. `initialize` is only for an approved empty first start;
only that mode may create data directories.
@@ -673,6 +686,12 @@ curl -fsS "http://127.0.0.1:${SERVER_HOST_PORT}/ready"
valid absolute paths remain installable.
- Formal deploy omits paths/mode, uses a relative/broad/overlapping path, or an
`existing` PGDATA lacks `PG_VERSION` -> fail before DB start.
- `REMOTE_DIR` overlaps PostgreSQL or attachment storage in either direction ->
fail before provenance, SSH and rsync.
- A custom `REMOTE_ENV_FILE` lies inside the sync tree without dynamic
protect/exclude filters -> deployment contract fails.
- `approval_rules` contains one or more rows -> `seed:config` reports them as
preserved and performs no approval-rule create, update or delete.
- New project resolves the same PGDATA while the old DB runs -> stop before
Compose start; do not use service health as permission to continue.
- Old project name was inferred, immutable image ID or baseline counts are
@@ -682,8 +701,11 @@ curl -fsS "http://127.0.0.1:${SERVER_HOST_PORT}/ready"
- `node deploy/trialrun/validate-env-contract.js`.
- The isolated backup/restore drill mutates Compose with a DB port and fixed
name and proves the contract rejects it.
name, invokes formal deploy with each data path nested below `REMOTE_DIR`, and
proves every unsafe shape is rejected before network access.
- `bash -n` covers deploy, timer, backup and restore scripts.
- Seed unit tests assert: an empty table receives exactly three defaults; any
non-empty table receives zero approval-rule writes and no delete call.
- Timer contract assertions cover all four explicit inputs, path isolation,
PGDATA marker, SHA/prefix validation and the four systemd `Environment=`
entries. On the deployment host, assert timer enabled/active, daily service
@@ -693,6 +715,9 @@ curl -fsS "http://127.0.0.1:${SERVER_HOST_PORT}/ready"
- Good: one approved project resolves one stopped-and-migrated PGDATA and the
matching active storage path; deploy, timer and backup all use that identity.
- Good: source lives under one absolute root, data lives under separate absolute
roots, a custom env file is protected, and existing approval rules survive
deployment init byte-for-byte until an approved rule transaction runs.
- Good: the timer unit contains the current project, active mount sources and
deployed full/short SHA pair; Compose config and a non-mutating server exec
resolve under those exact values while the daily service stays inactive.
@@ -704,6 +729,9 @@ curl -fsS "http://127.0.0.1:${SERVER_HOST_PORT}/ready"
the old container name alone is a rollback point.
- Bad: reinstall the timer with only `COMPOSE_PROJECT_NAME`, allowing systemd
Compose interpolation to fall back to repository-relative data directories.
- Bad: place PGDATA or attachments below `REMOTE_DIR` and rely on one hard-coded
rsync exclusion, or delete/recreate configurable approval rules in a routine
deployment seed.
### 7. Wrong vs Correct
@@ -742,6 +770,23 @@ VITE_BUILD_SHA=<verified-12-char-prefix> \
bash deploy/trialrun/install-warch3-ops-timer.sh install
```
For source synchronization and approval-rule defaults:
```bash
# Wrong: runtime data sits below the --delete synchronization root, and seed
# replaces operator-managed rules during every deployment.
REMOTE_DIR=/srv/oa POSTGRES_DATA_DIR=/srv/oa/data/postgres \
bash deploy/trialrun/deploy.sh
# seedApprovalRules: deleteMany -> createMany
# Correct: data roots are disjoint; an in-tree env file is dynamically
# protected; seed creates defaults only for an empty approval_rules table.
REMOTE_DIR=/srv/oa-app POSTGRES_DATA_DIR=/srv/oa-data/postgres \
STORAGE_HOST_DIR=/srv/oa-data/storage DEPLOY_DATA_MODE=existing \
bash deploy/trialrun/deploy.sh
# seedApprovalRules: count > 0 -> preserve; count = 0 -> create defaults
```
## Scenario: Paired Backup And Shadow-Only Restore
### 1. Scope / Trigger
@@ -0,0 +1 @@
{"_example": "Fill with {\"file\": \"<path>\", \"reason\": \"<why>\"}. Put spec/research files only — no code paths. Run `python3 .trellis/scripts/get_context.py --mode packages` to list available specs. Delete this line once real entries are added."}
@@ -0,0 +1,63 @@
# CEO 本人报销财务兜底设计
## 1. Boundary
本任务复用现有审批规则和工作流快照机制,不新增角色、数据表、迁移或公开端点。`finance_mgr` 保持兼容;本公司的新默认和生产规则改为 `finance`。
## 2. Cross-layer Contract
```text
审批规则配置
ownerSelfFallbackRole: finance | finance_mgr | null
-> API 白名单校验
-> ApprovalRule 持久化
-> 报销提交加载规则
-> CEO 本人且 manager 无法解析时使用 fallback
-> 移除本人 ceo 节点 + 角色去重
-> 校验 active 非本人角色持有人
-> 冻结 workflow_steps_snapshot
```
创建 API、省略字段时的默认值和前端新建表单默认值统一为 `finance`。旧规则返回 `finance_mgr` 时前端仍可展示、编辑和保存,避免破坏兼容数据。
## 3. Target Rule Matrix
| 金额档 | 普通员工规则 | CEO 本人快照 |
| --- | --- | --- |
| 小额 | `manager -> finance` | `finance` |
| 中额 | `manager -> finance` | `finance` |
| 大额 | `manager -> ceo -> finance` | `finance` |
CEO 本人快照只由现有标准步骤完成:`manager` 缺失时插入 `finance` fallback;随后相同 `finance` 节点去重,申请人自身 `ceo` 节点移除。无需添加按金额或人员姓名硬编码的特殊分支。
## 4. Validation And Failure Behavior
- fallback 不在 `finance|finance_mgr`:`400 VALID_001`。
- CEO 同时持有 fallback:`409 VALID_001 / owner_self_fallback_self_role`。
- 无 active 非本人 fallback 持有人:`409 VALID_001 / owner_self_fallback_role_unavailable`。
- 非 CEO:不触发 owner-self fallback,保留正常规则快照。
- 生产规则数量、金额区间或当前角色结构漂移:回填事务停止,零写入。
## 5. Production Rollout
1. 部署代码与前端,使 runtime 能读写 `finance` fallback;`seed:config` 对既有审批规则只保留不覆盖,避免部署提前改写生产规则。
2. 核对公网构建号和 runtime smoke。
3. 生产只读冻结 3 条规则、active `finance` 持有人、CEO 身份、角色/binding/历史快照计数。
4. 单事务更新 3 条启用规则:中/大额 `finance_mgr` 节点改为 `finance`,全部 fallback 改为 `finance`;逐条写脱敏审计。
5. 只读解析三档 CEO 快照和普通员工快照,不创建真实报销。
## 6. Rollback
- 代码兼容 `finance_mgr`,生产规则可在精确冻结原值后原子恢复。
- 回滚规则只影响回滚后新提交单据;既有 `workflow_steps_snapshot` 保持不变。
- 任一生产检查失败时先回滚规则,不改员工角色或历史单据。
## 6.1 Seed Safety
- 空库首次初始化时创建三条新默认规则。
- 只要审批规则表已有任意记录,`seed:config` 保留全部既有规则,不删除、不覆盖、不补写;生产规则只允许走获批的独立原子事务。
- 部署日志只记录创建/保留计数,不输出规则 ID、人员或业务内容。
## 7. Trade-off
不采用“给现有财务追加 `finance_mgr`”方案,因为该角色同时包含主数据、预算、集成、AI、资产和薪酬等更宽权限,超出“审批 CEO 本人报销”的最小职责。允许 `finance` 作为兜底能复用已有 `expense.approve`,权限面更小。
@@ -0,0 +1 @@
{"_example": "Fill with {\"file\": \"<path>\", \"reason\": \"<why>\"}. Put spec/research files only — no code paths. Run `python3 .trellis/scripts/get_context.py --mode packages` to list available specs. Delete this line once real entries are added."}
@@ -0,0 +1,45 @@
# CEO 本人报销财务兜底实施计划
## 1. Implementation
- [x] 扩展后端 owner-self fallback 白名单为 `finance|finance_mgr`,创建默认值改为 `finance`。
- [x] 将三档种子默认规则统一为财务 `finance` 收口,保留大额 `ceo` 节点。
- [x] 更新前端 API 类型、创建/编辑选项、默认值和推荐文案;兼容展示 `finance_mgr`。
- [x] 更新工作流、规则 API、报销提交回归测试,覆盖 CEO 三档唯一 `finance`、普通员工链、自审、无持有人和 legacy `finance_mgr`。
- [x] 修正 `seed:config` 对既有审批规则的删除重建副作用;仅空库创建默认规则,生产既有规则保留到独立原子回填阶段。
- [x] 新增默认 dry-run、双确认 apply 的生产规则回填脚本;事务内锁定并校验三档规则、CEO/财务/admin/binding 基线,更新三条规则、写三条脱敏审计并复核受保护计数。
- [x] 更新当前业务事实源,区分目标、代码部署和生产规则回填状态。
## 2. Local Validation
- [x] `cd server && node --check src/utils/workflow.js && node --check src/routes/approvalRules.js && node --check src/scripts/seed-config.js && node --check src/scripts/apply-ceo-finance-fallback.js`
- [x] `cd server && npx vitest run tests/unit/utils/workflow.test.js tests/unit/routes/approvalRules.permissions.test.js tests/unit/routes/reimbursements.expense-standards.test.js tests/unit/scripts/seed-config.defaults.test.js tests/unit/scripts/apply-ceo-finance-fallback.test.js`(58 项通过)
- [x] `cd server && npm run prisma:validate`
- [x] `cd client && npm run typecheck && npm run build`
- [x] `node scripts/qa/check-process-contract.mjs --baseline --json`
- [x] `node scripts/qa/audit-docs.mjs --check-registry`(0 error,保留既有 22 个未注册文档 warning)
- [x] `python3 ./.trellis/scripts/task.py validate 07-28-ceo-finance-fallback`
- [x] `node scripts/qa/check-trellis-governance.mjs && git diff --check`
- [x] `bash scripts/check-minimal.sh`(167 个测试文件、1053 项后端测试、前端 build 与矩阵门禁通过)
## 3. Review Gate
- [x] 只读 review 权限白名单、自审阻断、普通员工链、历史快照不回写、API/UI 类型一致性和生产事务边界;发现并修复部署 seed 提前覆盖规则问题后复核无剩余阻断。
## 4. Deployment And Production Apply
- [ ] 提交并推送已验证代码。
- [x] 修复部署脚本已知的数据目录保护缺陷:同步根目录与数据目录任一方向重叠时在 SSH 前阻断,自定义同步树内 `.env` 动态保护;隔离备份/恢复合同通过。
- [ ] 部署后核对 `/ready` 构建号、migration 状态、runtime smoke、external deps smoke 和前端 bundle。
- [ ] 生产只读冻结规则结构、active `finance`、CEO、角色、binding、历史快照和审计基线。
- [ ] 原子更新恰好 3 条启用规则并逐条写脱敏审计;任一预条件漂移则零写入停止。
- [ ] 只读解析 CEO 小/中/大额快照均为唯一 `finance`,普通员工链符合目标;不创建真实报销。
- [ ] 复核无角色、主管、binding、历史快照或企微写入变化。
## 5. Stop Conditions
- 生产 active `finance` 不是唯一非 CEO 持有人,或目标人员 inactive/unbound。
- 启用规则不是恰好 3 条,金额区间或角色结构与冻结基线不符。
- 部署路径仍可能删除 `.env`、附件或数据库宿主目录。
- 任一测试出现自审、管理员代批、历史快照重写或普通员工链异常。
- 真实 CEO 报销验证未取得单独批准。
@@ -0,0 +1,69 @@
# CEO 本人报销财务兜底修正
## Goal
让 `majianyong(ceo)` 本人提交任一金额档报销时,在禁止自审的前提下只进入 active 非本人 `finance` 审批;`wangshunli` 的系统管理员身份不进入业务审批链,也不通过给现有财务附加 `finance_mgr` 扩权来绕过问题。
## Scope
- 报销审批工作流的负责人本人 fallback 白名单、快照解析和失败关闭行为。
- 审批规则 API、前端配置页与三档默认种子规则的 `finance` 默认值和 `finance_mgr` 兼容。
- 部署同步根目录与 PostgreSQL、附件和环境文件的安全隔离合同。
- 经部署与公网验证后,对生产恰好 3 条启用报销规则进行一次原子回填和脱敏审计。
## Background
- 用户确认 `majianyong` 是公司 CEO、无直属主管;`wangshunli` 仅承担系统开发和系统管理。
- 生产有 3 条启用报销金额规则,均使用 `owner_self_fallback_role=finance_mgr`,中/大额规则还包含 `finance_mgr` 节点。
- 生产有 1 名 active 且已绑定企微的 `finance`,没有 active `finance_mgr`;当前 CEO 本人报销会以 `owner_self_fallback_role_unavailable` 失败关闭。
- 代码、审批规则 API、前端配置页和种子默认值目前只把 `finance_mgr` 作为负责人本人兜底角色。
- 现有工作流已具备 CEO 自审节点移除、重复角色去重、active 非本人持有人检查和历史审批快照冻结能力。
## Requirements
- `finance` 和兼容保留的 `finance_mgr` 均可作为 `ownerSelfFallbackRole`;新建规则默认使用 `finance`。
- 小团队默认金额规则调整为:小额 `manager -> finance`、中额 `manager -> finance`、大额 `manager -> ceo -> finance`,三档 `ownerSelfFallbackRole` 均为 `finance`。
- CEO 本人申请在三档规则下均生成唯一 `finance` 节点:替换无法解析的 `manager`、移除本人 `ceo`、去除重复 `finance`。
- `finance` 兜底必须存在 active 非本人持有人;CEO 若同时持有 `finance` 或无可用持有人,继续失败关闭。
- 普通员工的工作流仍按规则顺序生成,不引入 CEO 本人专属的全局跳步;历史 `workflow_steps_snapshot` 不回写。
- 审批规则 API 和前端配置页同时支持 `finance`/`finance_mgr`,默认与推荐值显示为 `finance`;不开放 `admin`、`system_admin`、`hr` 或其他角色作为负责人本人兜底。
- 部署后以原子事务将生产 3 条启用规则从现状修正为上述三档结构并写脱敏审计;写入前必须重新确认规则数量、金额区间、角色结构、active 财务持有人和 CEO 身份,任一漂移即停止。
- 生产回填不改员工角色、直属主管、既有报销审批快照、密码、企微 binding 或企微通讯录。
- 公网验证只创建一条由 CEO 本人发起的低风险测试报销时,必须再次取得真实业务写入批准;本任务默认只做规则只读解析/smoke,不自动创建真实报销。
## Acceptance Criteria
- [x] 后端工作流单元测试证明 CEO 本人小/中/大额快照均只有一个 `finance` 节点。
- [x] 无 active 非本人 `finance`、CEO 同时持有 `finance`、不支持的兜底角色均失败关闭且不创建半截工作流。
- [x] 普通员工规则快照保持 `manager -> finance` 或 `manager -> ceo -> finance`,无自审放宽。
- [x] 审批规则 API 接受 `finance` 和兼容的 `finance_mgr`,拒绝非白名单角色;新建默认值为 `finance`。
- [x] 前端类型、创建/编辑选项和推荐文案支持 `finance`,默认选择 `finance`,typecheck/build 通过。
- [x] 种子默认三档规则与目标链一致,相关回归测试通过。
- [x] 只读 review 确认权限、自审、状态机、历史快照和生产回填边界无扩大。
- [ ] 部署后公网 `/ready` 构建号匹配,runtime smoke 和关联认证/工作流检查通过。
- [ ] 生产恰好 3 条启用规则按目标结构回填并留下脱敏审计;active 账号、角色、binding 和历史审批快照计数无意外变化。
- [x] 当前事实源、管理员手册和任务结论不再宣称 `finance_mgr` 是本公司推荐兜底。
## Test Plan
- 后端语法、Prisma validate,以及 workflow、审批规则 API、报销提交和默认种子规则聚焦测试。
- 前端 typecheck/build 与审批规则配置页聚焦 Playwright。
- 部署环境静态合同、隔离备份恢复演练、流程合同、文档审计、Trellis task/governance 和敏感内容扫描。
- 部署后核对 `/ready` 构建号、migration、runtime/external-deps smoke 和公网 bundle。
- 生产回填前后只读冻结规则、角色、binding、历史快照和审计计数;只读解析 CEO 三档与普通员工链,不创建真实报销。
## Out of Scope
- 不删除 `finance_mgr` 角色或改变其现有权限。
- 不给现有财务人员新增 `finance_mgr`、`manager` 或其他角色。
- 不改变普通报销金额阈值,不重构通用工作流引擎,不修改历史审批快照。
- 不让 `admin`/`system_admin` 获得业务审批权,不允许任何申请人自审。
- 不执行真实付款、企微写入或未经再次批准的真实 CEO 报销提交。
## Technical Notes
- 后端白名单与快照:`server/src/utils/workflow.js`。
- 审批规则 API:`server/src/routes/approvalRules.js`。
- 默认规则:`server/src/scripts/seed-config.js`。
- 前端类型和页面:`client/src/api/approvalRules.ts`、`client/src/pages/ApprovalRulesPage.tsx`。
- 聚焦测试:`server/tests/unit/utils/workflow.test.js`、`server/tests/unit/routes/approvalRules.permissions.test.js`、`server/tests/unit/routes/reimbursements.expense-standards.test.js`。
@@ -0,0 +1,26 @@
{
"id": "ceo-finance-fallback",
"name": "ceo-finance-fallback",
"title": "CEO 本人报销财务兜底修正",
"description": "让 CEO 本人报销在禁止自审的前提下由 active 非本人 finance 审批,并以最小权限修正代码、默认规则和生产配置。",
"status": "in_progress",
"dev_type": null,
"scope": "server, client, deploy/trialrun, workflow tests, current docs, production approval rules",
"package": null,
"priority": "P2",
"creator": "shunli",
"assignee": "shunli",
"createdAt": "2026-07-28",
"completedAt": null,
"branch": null,
"base_branch": "main",
"worktree_path": null,
"commit": null,
"pr_url": null,
"subtasks": [],
"children": [],
"parent": null,
"relatedFiles": [],
"notes": "",
"meta": {}
}