diff --git a/docs/reviews/2026-10-22-increment-c-fe-review.md b/docs/reviews/2026-10-22-increment-c-fe-review.md new file mode 100644 index 0000000..01856c8 --- /dev/null +++ b/docs/reviews/2026-10-22-increment-c-fe-review.md @@ -0,0 +1,90 @@ +# 增量 C 前端独立评审(W10 审计工作台 + 依据/候选确认路径) + +- 评审对象:`feature/rdm-two-trees-followup-w10` @ `fa08be4`(基线 `ec1ced3`,dev 无本地改动) +- 范围:`src/components/RdmAuditWorkspace.tsx(+test)`、`Conversation.tsx`、`ConversationWork.tsx`、`Workbench.tsx`、`src/api.ts`、`src/styles.css`、`src/companion-conversation.css`、`e2e/rdm-audit.playwright.ts` +- 增量:`+1048 / -7`,9 文件(`git diff --stat ec1ced3 fa08be4`) +- 评审性质:只读评审。代码零改动;本文件是唯一写入。契约核对基线:RDM 侧 `Agent-System-RDM-wt-feature-rdm-two-trees-followup-c-w8-w10` @ `c4c6df7`(本地检出的 W8b/W5 命令面);计划基线:`modules/documents` `rdm-two-trees-stage5-implementation-test-plan-r1.md` §W10 +- 环境限制:本仓为独立检出,组合仓 `../ui`(`@ui` 源码别名)缺失、子模块未初始化 ⇒ 依赖 `@ui` 的 22 个测试文件与 `tsc -b` 无法在本工作区运行(见 §验证)。`npm ci --offline` 可用,故守护测试与 knip 实跑。 + +## 1. 逐项结论 + +| # | 检查项 | 结论 | 依据 | +|---|---|---|---| +| 1 | 设计门禁(令牌/动效/折叠) | **FAIL(部分)** | 裸色值、私有 keyframes、行内字号、`transition`、`--fs-ui-*` 全部通过守护测试(7/7 PASS);但两处折叠未用 `@ui Collapsible`(MAJOR-3) | +| 2 | DTO 边界(只消费服务端已裁剪 DTO、无新增全量拉取) | **PASS** | 页面只渲染 `display/recorded/disclosure`;服务端 `redactOperationBodies`(`inspection_visibility.go:70-88`)确实把无权正文的 `summary.display` 换成「受限」并清空 `object.current`,前端未越权补正文;列表单页 `limit:30`,证据/影响分析均为按需单条读取,无全量拉取 | +| 3 | 异步纪律(单一 owner、seq 丢迟到响应) | **PASS(有 1 处冗余镜像,见 MINOR-4)** | `createOperationsFeed` 每请求取 `++seq`,非最新 seq 的成功与失败一并丢弃,`dispose()` 亦作废在途;续页回传首页 `cutoffSeq`;其余面板沿用既有 `startLatestFetch` 单 owner 模式 | +| 4 | 四态可读 + 游标失效显式 | **PASS** | 加载/空/错误(denied·cursor·query·capability·auth 分流)/旧记录「当时未记录」/正文受限/部分覆盖/影响截断均有独立文案;错误分类正则与服务端真实串一致(`权限拒绝:…` 出自 `perms_api.go:234`;`操作记录游标已失效…` 出自 `operation_projection.go:42`),非臆测匹配 | +| 5 | 既有行为兼容(树/卡片/对话/导出预览;右栏收起的确认路径) | **FAIL** | 树/卡片/对话/导出预览无回归(页签按名定位,`审计记录` 与既有 `操作记录` 不冲突,`.rdm-audit-card` 计数断言互不干扰);但右栏收起后的「对话确认路径」在最需要它的工作卡场景下是空操作(MAJOR-1) | +| 6 | e2e 扩展质量(skip 如实) | **PASS(附 MINOR-6)** | `test.skip(!SVC_REAL_RDM_BIN, …)` 与注释如实说明「缺真实 RDM 二进制则整组跳过、不伪造」;但当前 CI 与 Dev `scripts/e2e.sh` 均不注入该变量 ⇒ 4 条新用例在任何现有执行入口下都永不运行(零证据) | +| 7 | 知识多挂载为 TODO 未实现、未虚报 | **PASS(附 MINOR-7)** | `RdmAuditWorkspace.tsx:591` 仅留 TODO,未见任何实现或「已完成」表述;TODO 给出的理由经核实为真:W8b 分支 RDM 侧 `grep '"mounts"'` 零命中,`rdm.inspect.tree/object` 确实无挂载字段(域层已有 `MountsOf`,仅未出 DTO) | + +## 2. BLOCKING + +无。 + +## 3. MAJOR + +### MAJOR-1 右栏收起后的「对话确认路径」在目标场景下是空操作(承诺与实现不符) + +- 位置:`src/components/Conversation.tsx:571-572, 734-740`;关联 `src/components/ConversationWork.tsx:65, 91`;承诺文案 `src/components/Workbench.tsx:82` +- 理由:新提示条仅做 DOM 定位滚动: + `body.querySelector('[data-card-id="…"]') ?? body.querySelector('[data-conversation-work]')`。 + 但显示条件恰是 `!workPaneOwnsCards`(右栏收起或非工作页签),此时: + 1. `ConversationWork` 命中 `if (!detailHere) return null`(`ConversationWork.tsx:91`)——`[data-conversation-work]` 整个子树不存在; + 2. 工作卡在对话正文里只经 `CardAnchor` 出现,而 `Conversation.tsx:56` 对带 `card.work` 的卡显式传 `card={undefined}`,UI 侧 `data-card-id={card?.id}`(`modules/ui/src/CardAnchor.tsx:25`)因此不落属性。 + 结果:W10 重点场景(候选工作卡待确认)点「N 项待你确认 · 在对话中处理 ↓」→ `target` 为 `null` → `target?.scrollIntoView` 静默无动作,而收起按钮的 title 却承诺「N 项待确认会在对话中继续处理」。 + 附带:`pendingHere`(含 `work.decision.state === 'open'`)与 `pendingCardId`(仅 `status === 'action_required'`,`Conversation.tsx:572`)口径不同,decision-open 卡即使能定位也算不出 `id`。 +- 修复建议:改走仓内既有的唯一定位路径 `store.openInbox(pendingCardId)`(`src/store.ts:1625-1633`:展开右栏、切工作页签、`flashLocate('.right-panel','data-card-id',…)`),或在 `!workPaneOwnsCards` 时把候选/决议控件渲染进对话正文;同时让 `pendingCardId` 与 `pendingConfirmations` 用同一谓词。 + +### MAJOR-2 新增「审计记录」页签无能力门禁,缺命令面的构建上恒为错误态 + +- 位置:`src/components/RdmAuditWorkspace.tsx:52, 125-137` +- 理由:页签无条件挂载,而仓内既有约定是「能力缺失即不暴露」(`src/store.ts:1462-1475` 依 `api.listCommands()` 刷新能力位;`src/workspaces.ts:52` `rdmAuditReady`)。当前就绪门 `RDM_AUDIT_COMMANDS` 只含 7 个 `rdm.inspect.*`(`workspaces.ts:23-31`),不含 W8b/W5 的 `rdm.audit.operations`、`rdm.trace.coverage`、`rdm.trace.pending-recheck`、`rdm.compliance.impact.*`。因此在已合入的 RDM 上审计工作区入口照常可见,而新页签必然落到 `classifyOperationsError → capability`(「当前服务未提供审计能力」),与 `e2e/plain-base.playwright.ts:38-51` 记录的「零暴露、不进入即 400」原则相悖。 +- 修复建议:在 `workspaces.ts` 增加 `rdmAuditOperationsReady(cmds)`(上述 5 条命令)并在页签列表按能力过滤;覆盖面板与影响分析入口同样受此门控;若坚持常显,则须在 PR 描述中登记为已知降级并由 RDM 发布节奏兜底。 + +### MAJOR-3 两处折叠未使用 `@ui Collapsible`(违反 DESIGN.md §9.2) + +- 位置:`src/components/RdmAuditWorkspace.tsx:1200`(`EvidenceLink`)、`:1266`(`ImpactAnalysisBlock`) +- 理由:两者用 ` + )} + + ) +} + +function OperationsErrorView({ error, onRestart }: { error: OperationsFeedError; onRestart: () => void }) { + return ( +
+ {error.message} + {(error.kind === 'cursor' || error.kind === 'query') && ( + + )} + 原始回执}> + {error.raw} + +
+ ) +} + +export function OperationRow({ pid, record, defaultOpen }: { pid: string; record: RdmOperationRecord; defaultOpen?: boolean }) { + const view = describeOperation(record) + const legacy = isLegacyOperation(record) + const restricted = record.change.disclosure === 'restricted' + const { actor, change, basis, impact, run, effectiveAt } = record + return ( +
+ + {view.who} + {view.whoNow && {view.whoNow}} + {view.action} + {view.object} + {view.objectNow && {view.objectNow}} + {view.when} + {view.outcome} + {legacy && 旧记录} + {restricted && 正文受限} + + )}> +
+
{view.sentence}
+ {legacy &&
这是旧记录:标为「当时未记录」的字段在操作发生时没有记录,不代表为空或为零。
} + {restricted &&
正文受限:你当前没有该对象的正文权(跨项目、无权或已撤权),这里只显示元数据。
} + 字段差异 +
+
修改前
{change.before.display}
+
修改后
{change.after.display}
+
差异摘要
{change.summary.display}
+
+ 来源与确认链 +
+
操作人(当时)
{actor.name.display}{actor.duty.recorded ? ` · ${actor.duty.display}` : ''}{view.whoNow ? `(${view.whoNow})` : ''}
+
依据原因
{basis.reason.display}
+
执行回执
{run.display}{run.runId ? ` · 运行 ${run.runId}` : ''}{run.stepId ? ` · 步骤 ${run.stepId}` : ''}{run.attempt ? ` · 第 ${run.attempt} 次尝试` : ''}
+
发起时间
{view.when}
+
生效时间
{effectiveAt.recorded && effectiveAt.value ? fmtTime(effectiveAt.value) : effectiveAt.display}
+
对象路径
{view.object}{view.objectNow ? `(${view.objectNow})` : ''}
+
+ + 影响(当时与当前) +
+
直接影响(当时)
{refsText(impact.direct)}
+
间接影响(当时)
{refsText(impact.indirect)}
+
受控影响分析
{impact.analyses.display}
+
+ {impact.analyses.items.map(a => )} +
+
+
+ ) +} + +function EvidenceList({ pid, refs }: { pid: string; refs: RdmOperationRefs }) { + return ( +
+ 依据证据 + {refs.items.length === 0 + ?
{refs.display}
+ : refs.items.map(r => )} +
+ ) +} + +/** 证据跳转:展开时才读取对象详情;服务端按当前正文权判定,无权时只给出可读拒绝,不假设有正文。 */ +function EvidenceLink({ pid, evidence }: { pid: string; evidence: RdmOperationRef }) { + const label = `${evidence.kind.label} ${evidence.id}` + return ( + {`查看证据:${label}`}}> + + + ) +} + +function EvidenceDetail({ pid, evidence, label }: { pid: string; evidence: RdmOperationRef; label: string }) { + const [detail, setDetail] = useState(null) + const [err, setErr] = useState('') + const genRef = useRef(0) + useEffect(() => { + setDetail(null); setErr('') + return startLatestFetch(genRef, + () => api.rdmCommand('rdm.inspect.object', { projectId: pid, kind: evidence.kind.key, id: evidence.id }), + setDetail, setErr) + }, [pid, evidence.kind.key, evidence.id]) + if (err) { + return /权限拒绝|无权|受限/.test(err) + ?
无权查看该证据(跨项目、无权或已撤权),仅保留元数据:{label}
+ : + } + return !detail ?
加载中…
: +} + +const refKey = (r: RdmObjectRef) => `${r.kind}:${r.id}` +const refLabel = (r: RdmObjectRef) => `${r.kind} ${r.id}${r.revision ? ` r${r.revision}` : ''}` + +/** 当时快照与按当前受控图重算结果的差异(按对象 kind:id 比较)。 */ +export function diffImpact(then: RdmImpactAnalysis, now: RdmImpactAnalysis) { + const thenKeys = new Set(then.involved.map(refKey)) + const nowKeys = new Set(now.involved.map(refKey)) + return { + added: now.involved.filter(r => !thenKeys.has(refKey(r))), + removed: then.involved.filter(r => !nowKeys.has(refKey(r))), + } +} + +function ImpactSnapshotView({ title, a }: { title: string; a: RdmImpactAnalysis }) { + return ( +
+ {title} +
起点 {refLabel(a.start)} · 分析 {a.id} · {fmtTime(a.at)}{a.actor ? ` · ${a.actor}` : ''}
+
直接影响 {a.direct.length} 项 · 共涉及 {a.involved.length} 项 · {a.layers.length} 层
+ {a.direct.length > 0 &&
直接:{a.direct.map(refLabel).join('、')}
} + {a.truncated &&
部分覆盖:已达上限({a.maxNodes} 个对象 / {a.maxDepth} 层)截断,结果不完整
} +
+ ) +} + +/** 当时与当前影响:展开时才读取不可变快照;重算须显式触发(生成新分析快照,不覆盖历史)。 */ +function ImpactAnalysisBlock({ pid, analysisId }: { pid: string; analysisId: string }) { + return ( +
+ {`查看影响分析 ${analysisId}`}}> + + +
+ ) +} + +function ImpactAnalysisDetail({ pid, analysisId }: { pid: string; analysisId: string }) { + const [snapshot, setSnapshot] = useState(null) + const [current, setCurrent] = useState(null) + const [err, setErr] = useState('') + const [busy, setBusy] = useState(false) + const snapRef = useRef(0) + const currentRef = useRef(0) + useEffect(() => { + setSnapshot(null); setErr('') + return startLatestFetch(snapRef, () => api.rdmImpactGet(pid, analysisId), setSnapshot, setErr) + }, [pid, analysisId]) + useEffect(() => () => { currentRef.current++ }, [pid, analysisId]) + + const revision = Number(snapshot?.start.revision) + const recompute = () => { + if (!snapshot || !(revision > 0)) return + setBusy(true); setErr('') + startLatestFetch(currentRef, + () => api.rdmImpactRecompute(pid, { startKind: snapshot.start.kind, startId: snapshot.start.id, startRevision: revision, maxNodes: snapshot.maxNodes, maxDepth: snapshot.maxDepth }), + a => { setCurrent(a); setBusy(false) }, + e => { setErr(e); setBusy(false) }) + } + const diff = snapshot && current ? diffImpact(snapshot, current) : null + + return ( + <> + {err && } + {!snapshot && !err &&
加载中…
} + {snapshot && } + {snapshot && revision > 0 && ( + + )} + {current && } + {diff && ( +
+
当前新增:{diff.added.length ? diff.added.map(refLabel).join('、') : '无'}
+
当前不再涉及:{diff.removed.length ? diff.removed.map(refLabel).join('、') : '无'}
+
+ )} + + ) +} + +interface TraceCoverageData { coverage: RdmTraceCoverage; pending: RdmPendingRecheckEdge[] } + +const PENDING_STATE_LABELS: Record = { 'pending-recheck': '待复核', stale: '已过期待复核' } + +function TraceCoveragePanel({ pid }: { pid: string }) { + const [data, setData] = useState(null) + const [err, setErr] = useState('') + const genRef = useRef(0) + useEffect(() => { + setData(null); setErr('') + return startLatestFetch(genRef, + () => Promise.all([api.rdmTraceCoverage(pid), api.rdmTracePendingRecheck(pid)]).then(([coverage, pending]) => ({ coverage, pending })), + setData, setErr) + }, [pid]) + if (err) return
受控覆盖统计暂不可用:{classifyOperationsError(err).message}
+ if (!data) return
正在读取受控覆盖…
+ return +} + +export function TraceCoverageView({ pid, data }: { pid: string; data: TraceCoverageData }) { + const { coverage, pending } = data + const partial = coverage.gaps.length > 0 || coverage.candidate > 0 || pending.length > 0 + const cards: [string, number][] = [ + ['正式覆盖', coverage.confirmed], + ['候选(不计正式覆盖)', coverage.candidate], + ['待复核', pending.length], + ['缺项链', coverage.gaps.length], + ] + return ( +
+ 受控追溯覆盖 +
+ {cards.map(([label, n]) => ( +
+
{n}
+
{label}
+
+ ))} +
+ {partial &&
部分覆盖:存在候选、待复核或缺项链,正式覆盖只计已确认的受控边。
} + {coverage.gaps.length > 0 && ( + 缺项链({coverage.gaps.length})}> + {coverage.gaps.map((g, i) => ( +
+ 规则 {g.rule}{g.node} · {g.reason} +
+ ))} +
+ )} + {pending.length > 0 && ( + 待复核受控边({pending.length})}> + {pending.map((e, i) => ( +
+ {PENDING_STATE_LABELS[e.state] ?? humanizeState(e.state)} + {e.kind} + {e.from}(r{e.fromOldRevision} → r{e.fromNewRevision || '?'})⇢ {e.to}(r{e.toOldRevision} → r{e.toNewRevision || '?'}) +
+ ))} +
+ )} +
+ ) +} diff --git a/src/components/Workbench.tsx b/src/components/Workbench.tsx index e36f95c..f04fb10 100644 --- a/src/components/Workbench.tsx +++ b/src/components/Workbench.tsx @@ -34,6 +34,23 @@ const scratchInboxCallbacks = { } const inboxCallbacks = { ...scratchInboxCallbacks, onArchive: (c: ActivityCard) => store.cardToProject(c) } +type Scope = string | null | undefined + +/** 本会话仍待用户处理的卡片:计数与定位共用这一谓词。 */ +const isPendingConfirmation = (card: ActivityCard, conversationId: Scope, projectId: Scope) => + card.status !== 'dismissed' && card.conversationId === conversationId && card.projectId === projectId + && (card.status === 'action_required' || card.work?.decision?.state === 'open') + +/** 本会话仍待用户处理的卡片数:右栏收起后由对话输入区提示承接确认路径。 */ +export function pendingConfirmations(cards: ActivityCard[], conversationId: Scope, projectId: Scope): number { + return cards.filter(card => isPendingConfirmation(card, conversationId, projectId)).length +} + +/** 右栏收起或不在「工作」页签时的确认路径:经 openInbox 展开右栏、切到工作页签并定位首张待处理卡。 */ +export function openPendingConfirmation(cards: ActivityCard[], conversationId: Scope, projectId: Scope) { + store.openInbox(cards.find(card => isPendingConfirmation(card, conversationId, projectId))?.id) +} + /** 项目收件箱视图(CF3/D49/D51):项目级卡片 ∪ 其他会话待办(done 不聚合)。 */ function ProjectInbox({ cards, filter }: { cards: ActivityCard[]; filter: InboxFilter }) { const own = cards.filter(c => !c.conversationId) @@ -72,6 +89,8 @@ export function Workbench() { const visible = inboxScope === 'project' ? projectCards : cards const pending = pendingCount(visible) const unread = visible.filter(card => card.work && card.work.resultVersion > card.work.readVersion).length + const pendingHere = useStore(s => pendingConfirmations(s.cards, s.currentConvId, s.currentProjectId)) + const collapseHint = pendingHere > 0 ? `收起右栏(${pendingHere} 项待确认可从对话输入区的提示回到工作栏处理)` : '收起右栏' // 离开文件入口时收回全屏展开,避免出现「工作」铺满主区的怪态。 const switchTab = (tab: 'work' | 'files') => store.setUI({ rightTab: tab, previewExpanded: tab === 'files' ? store.getState().ui.previewExpanded : false }) @@ -86,7 +105,7 @@ export function Workbench() { {rightTab === 'work' && ( )} - + {rightTab === 'files' ? diff --git a/src/store.ts b/src/store.ts index 9fbff05..6720d3d 100644 --- a/src/store.ts +++ b/src/store.ts @@ -11,7 +11,7 @@ import { canFlushOutbox, deriveShellPhase } from './runstate' import { flashElement } from './scrollsync' import { isThemeChoice, resolveTheme, type ThemeChoice } from './theme' import { mergeSurfacesByRevision, surfaceKey } from './surfaceKey' -import { cockpitReady, rdmAuditReady, rdmExportReady } from './workspaces' +import { cockpitReady, rdmAuditOperationsReady, rdmAuditReady, rdmExportReady } from './workspaces' import { rdmRolesReady, rdmMembersReady, rdmDutyReady, rdmPermMatrixReady } from './rdmManagement' import { NO_RENDER_CAPABILITIES, renderCapabilities, type RenderCapabilities } from './documentRenderPresentation' @@ -147,6 +147,8 @@ interface AppState { rdmCockpitAvailable: boolean /** 概览导出区:三条 rdm.inspect.export.* 都启用才为 true。不参与七命令就绪门。 */ rdmExportAvailable: boolean + /** 「审计记录」页签(W10):五条 W8b/W5 命令都启用才为 true。不参与七命令就绪门。 */ + rdmAuditOperationsAvailable: boolean /** U6 RDM 管理页能力:命令面齐备才显示对应分区/页签(普通基座零暴露,fail-closed)。 */ rdmRolesAvailable: boolean rdmMembersAvailable: boolean @@ -226,6 +228,7 @@ let state: AppState = { rdmAuditAvailable: false, rdmCockpitAvailable: false, rdmExportAvailable: false, + rdmAuditOperationsAvailable: false, rdmRolesAvailable: false, rdmMembersAvailable: false, rdmDocReviewAvailable: false, rdmPermMatrixAvailable: false, rdmRender: NO_RENDER_CAPABILITIES, adminSections: [], @@ -658,7 +661,8 @@ export const store = { plan: null, todos: null, artifacts: [], surfaces: [], projectSurfaces: [], cards: [], projectCards: [], cardActionStates: {}, workRecord: null, workReference: null, inboxScope: 'conv', runtimeMock: false, profiles: [], pluginPanels: [], serverWorkspaces: [], adminSections: [], - rdmAuditAvailable: false, rdmCockpitAvailable: false, rdmExportAvailable: false, rdmRolesAvailable: false, rdmMembersAvailable: false, + rdmAuditAvailable: false, rdmCockpitAvailable: false, rdmExportAvailable: false, rdmAuditOperationsAvailable: false, + rdmRolesAvailable: false, rdmMembersAvailable: false, rdmDocReviewAvailable: false, rdmPermMatrixAvailable: false, rdmRender: NO_RENDER_CAPABILITIES, connected: false, posting: false, error: null, toast: null, outbox: [], outboxDraining: false, previewTabs: {}, previewActive: {}, previewOpenIntent: 0 }) @@ -1465,6 +1469,7 @@ export const store = { rdmAuditAvailable: rdmAuditReady(cmds), rdmCockpitAvailable: cockpitReady(cmds), rdmExportAvailable: rdmExportReady(cmds), + rdmAuditOperationsAvailable: rdmAuditOperationsReady(cmds), rdmRolesAvailable: rdmRolesReady(cmds), rdmMembersAvailable: rdmMembersReady(cmds), rdmDocReviewAvailable: rdmDutyReady(cmds), @@ -1473,7 +1478,8 @@ export const store = { }) } catch { if (epoch !== identityEpoch) return - set({ rdmAuditAvailable: false, rdmCockpitAvailable: false, rdmExportAvailable: false, rdmRolesAvailable: false, rdmMembersAvailable: false, + set({ rdmAuditAvailable: false, rdmCockpitAvailable: false, rdmExportAvailable: false, rdmAuditOperationsAvailable: false, + rdmRolesAvailable: false, rdmMembersAvailable: false, rdmDocReviewAvailable: false, rdmPermMatrixAvailable: false, rdmRender: NO_RENDER_CAPABILITIES }) } try { diff --git a/src/styles.css b/src/styles.css index 54f065a..0f02bc3 100644 --- a/src/styles.css +++ b/src/styles.css @@ -669,6 +669,9 @@ button { font-family: inherit; font-size: inherit; color: inherit; } .rdm-audit-export-result { display: flex; flex-direction: column; align-items: flex-start; gap: 4px; font-size: var(--fs-ui-sm); color: var(--text-dim); } .rdm-audit-export-err { font-size: var(--fs-ui-sm); color: var(--err-text); } .rdm-audit-coverage-item { display: flex; align-items: center; gap: 8px; padding: 3px 0; font-size: var(--fs-ui-sm); color: var(--text-dim); } +.rdm-audit-wrap { overflow-wrap: anywhere; min-width: 0; } +.rdm-audit-op .rdm-audit-opgroup-head, .rdm-audit-op-pending { flex-wrap: wrap; } +.rdm-audit-op-object { color: var(--text); overflow-wrap: anywhere; min-width: 0; } .rdm-audit-err { display: flex; flex-direction: column; gap: 8px; } .rdm-audit-err-toggle { font-size: var(--fs-ui-caption); color: var(--text-dim); } .rdm-audit-err-raw { display: block; font-family: var(--mono); font-size: var(--fs-code); color: var(--text-dim); background: var(--bg-inset); border-radius: var(--radius-sm); padding: 8px 12px; white-space: pre-wrap; word-break: break-all; } diff --git a/src/workspaces.ts b/src/workspaces.ts index 0e9f0d5..f51e1f5 100644 --- a/src/workspaces.ts +++ b/src/workspaces.ts @@ -54,6 +54,21 @@ export function rdmAuditReady(cmds: { id?: string; enabled?: boolean }[] | null return RDM_AUDIT_COMMANDS.every(need => cmds.some(c => c?.id === need && c.enabled === true)) } +/** 「审计记录」页签(W10)消费的 W8b/W5 命令:操作记录列表、受控覆盖面板、影响分析快照与重算。不并入七命令就绪门。 */ +export const RDM_AUDIT_OPERATIONS_COMMANDS = [ + 'rdm.audit.operations', + 'rdm.trace.coverage', + 'rdm.trace.pending-recheck', + 'rdm.compliance.impact.get', + 'rdm.compliance.impact.recompute', +] as const + +/** 五命令齐备且 enabled 才展示「审计记录」页签(能力缺失即不暴露,避免进入即 400)。 */ +export function rdmAuditOperationsReady(cmds: { id?: string; enabled?: boolean }[] | null | undefined): boolean { + if (!Array.isArray(cmds)) return false + return RDM_AUDIT_OPERATIONS_COMMANDS.every(need => cmds.some(c => c?.id === need && c.enabled === true)) +} + /** 文档驾驶舱消费的全部 rdm.cockpit.* 命令(I1 只读面;rollout I2)。 */ export const COCKPIT_COMMANDS = [ 'rdm.cockpit.overview',