merge: fix/document-revision-duty-cell-assertion

This commit is contained in:
weichen
2026-09-13 21:29:26 +00:00
11 changed files with 378 additions and 12 deletions
@@ -0,0 +1,48 @@
---
title: "UI-13a 用整行 option 文案误判编制负责人"
status: in_progress
created: 2026-09-13
updated: 2026-09-13
severity: high
---
# UI-13a 用整行 option 文案误判编制负责人
## 现象
第16轮真实 UI(schema 30,`dev@dd5671e`):UI-00 至 UI-12 PASS;分类导航修复已实证,UI-13a 已能从「我的私密」切回「流程受控」并打开 reviewer B 权限矩阵。
失败在职责断言:`intakeRow.innerText` 被整行匹配,报 `reviewer B must not be 编制负责人`。实际 `innerText` 为:
`V0.5 用户初始需求收集表当前打开 状态: drafting ✍️ 编制负责人 🔍 审核负责人 👑 批准负责人 👁️ 查阅者 (只读) 前往`
## 根因分析
| 层 | 问题 | 后果 |
|---|---|---|
| 职责单元格 | `onRoleChangeForDoc` 存在时渲染 `<select>`,四个 option 文案都在 DOM | `element.innerText` 含全部 option |
| 验收 | `/编制负责人/.test(整行文本)` | 审核人行也被判成编制负责人 |
| 控件语义 | select/badge 无稳定 `data-testid` / `data-doc-role` | 脚本只能刮行文案 |
select 的 `value` 才是当前职责。USER_B 在 UI-01 被明确分配 reviewer:验收必须证明 `role=reviewer`,不得把职责同步丢失降级成的 `viewer` 判通过。select 还须 `data-can-assign=false` 且 `disabled`;`canAssignLead=true` 一律拒绝。
## 严重度
`high`
权限矩阵已打开,真实只读检查未跑到就被假阳性打断。
## 关联整改
- Remediation: [../remediation/2026-09-13-document-revision-duty-cell-assertion.md](../remediation/2026-09-13-document-revision-duty-cell-assertion.md)
- Short-term: [../ops/short-term/2026-09-13-document-revision-duty-cell-assertion.md](../ops/short-term/2026-09-13-document-revision-duty-cell-assertion.md)
- 分支: `fix/document-revision-duty-cell-assertion`
## 证据
- 失败证据(只读):`.doc/test-reports/assets/2026-09-13-document-revision-runtime-ui/schema30-ui13a-duty-cell-assertion-failure/`
## 开放问题
- 本轮不重跑真实 UI-13a;不得标 E2E 通过或任务 `done`
- 独立审查:不得用 `reviewer|viewer` 宽判;`evaluateReviewerDuty` 只接受精确 reviewer
+8
View File
@@ -31,6 +31,14 @@
### 最新专项审计(2026-09-13)
**文件**: [2026-09-13-document-revision-duty-cell-assertion.md](./2026-09-13-document-revision-duty-cell-assertion.md)
**结论**: 第16轮 UI-13a 已打开 reviewer B 权限矩阵;整行 `innerText` 含全部 select option,误命中「编制负责人」。须读 select value / badge role。
**整改**: [职责单元格精确断言整改](../remediation/2026-09-13-document-revision-duty-cell-assertion.md)
### 受控文档分类导航(2026-09-13)
**文件**: [2026-09-13-document-revision-workflow-category-navigation.md](./2026-09-13-document-revision-workflow-category-navigation.md)
**结论**: 第15轮 UI-00–12 PASS;UI-13a 在私密分类下直接展开文档菜单,找不到 `doc_proj*_intake`。须先用页面控件切到流程受控。
+4 -2
View File
@@ -1,11 +1,11 @@
# 当前焦点
**更新**: 2026-09-13
**production_ready**: no(DOCREV:schema **30**;第15轮 UI-13a 分类导航契约;不执行 env-service 重建 / 不声称 E2E 通过)
**production_ready**: no(DOCREV:schema **30**;第16轮 UI-13a 职责单元格断言;不执行 env-service 重建 / 不声称 E2E 通过)
## 焦点
1. **DOCREV-RT**: [受控文档分类导航契约](short-term/2026-09-13-document-revision-workflow-category-navigation.md)(`fix/document-revision-workflow-category-navigation`;第15轮 UI-13a 停在私密分类找不到 intake;不声称 E2E 通过)
1. **DOCREV-RT**: [职责单元格精确断言](short-term/2026-09-13-document-revision-duty-cell-assertion.md)(`fix/document-revision-duty-cell-assertion`;第16轮整行 option 文案误判编制负责人;不声称 E2E 通过)
2. **DOCREV-RT**: [质量修复子工作区契约](short-term/2026-09-13-document-revision-quality-repair-workspace-contract.md)(已合入 `dev@339e871`;仍 `in_progress`,待占槽重跑 UI-03)
3. **DOCREV-RT**: [编辑弹窗 hydration 竞态](short-term/2026-09-13-document-revision-editor-hydration-race.md)(已合入 `dev@8b1a531`;仍 `in_progress`,待占槽重跑 UI-06)
@@ -32,6 +32,7 @@
| Cursor-Auto | document-revision-quality-repair-workspace-contract | fix/document-revision-quality-repair-workspace-contract | - | document-revision-quality-repair-workspace-contract | in_progress |
| Cursor-Auto | document-revision-fast-completion-observation | fix/document-revision-fast-completion-observation | - | document-revision-fast-completion-observation | in_progress |
| Cursor-Auto | document-revision-workflow-category-navigation | fix/document-revision-workflow-category-navigation | - | document-revision-workflow-category-navigation | in_progress |
| Cursor-Auto | document-revision-duty-cell-assertion | fix/document-revision-duty-cell-assertion | - | document-revision-duty-cell-assertion | in_progress |
> 新任务开始前登记。`branch` 必须为 `feature/<slug>` 或 `fix/<slug>`,**禁止** `dev` / `test` / `main`。同一 `mutex` 同时仅一个 `in_progress`。
@@ -81,3 +82,4 @@
| UI-03 质量修复子工作区契约审计 | [../audit/2026-09-13-document-revision-quality-repair-workspace-contract.md](../audit/2026-09-13-document-revision-quality-repair-workspace-contract.md) |
| UI-11 快速完成观测误判审计 | [../audit/2026-09-13-document-revision-fast-completion-observation.md](../audit/2026-09-13-document-revision-fast-completion-observation.md) |
| UI-13a 私密分类导航失败审计 | [../audit/2026-09-13-document-revision-workflow-category-navigation.md](../audit/2026-09-13-document-revision-workflow-category-navigation.md) |
| UI-13a 职责单元格误判审计 | [../audit/2026-09-13-document-revision-duty-cell-assertion.md](../audit/2026-09-13-document-revision-duty-cell-assertion.md) |
@@ -0,0 +1,55 @@
---
title: "职责单元格精确断言"
status: in_progress
created: 2026-09-13
updated: 2026-09-13
owner: Cursor-Auto
branch: fix/document-revision-duty-cell-assertion
worktree: "-"
mutex: document-revision-duty-cell-assertion
---
# 职责单元格精确断言
## 目标
堵住第16轮 UI-13a:权限矩阵已打开,整行 `innerText` 含全部 select option,误命中「编制负责人」。改为读控件 value/badge,且 USER_B 必须精确 `reviewer`,不得接受 viewer 降级。
## 范围
- `web/src/lib/dutyCellAssertion.ts` 与单测
- `UserProjectRightsModal` 职责控件可访问语义
- `web/e2e/document-revision-runtime.spec.ts` UI-13a
- audit / remediation / CURRENT
## 非目标
- 不放宽 reviewer 只读;不删双击/无编辑提示检查
- 不碰证据;不合 dest;不启停容器;不 E2E;不 push
## 步骤
1. [x] 从 `dev@dd5671e` 开分支并登记 mutex
2. [x] 精确角色解析 + option 文案不得误判测试
3. [x] 产品控件 data-* + UI-13a 接线
4. [x] `npm test` + `npm run build` 并原子提交
5. [x] 独立审查收紧:只接受 reviewer;viewer 与 canAssignLead=true 拒绝
## 验收标准
- [x] select/badge 必须精确 `reviewer`;viewer/author/approver 拒绝
- [x] 整行 option 文案不得推断职责
- [x] select 必须不可配置且 disabled
- [ ] 现场 UI-13a(未跑)
## 相关链接
- Audit: [../../audit/2026-09-13-document-revision-duty-cell-assertion.md](../../audit/2026-09-13-document-revision-duty-cell-assertion.md)
- Remediation: [../../remediation/2026-09-13-document-revision-duty-cell-assertion.md](../../remediation/2026-09-13-document-revision-duty-cell-assertion.md)
## 证据 / 备注
- mutex:`document-revision-duty-cell-assertion`(新 mutex)
- 基线:`dev@dd5671e`
- 验证(收紧后):`npx tsx --test src/lib/dutyCellAssertion.test.ts` PASS(6);`cd web && npm test` PASS;`npm run build` PASS
- **未跑真实 UI-13a,不得标 done**;**不得 merge / 启停容器 / E2E / push**
@@ -0,0 +1,45 @@
---
title: "职责单元格按 value/badge 精确断言"
status: in_progress
created: 2026-09-13
updated: 2026-09-13
---
# 职责单元格按 value/badge 精确断言
## 背景
链到 [../audit/2026-09-13-document-revision-duty-cell-assertion.md](../audit/2026-09-13-document-revision-duty-cell-assertion.md)。
## 目标
- 精确定位该文档行的职责控件
- select:`value` 必须是 `reviewer`,且 `data-can-assign=false`、`disabled`;`canAssignLead=true` 拒绝
- badge:`data-doc-role` 必须是 `reviewer`;`viewer` / `author` / `approver` / 未知值拒绝
- 保留无「双击任意段落就地编辑」、双击不出现编辑器
## 分波次计划
| 波次 | 内容 | 验收 |
|------|------|------|
| Wave 0 | 控件 `data-testid`/`data-doc-id`/`data-doc-role` + 精确 reviewer 谓词 + UI-13a | option 文案不得误判;viewer/可配置 select 拒绝;`npm test`/`build` |
| Wave 1 | 占槽重跑真实 UI-13a(本轮不做) | 矩阵断言过,只读检查仍跑 |
## 设计
1. `resolveDutyCell`:只接受 `select.value` 或 `badge.role`;`row-text` 一律 reject。
2. `evaluateExpectedDuty(input, expected)` / `evaluateReviewerDuty`:UI-13a 只接受 `reviewer`。
3. 产品 select/badge/行补 `data-testid`、`data-doc-id`、`data-doc-role`、`data-can-assign`。
4. UI-13a 按 docId 定位控件,不再 `intakeRow.innerText`。
## 验收标准
- [x] 整行含全部 option 时不得推断职责
- [x] select/badge 必须 `reviewer`;viewer/author/approver/未知拒绝
- [x] select 必须 `canAssignLead=false` 且 disabled;`canAssignLead=true` 拒绝
- [ ] 真实 UI-13a(本轮不做)
## 相关链接
- Audit / Short-term:同 slug `2026-09-13-document-revision-duty-cell-assertion`
- 证据(只读):`.doc/test-reports/assets/2026-09-13-document-revision-runtime-ui/schema30-ui13a-duty-cell-assertion-failure/`
+1
View File
@@ -3,6 +3,7 @@
| 文档 | 说明 |
|------|------|
| [gap-matrix.md](./gap-matrix.md) | 发布缺口 SSOT |
| [2026-09-13-document-revision-duty-cell-assertion.md](./2026-09-13-document-revision-duty-cell-assertion.md) | UI-13a 整行 option 文案误判编制负责人,须读 select value/badge |
| [2026-09-13-document-revision-workflow-category-navigation.md](./2026-09-13-document-revision-workflow-category-navigation.md) | UI-13a 停在私密分类,打开受控文档前必须先切流程受控 |
| [2026-09-13-document-revision-fast-completion-observation.md](./2026-09-13-document-revision-fast-completion-observation.md) | UI-11 快完成被硬等 inflight 误杀,只改验收观测契约 |
| [2026-09-13-document-revision-quality-repair-workspace-contract.md](./2026-09-13-document-revision-quality-repair-workspace-contract.md) | 质量修复子工作区越界改根 output.md,本地目标缺失 |
+1 -1
View File
@@ -25,7 +25,7 @@
| 前端工作台 | 三栏 + 块文档 + SSE | partial | [test workbench](../test-reports/2026-09-08-three-column-workbench-and-block-document-test-report.md);[现场专项审计](../audit/2026-09-13-conversation-intent-runtime-state.md) | 错误卡片/任务进度/文档 busy 状态刷新恢复与人工抽检待完成 | `web` |
| PI Runner / 配置链 | pi-agent + MiniMax 真实链路 | done | [test piagent](../test-reports/2026-09-13-codex-audit-p0-remediation-test-report.md);[container-env](../test-reports/2026-09-13-container-env-configuration-test-report.md) | — | `pi-agent` |
| 三环境运维 | dev/test/main + 共享 KB | done | [PROJECT.md](../../PROJECT.md) §4;四环境 healthy | — | `infra` |
| CI / E2E | PG 集成 + 真实 PI E2E | partial | [gap-matrix BUG-08](../remediation/gap-matrix.md);[test pg-ci](../test-reports/2026-09-12-pg-ci-soak-e2e-test-report.md);[UI-13a 分类导航](../audit/2026-09-13-document-revision-workflow-category-navigation.md) | CI `TEST_DATABASE_URL` + 真实 PI E2E 实录;打开受控文档前须先切流程受控分类 | `ci` |
| CI / E2E | PG 集成 + 真实 PI E2E | partial | [gap-matrix BUG-08](../remediation/gap-matrix.md);[test pg-ci](../test-reports/2026-09-12-pg-ci-soak-e2e-test-report.md);[UI-13a 职责单元格](../audit/2026-09-13-document-revision-duty-cell-assertion.md) | CI `TEST_DATABASE_URL` + 真实 PI E2E 实录;职责断言不得用整行 option 文案 | `ci` |
---
+46 -9
View File
@@ -14,6 +14,11 @@ import {
CATEGORY_TRIGGER_LABEL,
requiredCategoryForDocOpen,
} from '../src/lib/workflowCategoryNavigation';
import {
DOC_DUTY_BADGE_TESTID,
DOC_DUTY_SELECT_TESTID,
evaluateReviewerDuty,
} from '../src/lib/dutyCellAssertion';
/**
* Document revision runtime — real UI acceptance (UI-01..UI-16).
@@ -373,6 +378,46 @@ async function openWorkflowDocById(page: Page, docId: string): Promise<void> {
await item.click();
}
async function readReviewerIntakeDuty(page: Page, docId?: string): Promise<string> {
const select = docId
? page.locator(`[data-testid="${DOC_DUTY_SELECT_TESTID}"][data-doc-id="${docId}"]`)
: page
.locator('tr')
.filter({ hasText: /用户初始需求|需求摸底/ })
.locator(`[data-testid="${DOC_DUTY_SELECT_TESTID}"]`);
if ((await select.count()) > 0) {
await expect(select).toBeVisible({ timeout: 10_000 });
const verdict = evaluateReviewerDuty({
source: 'select',
value: await select.inputValue(),
disabled: await select.isDisabled(),
canAssignLead: (await select.getAttribute('data-can-assign')) === 'true',
});
if (!verdict.ok || verdict.role !== 'reviewer') {
fail(`reviewer B intake duty select must be reviewer: ${verdict.reason}`);
}
return 'reviewer';
}
const badge = docId
? page.locator(`[data-testid="${DOC_DUTY_BADGE_TESTID}"][data-doc-id="${docId}"]`)
: page
.locator('tr')
.filter({ hasText: /用户初始需求|需求摸底/ })
.locator(`[data-testid="${DOC_DUTY_BADGE_TESTID}"]`);
if ((await badge.count()) === 0) {
fail('intake duty cell missing select and badge');
}
await expect(badge).toBeVisible({ timeout: 10_000 });
const verdict = evaluateReviewerDuty({
source: 'badge',
role: (await badge.getAttribute('data-doc-role')) || '',
});
if (!verdict.ok || verdict.role !== 'reviewer') {
fail(`reviewer B intake duty badge must be reviewer: ${verdict.reason}`);
}
return 'reviewer';
}
async function sendChat(page: Page, text: string) {
const box = page.getByPlaceholder(/向项目助手提问/);
await expect(box).toBeVisible({ timeout: 15_000 });
@@ -1229,15 +1274,7 @@ test.describe.serial('document-revision-runtime UI acceptance', () => {
await pageB.locator('[title*="法定权限矩阵"]').click();
await expect(pageB.getByText('当前账号与项目职责权限矩阵')).toBeVisible({ timeout: 15_000 });
const intakeRow = pageB.locator('tr').filter({ hasText: /用户初始需求|需求摸底/ }).first();
await expect(intakeRow).toBeVisible({ timeout: 10_000 });
const duty = (await intakeRow.innerText()).replace(/\s+/g, ' ');
if (/编制负责人/.test(duty)) {
fail(`reviewer B must not be 编制负责人 on intake; duty=${duty}`);
}
if (!/审核负责人|查阅者/.test(duty)) {
fail(`reviewer B intake duty must be 审核/只读, got ${duty}`);
}
const duty = await readReviewerIntakeDuty(pageB, controlledDocId);
await pageB.getByRole('button', { name: '关闭面板' }).click();
await expect(pageB.getByText('当前账号与项目职责权限矩阵')).toHaveCount(0, { timeout: 10_000 });
@@ -10,6 +10,11 @@ import {
import { UserProfile, DocPermissionRole } from '../../types/docgov';
import { DeliverableItem } from '../../types/workbench';
import { resolveCurrentDocRole } from '../../lib/currentDocRole';
import {
DOC_DUTY_BADGE_TESTID,
DOC_DUTY_ROW_TESTID,
DOC_DUTY_SELECT_TESTID,
} from '../../lib/dutyCellAssertion';
interface UserProjectRightsModalProps {
isOpen: boolean;
@@ -424,6 +429,9 @@ export const UserProjectRightsModal: FC<UserProjectRightsModalProps> = ({
return (
<tr
key={item.docId}
data-testid={DOC_DUTY_ROW_TESTID}
data-doc-id={item.docId}
data-doc-role={role}
style={{
borderBottom: idx === deliverables.length - 1 ? 'none' : '1px solid var(--border)',
backgroundColor: isCurrentActive ? 'rgba(79, 107, 255, 0.08)' : 'transparent',
@@ -467,6 +475,10 @@ export const UserProjectRightsModal: FC<UserProjectRightsModalProps> = ({
<td style={{ padding: '10px 12px' }}>
{onRoleChangeForDoc ? (
<select
data-testid={DOC_DUTY_SELECT_TESTID}
data-doc-id={item.docId}
data-doc-role={role}
data-can-assign={canAssignLead ? 'true' : 'false'}
value={role}
disabled={!canAssignLead}
onChange={(e) => onRoleChangeForDoc(item.docId, e.target.value as DocPermissionRole)}
@@ -494,6 +506,9 @@ export const UserProjectRightsModal: FC<UserProjectRightsModalProps> = ({
</select>
) : (
<span
data-testid={DOC_DUTY_BADGE_TESTID}
data-doc-id={item.docId}
data-doc-role={role}
style={{
display: 'inline-block',
padding: '3px 8px',
+82
View File
@@ -0,0 +1,82 @@
import { describe, it } from 'node:test';
import assert from 'node:assert/strict';
import {
evaluateExpectedDuty,
evaluateReviewerDuty,
resolveDutyCell,
} from './dutyCellAssertion';
const FIELD_FAILURE_ROW =
'V0.5 用户初始需求收集表当前打开 状态: drafting ✍️ 编制负责人 🔍 审核负责人 👑 批准负责人 👁️ 查阅者 (只读) 前往';
describe('dutyCellAssertion', () => {
it('parses author, reviewer, and viewer from select value or badge role', () => {
assert.equal(resolveDutyCell({ source: 'select', value: 'author', disabled: false, canAssignLead: true }).role, 'author');
assert.equal(
resolveDutyCell({ source: 'select', value: 'reviewer', disabled: true, canAssignLead: false }).role,
'reviewer',
);
assert.equal(resolveDutyCell({ source: 'badge', role: 'viewer' }).role, 'viewer');
});
it('must not infer author from a row whose option labels include 编制负责人', () => {
assert.throws(
() => resolveDutyCell({ source: 'row-text', text: FIELD_FAILURE_ROW }),
/must_not_infer_duty_from_row_option_text/,
);
const verdict = evaluateReviewerDuty({ source: 'row-text', text: FIELD_FAILURE_ROW });
assert.equal(verdict.ok, false);
assert.match(verdict.reason, /must_not_infer_duty_from_row_option_text/);
});
it('accepts only a disabled non-assignable reviewer select', () => {
const verdict = evaluateReviewerDuty({
source: 'select',
value: 'reviewer',
disabled: true,
canAssignLead: false,
});
assert.equal(verdict.ok, true);
assert.equal(verdict.role, 'reviewer');
});
it('rejects an enabled reviewer select when the reviewer cannot assign leads', () => {
const verdict = evaluateReviewerDuty({
source: 'select',
value: 'reviewer',
disabled: false,
canAssignLead: false,
});
assert.equal(verdict.ok, false);
assert.match(verdict.reason, /disabled/);
});
it('rejects canAssignLead=true even if the reviewer select is disabled', () => {
const verdict = evaluateReviewerDuty({
source: 'select',
value: 'reviewer',
disabled: true,
canAssignLead: true,
});
assert.equal(verdict.ok, false);
assert.match(verdict.reason, /assign_lead/);
});
it('rejects viewer, author, approver, and unknown values for USER_B', () => {
assert.equal(
evaluateReviewerDuty({ source: 'select', value: 'viewer', disabled: true, canAssignLead: false }).ok,
false,
);
assert.equal(evaluateReviewerDuty({ source: 'badge', role: 'viewer' }).ok, false);
assert.equal(
evaluateReviewerDuty({ source: 'select', value: 'author', disabled: true, canAssignLead: false }).ok,
false,
);
assert.equal(evaluateReviewerDuty({ source: 'badge', role: 'approver' }).ok, false);
assert.equal(evaluateReviewerDuty({ source: 'badge', role: 'unknown' }).ok, false);
assert.match(
evaluateExpectedDuty({ source: 'badge', role: 'viewer' }, 'reviewer').reason,
/expected_duty:reviewer;got:viewer/,
);
});
});
+73
View File
@@ -0,0 +1,73 @@
export const DOC_DUTY_ROW_TESTID = 'doc-duty-row';
export const DOC_DUTY_SELECT_TESTID = 'doc-duty-select';
export const DOC_DUTY_BADGE_TESTID = 'doc-duty-badge';
export const DOC_DUTY_ROLES = ['author', 'reviewer', 'approver', 'viewer'] as const;
export type DocDutyRole = (typeof DOC_DUTY_ROLES)[number];
export type DutyCellInput =
| { source: 'select'; value: string; disabled: boolean; canAssignLead: boolean }
| { source: 'badge'; role: string }
| { source: 'row-text'; text: string };
export type DutyCellVerdict = {
ok: boolean;
role?: DocDutyRole;
reason: string;
};
export function isDocDutyRole(value: string): value is DocDutyRole {
return (DOC_DUTY_ROLES as readonly string[]).includes(value);
}
export function resolveDutyCell(input: DutyCellInput): { role: DocDutyRole; disabled?: boolean } {
if (input.source === 'row-text') {
throw new Error('must_not_infer_duty_from_row_option_text');
}
if (input.source === 'select') {
if (!isDocDutyRole(input.value)) {
throw new Error(`unknown_duty_select_value:${input.value}`);
}
return { role: input.value, disabled: input.disabled };
}
if (!isDocDutyRole(input.role)) {
throw new Error(`unknown_duty_badge_role:${input.role}`);
}
return { role: input.role };
}
export function evaluateExpectedDuty(input: DutyCellInput, expected: DocDutyRole): DutyCellVerdict {
if (input.source === 'row-text') {
return { ok: false, reason: 'must_not_infer_duty_from_row_option_text' };
}
let role: DocDutyRole;
try {
role = resolveDutyCell(input).role;
} catch (err) {
return { ok: false, reason: err instanceof Error ? err.message : String(err) };
}
if (role !== expected) {
return { ok: false, role, reason: `expected_duty:${expected};got:${role}` };
}
return { ok: true, role, reason: 'ok' };
}
/**
* USER_B was assigned reviewer in UI-01. Accept only role=reviewer.
* A select must be non-assignable and disabled; viewer/author/approver fail closed.
*/
export function evaluateReviewerDuty(input: DutyCellInput): DutyCellVerdict {
const verdict = evaluateExpectedDuty(input, 'reviewer');
if (!verdict.ok) {
return verdict;
}
if (input.source === 'select') {
if (input.canAssignLead) {
return { ok: false, role: 'reviewer', reason: 'reviewer_must_not_assign_lead' };
}
if (!input.disabled) {
return { ok: false, role: 'reviewer', reason: 'reviewer_select_must_be_disabled' };
}
}
return verdict;
}