## 背景(来源:#23 UI 实测 K-3,调度员 2026-09-03 已核实成因,维护者拍板立单) 人力审核(仅持 `kpi_hr_reviewer` 岗位)对「领导审批中」的填报单点「审核通过」真的生效;部门填报人员在自己单上能看到「归档」「审核通过」「驳回」按钮。**成因**:四个流程按钮(提交填报 / 审核通过 / 驳回 / 归档)是带 `api.write` 能力的 JS 动作体,平台 17.2.0 以「受信任」方式执行动作体(不携带调用者 ExecutionContext,见运行时 `buildActionEngineFacade` 注释与 objectstack#2849),写入到达 `src/hooks/sheet.hook.ts` 时 `ctx.session.isSystem === true`,`hasPosition` 第一行 `if (isSystem(ctx) || !position) return true` 直接放行,「非系统写入不得直改 status」等其它以 `isSystem` 为闸的规则同样失效。经 REST 直接 PATCH `pending_action` 不受影响(hook 正常拦截)。 ## 目标 按钮发起的每一步流程动作都按**发起人的岗位**校验,与 REST 路径同一口径;按钮只对当前节点的岗位显示。 ## 范围 1. `src/hooks/sheet.hook.ts`(状态机唯一真值,本单允许改):岗位校验不再以 `isSystem(ctx)` 免检。规则:只要本次写入带有发起用户(`ctx.user` / `actorId(ctx)` 可解析)且写入的是 `pending_action`,就按发起人校验 `requiredPositionFor` 的岗位;`isSystem` 免检仅保留给**没有发起用户**的纯系统写入(发布生成填报单、重算、归档快照回写)。同一口径检查 `src/hooks/util.ts` 的 `hasPosition` 与其它 hook(bonus / adjustment / check-task / dispute)里以 `isSystem` 为闸的岗位校验,凡是「用户点按钮 → 动作体写入」能到达的分支一并修正;纯系统写入路径不能被误伤(发布、重算、种子、脚本仍要能跑)。 2. 按钮可见性:`src/actions/index.ts` 四个填报单动作的 `visibleWhen`(或平台等价机制)按「当前状态对应节点的审核岗位 = 当前用户岗位」收敛;归档只对人力审核显示。若平台的 `visibleWhen` 表达式拿不到当前用户岗位,如实记录并保留 hook 拦截为唯一防线,把平台能力缺口写进「需拍板事项」(只上报不修复)。 3. 单测:`test/` 中为 sheet.hook 补「非本节点岗位经动作体路径发起 approve/reject/archive 被拒」「本节点岗位放行」「系统写入(无发起用户)放行」三类用例;现有 90 个用例不得回退。 4. 文案:拒绝提示沿用现有三段式 `KPI_SHEET_POSITION` 文案。 ## 不做 不改流程节点顺序与驳回口径(第 10 章第 8 项);不改 `src/lib/scoring.ts`;不改平台包;不做 K-1(分公司填报人)、K-5(批量确认)。 ## 方案分级与放行(调度员) 改已有状态机 = 高风险。放行方向 = 上述范围 1 的 A 方案(按发起人校验、系统免检收窄),维护者 2026-09-03 已知悉并同意立单派发。开发子 agent 开工前仍须在本单评论输出「需求理解 + 实现方式 + 受影响写入路径清单(逐条标注 系统/用户)」,与放行方向一致即可直接开工;若发现必须改动 `pending_action` 机制本身或平台行为,停手升级。 ## 验收标准 1. 用岗位账号在**界面**点按钮:人力审核对「领导审批中」的单点「审核通过」被拒并显示岗位提示;部门填报人员对任何单点「审核通过 / 驳回 / 归档」被拒(若按钮已隐藏则以「不可见」为证据);分管领导对「领导审批中」的单审批通过正常;人力审核归档正常。 2. 同一组动作经 REST 直接 PATCH `pending_action` 的结果与界面一致。 3. 发布方案生成填报单、加减分/数据调整落地后的重算、归档快照回写等系统路径全部正常(`scripts/software-flow.mjs` 在空库软件档案实例上仍 54/54 PASS;`scripts/e2e-flow.mjs` 在默认档案上仍全 PASS)。 4. `pnpm verify` 绿,新增单测通过。 5. 测试报告(含岗位账号截图,成功与拒绝分支各有)与需求符合度清单挂本单评论,截图走 `acceptance-evidence` 40 位 SHA 图链。 ## 测试计划草稿 T1 单测三类用例 | T2 界面:人力审核越节点审批被拒 | T3 界面:部门填报人员看不到/点不动审核与归档 | T4 界面:分管领导正常审批、人力审核正常归档 | T5 REST 同口径 | T6 software-flow 54/54 + e2e-flow 全 PASS | T7 pnpm verify
Activity
baozhoutao commentedon Sep 3, 2026
认领(调度员占坑,2026-09-03)
local_0e65d2bc-719b-4314-aae2-351176a17e5f(同批派发:流程按钮绕过岗位校验:动作体以受信任身份写入被 hook 当系统写入免检,任何岗位可越节点审批/归档 #28 / 发布前完整性检查缺「每个参与主体须配分管领导」,未配置的主体流程死在领导审批节点 #29 / 数据范围:指标下达、参与主体、审核记录等配置/审计对象按主体过滤,菜单按岗位裁剪(各部门目标值互相保密) #30 并行)issue-28-position-guard,基线 main @a760651293af660a8a4a13f7ec30fb3983f9a868../kpi-issue-28-position-guard,dev 端口3113,数据库file:./.objectstack/issue-28-position-guard.dbbaozhoutao commentedon Sep 4, 2026
开工前方案说明(开发子 agent,2026-09-03)
一、需求理解
问题:四个流程按钮是带
api.write的 JS 动作体。平台 17.2.0 的动作体以「受信任」方式执行,写入到达 hook 时ctx.session.isSystem === true,于是hasPosition()第一行if (isSystem(ctx) || !position) return true无条件放行 —— 岗位闸对按钮路径整体失效;REST 直接 PATCH 走非系统上下文,岗位闸正常。结果是同一条规则在两条路径上两个口径。要达到的目标(《设计方案》第 9 章:流程按钮由系统按当前节点的审核岗位把关):按钮发起的每一步流程动作,按发起人的岗位校验,与 REST 路径同一口径;并让按钮只在「当前节点岗位 = 当前用户岗位」时显示。
关键事实(已核实平台源码,非推测):平台构造动作体上下文用的是
buildActionExecutionContext(ec) = { ...ec, isSystem: true }(@objectstack/runtime@17.2.0),发起人的userId被完整保留;ObjectQL.buildSession()把userId与isSystem一并透给 hook,buildUser()在userId == null时才返回 undefined。所以「有没有发起人」是可用的判别位,actorId(ctx)在按钮路径上能拿到真实发起人 —— 放行方向 A 可以落地,不需要改pending_action机制、不需要平台改动。二、实现方式
src/hooks/util.tsisSystemWrite(ctx)=isSystem(ctx) && actorId(ctx) === null——「纯系统写入」的唯一判据:系统标记 且 没有发起用户。hasPosition()的免检条件从isSystem(ctx)收窄为isSystemWrite(ctx)。一处改动,让 sheet / check-task / bonus / adjustment 四个 hook 的岗位闸在按钮路径上同时恢复。sysNoActor(ctx):在sudo()的基础上摘掉发起人(保留租户与事务信息),供 hook 内部自动推进的写入使用 —— 这类写入不是任何人点出来的,按发起人校验没有意义,必须走系统免检。src/hooks/sheet.hook.ts:岗位闸(第 73 行)因hasPosition修正自动生效;把「驳回时重置核对任务」改用sysNoActor(否则会被 check-task 收紧后的「已确认不能撤回」误伤);「非系统写入不得直改 status」收紧为按发起人判定;归档锁把pending_action/action_reason两个流程暂存字段排除在「改动」之外(after 阶段的清场写入),规则本身同步收紧。src/hooks/check-task.hook.ts:去掉isSystem早退(这正是「确认无误 / 提出争议」两个按钮绕过kpi_branch_checker的口子);全部确认后的自动推进改用sysNoActor写入。src/hooks/bonus.hook.ts/src/hooks/adjustment.hook.ts:归档锁、已批准锁两条isSystem闸同口径收紧(按钮可达);岗位闸由hasPosition修正覆盖。src/actions/index.ts:四个填报单动作的visible按「当前节点岗位 = 当前用户岗位」收敛,归档只对人力审核显示。前置待验:平台 record/list 动作的visible表达式作用域里是否绑定current_user(页面组件谓词绑定了,动作按钮这一条我还没在运行期证实);另外《设计方案》10.1 第 9 条明确「可改审核岗位」,节点岗位是按方案配置的,单靠record.status推不出所需岗位。这两点会在 dev 实例上实测后如实记录,拿不到就保留 hook 拦截为唯一防线并把能力缺口写进报告(只上报不修复)。test/补 sheet.hook 三类用例(越节点岗位经动作体路径被拒 / 本节点岗位放行 / 无发起用户的系统写入放行),现有 90 例不回退。KPI_SHEET_POSITION,不新增文案。三、受影响写入路径清单(逐条标 系统 / 用户)
ctx.session实况pending_actionisSystem=true+ 发起人userIdpending_actionisSystem=false+userIdpending_action='approve'isSystem=true+ 最后确认人userIdsysNoActor,免检(节点岗位可被方案改配,不能指望「最后确认人恰好持有该岗位」)pending_action/action_reasonisSystem=true+userIdupdated_by归属不变pendingisSystem=true+userIdsysNoActor(否则被收紧后的核对 hook 误伤)isSystem=true+ 发布人userIdplan.hook.ts本单禁触碰,且无任何按钮能到达填报单 insert;新建填报单保护维持isSystem免检createSnapshot→ 写kpi_snapshotisSystem=true+userIdisSystem免检;无按钮可达)regenerateResults→ 写kpi_resultisSystem=true+userIdwriteReview→ 写kpi_review_recordisSystem=true+userIdkpi_check_task.statusisSystem=true+ 发起人userIdisSystem早退,恢复kpi_branch_checker岗位闸kpi_bonus.statusisSystem=true+ 发起人userIdisSystem=true+ 发起人userIdkpi_entry_lineisSystem=true+ 审批人userIdscripts/*.mjs建链 / backfillisSystem=true,无userIdisSystem=true+ 发起人userId四、与放行方向的一致性
与调度员放行的范围 1 A 方案一致:按发起人校验、系统免检收窄到「没有发起用户」。没有改动
pending_action机制本身,没有改动平台行为,没有改流程节点顺序与驳回口径。按此开工。baozhoutao commentedon Sep 4, 2026
测试报告 —— #28 流程按钮岗位校验(开发自测)
结论:通过。 7 项测试计划(T1~T7)全部通过;新增 23 条单测,原有 90 条零回退;
scripts/software-flow.mjs54/54 PASS、scripts/e2e-flow.mjs73/73 PASS;pnpm verify绿。按钮路径与 REST 路径现在是同一口径:同一个账号、同一个动作,两条路径给出同一条三段式岗位提示。
一处如实记录:范围 2「按钮可见性收敛」未实现,原因是平台能力缺口(动作
visible表达式拿不到当前用户的业务岗位),已上报 objectstack-ai/objectstack#15136,hook 拦截作为唯一防线。详见下文 T2 与需求符合度清单。一、测试环境
issue-28-position-guard,基线 main @a760651OS_PORT=3113,OS_DATABASE_URL=file:./.objectstack/issue-28-position-guard.db,OS_SEED_PROFILE=software(空库重建)scripts/software-people.mjs建的岗位账号,口令统一;界面操作全部用岗位账号,不用管理员二、逐项结果
test/position-guard.test.ts,23 条;pnpm test113 passed(原 90 + 新 23)software-flow54/54 +e2e-flow全 PASSpnpm verify绿三、执行记录与证据
T2 人力审核(马丽,仅持「人力审核」岗位)对「领导审批中」的填报单点「审核通过」
操作:以
hr.reviewer@kpi.demo登录 → 打开「2026 年第 3 季度考核 · 销售部」(状态:领导审批中)→ 点「审核通过」→ 弹窗点「确认」。修改前的行为:同一操作会成功推进 —— 这正是本单要修的缺陷。
T3 部门填报人员(赵敏 / 李娜,仅持「部门填报人员」岗位)在自己的单上点审核类按钮
按钮仍然可见(可见性收敛受平台限制,见 §4),但点了拿不到结果 —— 拦截在 hook,数据零变化。
T4 正常分支
扩围验证(同因同域,一并修的分支)——加减分的岗位分离经按钮路径也恢复了:
修改前:这两个按钮同样以受信任身份写入,岗位分离在按钮路径上整体失效(REST 路径一直是好的)。
T5 REST 同口径(7/7 PASS)
以研发部填报单为对象,岗位账号经 REST 直接 PATCH:
界面与 REST 的提示文案、拒绝码逐字相同,验收标准 2 成立。
T6 系统路径回归
scripts/software-flow.mjs(空库软件档案):54/54 PASS。其中直接压到本次改动的两条系统路径:PASS T8f 3 家分公司全部确认后自动推进到人力审核中 — status=hr_reviewing(hook 内部自动推进走无发起人系统上下文,未被岗位闸误伤)PASS T9b 人力审核驳回 → 退回上一节点(分公司核对),核对任务重置为待核对 — 任务=pending,pending,pending(驳回重置核对任务同样走无发起人系统上下文,未被收紧后的「已确认不能撤回」误伤)T3 发布成功、T4a 发布生成 11 张填报单、T16a 归档快照生成、T14a/T14b 调整落地后重算—— 发布、快照、重算三条系统路径全部正常。scripts/e2e-flow.mjs(空库默认档案):73/73 PASS(该脚本当前用例数为 73)。T7 门禁
四、平台能力缺口(如实记录,只上报不修复)
范围 2「四个填报单动作的
visible按当前节点岗位收敛」未实现。实测经过(在本 worktree 的 dev 实例上做的探针实验,做完已把
src/actions/index.ts恢复原样,最终提交里不含探针):visible改成... && ('kpi_hr_reviewer' in current_user.positions)→ 按钮对所有人消失,包括持有该岗位的马丽;控制台无 CEL 告警。has(current_user.id)→ 按钮出现;换成has(current_user.positions)→ 按钮也出现。说明根与字段都绑上了,不是表达式写错。GET /api/v1/auth/me/permissions→positions: ["org_member","kpi_hr_reviewer","everyone"](服务端授权用的就是这一组);GET /api/v1/auth/get-session→user.positions: ["user","org_member"]—— Console 的current_user取自这里,业务岗位不在其中。结论:动作
visible里的current_user.positions装的是 auth 层角色,不是安全层岗位,按岗位收敛的谓词一律静默判假。已按支线 B 上报平台:objectstack-ai/objectstack#15136(含现象、最小复现、期望能力、平台版本)。本单不在平台仓提交修复。应用侧现状:四个按钮的可见性仍只按
record.status判定,拦截由 hook 承担(T2 / T3 的截图就是这条防线在工作)。另需注意:《设计方案》10.1 第 9 条明确「可增删节点、可改审核岗位」,节点审核岗位是按方案配置的,即使拿得到岗位,record.status也推不出所需岗位 —— 要做到配置忠实的可见性,还需要把「当前节点所需岗位」落到填报单上(见需求符合度清单的出口说明)。五、缺陷与遗留
自测过程中未发现本单引入的缺陷。以下为发现但本单不做的既有问题(不属于本单范围,记为遗留):
KpiError:前缀。按钮路径的错误经/api/v1/actions/...返回时,message被拼成KpiError: 操作失败:…,界面 toast 原样显示;REST 路径返回的是干净的操作失败:…。文案本身是合规的三段式,前缀是异常类名泄漏(std-copy「无异常原文」)。本单只改口径不改文案(范围 4),故未动;修法有两条:应用侧在动作体里 catch 后重抛纯消息,或上报平台统一错误呈现。baozhoutao commentedon Sep 4, 2026
需求符合度清单 —— #28
逐条对照工作项正文的「范围」「不做」「验收标准」。三态:✅ 完整实现 /⚠️ 有偏差 / ❌ 未实现。非 ✅ 条目均带出口链接。
范围 1 —— hook 岗位校验按发起人
src/hooks/sheet.hook.ts岗位校验不再以isSystem(ctx)免检isSystemWrite(ctx)(系统标记 且 无发起人),在hasPosition一处收口,填报单流程推进的岗位闸随之生效pending_action,就按发起人校验requiredPositionFor的岗位SheetTransitionHook的pending_action分支无条件走hasPosition(ctx, requiredPositionFor(...));发起人由actorId(ctx)解析(ctx.user.id→ctx.session.userId)isSystem免检仅保留给没有发起用户的纯系统写入(发布生成填报单、重算、归档快照回写)hasPosition)已完全按此收窄。但另有 4 处非岗位类的isSystem闸按原样保留:① 填报单新建保护 ② 填报单删除保护 ③ 填报明细提交后锁定 ④ 快照 / 审核记录 / 结果的不可变闸。理由逐条:发布生成填报单由plan.hook.ts以带发起人的系统上下文 insert,而plan.hook.ts本单禁触碰,收紧会直接打断发布;调整落地写明细、归档写快照、审批写结果同理,收紧会误伤。这四处没有任何按钮的动作体能到达(填报单无 insert/delete 型动作,明细与快照 / 结果无任何动作),REST 路径仍走非系统上下文照拦。差异已在代码注释逐处写明理由src/hooks/util.ts的hasPositionhasPosition首行由isSystem(ctx) || !position改为!position || isSystemWrite(ctx);新增isSystemWrite与sysNoActor两个具名口径,含成因注释isSystem为闸的岗位校验,凡「按钮 → 动作体写入」能到达的分支一并修正isSystem早退,「确认无误 / 提出争议」两个按钮恢复kpi_branch_checker岗位闸;bonus:归档锁与已批准锁收紧,岗位分离闸随hasPosition恢复(界面已实测,见测试报告 T4 扩围);adjustment:归档锁与已批准锁收紧,审批岗位闸随hasPosition恢复。dispute 现状就没有岗位闸,本单只修「失效的闸」,不新增规则(新增属需求变更)sysNoActor(核对全部完成后的自动推进、驳回时重置核对任务),落在免检侧;software-flow54/54、e2e-flow73/73 全 PASS,覆盖发布 / 重算 / 快照 / 调整落地 / 种子范围 2 —— 按钮可见性
visibleWhen按「当前状态对应节点的审核岗位 = 当前用户岗位」收敛visible的 CEL 里current_user.positions装的是 auth 层角色(["user","org_member"]),不含业务岗位;按岗位写的谓词对所有人静默判假,持有该岗位的人也看不到按钮。实测三步与两个接口对照见测试报告 §4。出口:已上报平台 → objectstack-ai/objectstack#15136(含现象、最小复现、期望能力、平台版本);按本工作项正文范围 2 的明文授权(「拿不到当前用户岗位…保留 hook 拦截为唯一防线」)保留 hook 拦截范围 3 —— 单测
test/position-guard.test.ts:人力审核越节点 approve、部门填报人员 approve / reject / archive,共 4 条,均断言KPI_SHEET_POSITIONapproved并盖approved_by;人力审核 approve →leader_approving;人力审核 archive →archived,共 3 条branch_checking → hr_reviewing;hasPosition与isSystemWrite的直接用例;核对任务重置放行,共 4 条pnpm test:7 个文件 113 条通过(原 90 + 新 23),零失败范围 4 —— 文案
KPI_SHEET_POSITION文案pnpm i18n:extract:check报 529/529 in sync「不做」项自查
src/lib/workflow.ts零改动src/lib/scoring.tsnode_modules零改动;平台问题只上报(objectstack#15136),未在平台仓开 PRplan.hook.ts/sharing-service.ts/security/index.ts/apps/index.ts/src/data//scripts//docs//README.md/CLAUDE.md)验收标准逐条
software-flow54/54;e2e-flow全 PASSpnpm verify绿,新增单测通过acceptance-evidence40 位 SHA 图链70ae4a0f9fd7e8ed3a4cba153ac54f819005ad32,已逐个gh api contents核对可达改动面
零元数据改动(对象 / 视图 / 动作 / 权限 / 共享 / 应用导航一律未动),因此无 i18n 词条变化。
baozhoutao commentedon Sep 4, 2026
交接验收侧
issue-28-position-guard,基线 main @a760651status:AI开发中→status:自测报告中→status:PR审查中acceptance-evidencecommit70ae4a0f9fd7e8ed3a4cba153ac54f819005ad32,目录issue-28/,10 张,已逐个gh api contents核对可达需要拍板的一件事:范围 2「按钮可见性按岗位收敛」因平台能力受限未实现(动作
visible的current_user.positions只带 auth 层角色,拿不到业务岗位),已上报 objectstack-ai/objectstack#15136;当前由 hook 拦截兜底,点了才报「你没有该岗位」。是否接受这个现状、还是另立单在填报单上落一个「当前节点所需岗位」字段来做可见性,请验收侧定。Merge pull request #33: workflow buttons validate the initiator's pos…
baozhoutao commentedon Sep 4, 2026
合并记录(调度员,2026-09-03)
cba7628修复(清场写入改无发起人系统路径 + 归档锁排除四个平台审计戳字段;替身重写并新增 6 条可证伪用例);复查轮「可合并」:连续归档 11/11、e2e 三轮 74/74、白名单混写探针全部被拒、回退抽验三种情形均按预期变红。KpiError:前缀、归档被拒提示节点名指向最后节点)与「已归档单仅含审计戳的 PATCH 变为 200 空操作」按记录级留痕,待应用侧缺陷批次一并排。status:PM验收中,处理人不变(验收侧)。