agent-exec transport a9ccb6178c1cd1ffb0e66f9ad77b6b65/c2d99ab92f51

This commit is contained in:
agent-exec-transport
2026-10-22 05:57:33 +00:00
parent a61eaac9ae
commit 4882e0ae25
6 changed files with 183 additions and 70 deletions
+4 -8
View File
@@ -9,7 +9,7 @@ import { liveToMessage, livePhaseLabel, runEventsToTimeline, showProcessInline }
import { deriveShellPhase, isShellBusy, showLiveBubble, type ShellPhase } from '../runstate'
import type { Message, Part, HITLCard, ActivityCard, Artifact } from '../contracts'
import { UIRenderer, InlineConfirmCard, hasOpenConfirm, CardAnchor } from '@ui'
import { docBridge, pendingConfirmations } from './Workbench'
import { docBridge, openPendingConfirmation, pendingConfirmations } from './Workbench'
import { splitTimeline, mergeProcessTimeline, artifactIcon, humanSize, artifactIdsIn, markMissingArtifacts, dedupeDeliverables, foldRunErrors, processSummary, humanError } from '../messageparts'
import { ProcessTimeline } from './ProcessTimeline'
import { ConversationWork } from './ConversationWork'
@@ -569,7 +569,6 @@ function ConversationSession() {
const bgRunning = runs.filter(r => r.kind === 'subagent' && !isTerminal(r.status)).length
const unreadResults = cards.filter(card => card.work?.result && card.work.resultVersion > card.work.readVersion)
const pendingHere = pendingConfirmations(cards, currentConvId, currentProjectId)
const pendingCardId = cards.find(card => card.conversationId === currentConvId && card.projectId === currentProjectId && card.status === 'action_required')?.id ?? ''
useEffect(() => {
if (!stickRef.current) return
@@ -731,12 +730,9 @@ function ConversationSession() {
<div className="input-bar">
<OutboxPanel />
{!workPaneOwnsCards && pendingHere > 0 && <button type="button" className="work-result-notice" data-pending-confirm onClick={() => {
const body = bodyRef.current
const target = (pendingCardId ? body?.querySelector(`[data-card-id="${CSS.escape(pendingCardId)}"]`) : null) ?? body?.querySelector('[data-conversation-work]')
target?.scrollIntoView({ block: 'center' })
}}>
<Icon name="message" size={14} /><span>{pendingHere} 项待你确认</span><span>在对话中处理 ↓</span>
{!workPaneOwnsCards && pendingHere > 0 && <button type="button" className="work-result-notice" data-pending-confirm
onClick={() => openPendingConfirmation(cards, currentConvId, currentProjectId)}>
<Icon name="message" size={14} /><span>{pendingHere} 项待你确认</span><span>在工作栏处理 ↗</span>
</button>}
{unreadResults.length > 0 && <button type="button" className="work-result-notice" onClick={() => store.openInbox(unreadResults[0].id)}>
<Icon name="check" size={14} /><span>{unreadResults.length} 项工作有新结果</span><span>查看工作 ↗</span>
+77 -2
View File
@@ -1,15 +1,18 @@
import { describe, expect, it, vi } from 'vitest'
import { renderToStaticMarkup } from 'react-dom/server'
import { setPersistedOpen } from '@ui'
import type { RdmImpactAnalysis, RdmOperationPage, RdmOperationQuery, RdmOperationRecord } from '../api'
import {
classifyOperationsError, createOperationsFeed, describeOperation, diffImpact, isLegacyOperation,
OperationRow, OperationsListView, startLatestFetch, TraceCoverageView, TreeNodeRow,
OperationRow, OperationsListView, startLatestFetch, TraceCoverageView, TreeNodeRow, visibleAuditTabs,
type OperationsFeedState,
} from './RdmAuditWorkspace'
import type { ActivityCard } from '../contracts'
import { store } from '../store'
import { RDM_AUDIT_COMMANDS, RDM_AUDIT_OPERATIONS_COMMANDS, rdmAuditOperationsReady } from '../workspaces'
import { BasisStrip } from './Conversation'
import { candidateActions, candidateChanges } from './ConversationWork'
import { pendingConfirmations } from './Workbench'
import { openPendingConfirmation, pendingConfirmations } from './Workbench'
function deferred<T>() {
let resolve!: (v: T) => void
@@ -310,6 +313,58 @@ describe('OperationRow 展开内容', () => {
})
})
describe('证据与影响分析折叠(@ui Collapsible,稳定 id)', () => {
const row = () => renderToStaticMarkup(<OperationRow pid="p1" record={record('a')} defaultOpen />)
const trigger = (expanded: boolean, text: string) =>
new RegExp(`<button type="button" class="mk-col-trigger\\s*" aria-expanded="${expanded}"><span class="rdm-audit-err-toggle rdm-audit-wrap">${text}</span></button>`)
it('默认收起:触发钮由 Collapsible 渲染,详情未挂载(不发起读取)', () => {
const html = row()
expect(html).toMatch(trigger(false, '查看证据:笔记切片 SL-9'))
expect(html).toMatch(trigger(false, '查看影响分析 IA-1'))
expect(html).not.toContain('btn v-ghost rdm-audit-wrap')
expect(html).not.toContain('加载中…')
})
it('展开态按稳定 id 记忆:重挂载后恢复展开并挂载详情', () => {
setPersistedOpen('rdm-audit-evidence:p1/slice/SL-9', true)
setPersistedOpen('rdm-audit-impact:p1/IA-1', true)
try {
const html = row()
expect(html).toMatch(trigger(true, '查看证据:笔记切片 SL-9'))
expect(html).toMatch(trigger(true, '查看影响分析 IA-1'))
expect(html.match(/加载中…/g)).toHaveLength(2)
} finally {
setPersistedOpen('rdm-audit-evidence:p1/slice/SL-9', false)
setPersistedOpen('rdm-audit-impact:p1/IA-1', false)
}
})
})
describe('审计记录页签能力门禁', () => {
const enabled = (ids: readonly string[]) => ids.map(id => ({ id, enabled: true }))
it('五条 W8b/W5 命令齐备且启用才就绪;缺一、停用或清单不可用均不就绪', () => {
expect(RDM_AUDIT_OPERATIONS_COMMANDS).toHaveLength(5)
const all = enabled(RDM_AUDIT_OPERATIONS_COMMANDS)
expect(rdmAuditOperationsReady(all)).toBe(true)
for (const missing of RDM_AUDIT_OPERATIONS_COMMANDS) {
expect(rdmAuditOperationsReady(all.filter(c => c.id !== missing))).toBe(false)
expect(rdmAuditOperationsReady(all.map(c => (c.id === missing ? { ...c, enabled: false } : c)))).toBe(false)
}
expect(rdmAuditOperationsReady(null)).toBe(false)
})
it('只有 rdm.inspect.* 七命令的构建上不就绪', () => {
expect(rdmAuditOperationsReady(enabled(RDM_AUDIT_COMMANDS))).toBe(false)
})
it('能力缺失时不暴露「审计记录」页签,其余页签不受影响', () => {
expect(visibleAuditTabs(false).map(t => t.label)).toEqual(['概览', '资料与文档', '关联追溯', '操作记录'])
expect(visibleAuditTabs(true).map(t => t.label)).toEqual(['概览', '资料与文档', '关联追溯', '操作记录', '审计记录'])
})
})
describe('TraceCoverageView 部分覆盖', () => {
it('候选不计正式覆盖;缺项与待复核提示部分覆盖', () => {
const html = renderToStaticMarkup(<TraceCoverageView pid="p1" data={{
@@ -380,6 +435,26 @@ describe('对话侧:候选差异、确认路径与依据条', () => {
expect(pendingConfirmations(cards, 'v1', 'p2')).toBe(0)
})
it('右栏收起后的确认路径经 openInbox 定位首张待处理卡,与计数同一谓词(含决议开放的工作卡)', () => {
const openInbox = vi.spyOn(store, 'openInbox').mockImplementation(() => {})
try {
const decisionOpen = { resultVersion: 1, readVersion: 1, decision: { mode: 'single' as const, state: 'open' as const } }
const cards = [
card({ id: 'dismissed', status: 'dismissed' }),
card({ id: 'elsewhere', conversationId: 'other' }),
card({ id: 'decision', status: 'active', work: decisionOpen }),
card({ id: 'confirm' }),
]
expect(pendingConfirmations(cards, 'v1', 'p1')).toBe(2)
openPendingConfirmation(cards, 'v1', 'p1')
expect(openInbox).toHaveBeenLastCalledWith('decision')
openPendingConfirmation(cards.filter(c => c.id !== 'decision'), 'v1', 'p1')
expect(openInbox).toHaveBeenLastCalledWith('confirm')
} finally {
openInbox.mockRestore()
}
})
it('依据条列出来源与 token 摘要,不含正文', () => {
const html = renderToStaticMarkup(<BasisStrip obs={{
items: ['项目知识', '对话历史'], estChars: 1800, truncations: ['历史已裁剪'],
+62 -52
View File
@@ -53,6 +53,11 @@ const TABS = [
] as const
type TabId = (typeof TABS)[number]['id']
/** 「审计记录」页签及其覆盖面板、影响分析入口只在 W8b/W5 命令面就绪时暴露(store.rdmAuditOperationsAvailable)。 */
export function visibleAuditTabs(operationsAvailable: boolean) {
return TABS.filter(t => t.id !== 'operations' || operationsAvailable)
}
const OPERATION_FILTERS = [
['', '全部操作'],
['doc.instantiate', '创建文档实例'],
@@ -107,6 +112,8 @@ export function RdmAuditWorkspace() {
const project = caps?.projects.find(p => p.projectId === pid)
const exportOn = useStore(s => s.rdmExportAvailable)
const tabs = visibleAuditTabs(useStore(s => s.rdmAuditOperationsAvailable))
const activeTab: TabId = tabs.some(t => t.id === tab) ? tab : 'summary'
if (error) return <RdmErrorState raw={error} />
if (!caps || capsIdentity !== identity) return <div className="empty-note">加载中…</div>
if (caps.projects.length === 0) return <EmptyState icon="folder">无可见研发项目——项目创建者与域管理员可查看追溯审计</EmptyState>
@@ -123,18 +130,18 @@ export function RdmAuditWorkspace() {
{caps.crossProject && <Chip tone="info">跨项目</Chip>}
</div>
<div className="tabs rdm-audit-tabs" role="tablist">
{TABS.map(t => (
<button key={t.id} role="tab" aria-selected={tab === t.id}
className={`tab${tab === t.id ? ' active' : ''}`}
{tabs.map(t => (
<button key={t.id} role="tab" aria-selected={activeTab === t.id}
className={`tab${activeTab === t.id ? ' active' : ''}`}
onClick={() => setTab(t.id)}>{t.label}</button>
))}
</div>
<div className="rdm-audit-body">
{tab === 'summary' && <SummaryTab pid={pid} exportOn={exportOn} />}
{tab === 'materials' && <MaterialsTab key={pid} pid={pid} />}
{tab === 'trace' && <TraceTab pid={pid} />}
{tab === 'timeline' && <TimelineTab pid={pid} />}
{tab === 'operations' && <OperationsTab key={pid} pid={pid} />}
{activeTab === 'summary' && <SummaryTab pid={pid} exportOn={exportOn} />}
{activeTab === 'materials' && <MaterialsTab key={pid} pid={pid} />}
{activeTab === 'trace' && <TraceTab pid={pid} />}
{activeTab === 'timeline' && <TimelineTab pid={pid} />}
{activeTab === 'operations' && <OperationsTab key={pid} pid={pid} />}
</div>
</div>
)
@@ -1181,32 +1188,33 @@ function EvidenceList({ pid, refs }: { pid: string; refs: RdmOperationRefs }) {
)
}
/** 证据跳转:按需读取对象详情;服务端按当前正文权判定,无权时只给出可读拒绝,不假设有正文。 */
/** 证据跳转:展开时才读取对象详情;服务端按当前正文权判定,无权时只给出可读拒绝,不假设有正文。 */
function EvidenceLink({ pid, evidence }: { pid: string; evidence: RdmOperationRef }) {
const [open, setOpen] = useState(false)
const label = `${evidence.kind.label} ${evidence.id}`
return (
<Collapsible id={`rdm-audit-evidence:${pid}/${evidence.kind.key}/${evidence.id}`}
trigger={<span className="rdm-audit-err-toggle rdm-audit-wrap">{`查看证据:${label}`}</span>}>
<EvidenceDetail pid={pid} evidence={evidence} label={label} />
</Collapsible>
)
}
function EvidenceDetail({ pid, evidence, label }: { pid: string; evidence: RdmOperationRef; label: string }) {
const [detail, setDetail] = useState<ObjectDetail | null>(null)
const [err, setErr] = useState('')
const genRef = useRef(0)
useEffect(() => {
if (!open) return
setDetail(null); setErr('')
return startLatestFetch(genRef,
() => api.rdmCommand<ObjectDetail>('rdm.inspect.object', { projectId: pid, kind: evidence.kind.key, id: evidence.id }),
setDetail, setErr)
}, [open, pid, evidence.kind.key, evidence.id])
const label = `${evidence.kind.label} ${evidence.id}`
return (
<div>
<button type="button" className="btn v-ghost rdm-audit-wrap" aria-expanded={open} onClick={() => setOpen(!open)}>
{open ? `收起证据:${label}` : `查看证据:${label}`}
</button>
{open && (err
? (/权限拒绝|无权|受限/.test(err)
? <div className="rdm-audit-truncated">无权查看该证据(跨项目、无权或已撤权),仅保留元数据:{label}</div>
: <RdmErrorState raw={err} />)
: !detail ? <div className="empty-note">加载中…</div> : <ObjectDetailView detail={detail} />)}
</div>
)
}, [pid, evidence.kind.key, evidence.id])
if (err) {
return /权限拒绝|无权|受限/.test(err)
? <div className="rdm-audit-truncated">无权查看该证据(跨项目、无权或已撤权),仅保留元数据:{label}</div>
: <RdmErrorState raw={err} />
}
return !detail ? <div className="empty-note">加载中…</div> : <ObjectDetailView detail={detail} />
}
const refKey = (r: RdmObjectRef) => `${r.kind}:${r.id}`
@@ -1234,9 +1242,19 @@ function ImpactSnapshotView({ title, a }: { title: string; a: RdmImpactAnalysis
)
}
/** 当时与当前影响:读取不可变快照;重算须显式触发(生成新分析快照,不覆盖历史)。 */
/** 当时与当前影响:展开时才读取不可变快照;重算须显式触发(生成新分析快照,不覆盖历史)。 */
function ImpactAnalysisBlock({ pid, analysisId }: { pid: string; analysisId: string }) {
const [open, setOpen] = useState(false)
return (
<div className="rdm-audit-detail-body">
<Collapsible id={`rdm-audit-impact:${pid}/${analysisId}`}
trigger={<span className="rdm-audit-err-toggle rdm-audit-wrap">{`查看影响分析 ${analysisId}`}</span>}>
<ImpactAnalysisDetail pid={pid} analysisId={analysisId} />
</Collapsible>
</div>
)
}
function ImpactAnalysisDetail({ pid, analysisId }: { pid: string; analysisId: string }) {
const [snapshot, setSnapshot] = useState<RdmImpactAnalysis | null>(null)
const [current, setCurrent] = useState<RdmImpactAnalysis | null>(null)
const [err, setErr] = useState('')
@@ -1244,10 +1262,9 @@ function ImpactAnalysisBlock({ pid, analysisId }: { pid: string; analysisId: str
const snapRef = useRef(0)
const currentRef = useRef(0)
useEffect(() => {
if (!open) return
setSnapshot(null); setErr('')
return startLatestFetch(snapRef, () => api.rdmImpactGet(pid, analysisId), setSnapshot, setErr)
}, [open, pid, analysisId])
}, [pid, analysisId])
useEffect(() => () => { currentRef.current++ }, [pid, analysisId])
const revision = Number(snapshot?.start.revision)
@@ -1262,30 +1279,23 @@ function ImpactAnalysisBlock({ pid, analysisId }: { pid: string; analysisId: str
const diff = snapshot && current ? diffImpact(snapshot, current) : null
return (
<div className="rdm-audit-detail-body">
<button type="button" className="btn v-ghost rdm-audit-wrap" aria-expanded={open} onClick={() => setOpen(!open)}>
{open ? '收起影响分析' : `查看影响分析 ${analysisId}`}
</button>
{open && (
<>
{err && <RdmErrorState raw={err} />}
{!snapshot && !err && <div className="empty-note">加载中…</div>}
{snapshot && <ImpactSnapshotView title="当时影响(不可变快照)" a={snapshot} />}
{snapshot && revision > 0 && (
<button type="button" className="btn v-ghost" disabled={busy} onClick={recompute}>
{busy ? '正在重算…' : '按当前受控图重算(生成新快照,不覆盖历史)'}
</button>
)}
{current && <ImpactSnapshotView title="当前影响" a={current} />}
{diff && (
<div className="rdm-audit-detail-body rdm-audit-wrap">
<div>当前新增:{diff.added.length ? diff.added.map(refLabel).join('、') : '无'}</div>
<div>当前不再涉及:{diff.removed.length ? diff.removed.map(refLabel).join('、') : '无'}</div>
</div>
)}
</>
<>
{err && <RdmErrorState raw={err} />}
{!snapshot && !err && <div className="empty-note">加载中…</div>}
{snapshot && <ImpactSnapshotView title="当时影响(不可变快照)" a={snapshot} />}
{snapshot && revision > 0 && (
<button type="button" className="btn v-ghost" disabled={busy} onClick={recompute}>
{busy ? '正在重算…' : '按当前受控图重算(生成新快照,不覆盖历史)'}
</button>
)}
</div>
{current && <ImpactSnapshotView title="当前影响" a={current} />}
{diff && (
<div className="rdm-audit-detail-body rdm-audit-wrap">
<div>当前新增:{diff.added.length ? diff.added.map(refLabel).join('、') : '无'}</div>
<div>当前不再涉及:{diff.removed.length ? diff.removed.map(refLabel).join('、') : '无'}</div>
</div>
)}
</>
)
}
+16 -5
View File
@@ -34,10 +34,21 @@ const scratchInboxCallbacks = {
}
const inboxCallbacks = { ...scratchInboxCallbacks, onArchive: (c: ActivityCard) => store.cardToProject(c) }
/** 本会话仍待用户处理的卡片数:右栏收起后由对话承接确认路径。 */
export function pendingConfirmations(cards: ActivityCard[], conversationId: string | null | undefined, projectId: string | null | undefined): number {
return cards.filter(card => card.status !== 'dismissed' && card.conversationId === conversationId && card.projectId === projectId
&& (card.status === 'action_required' || card.work?.decision?.state === 'open')).length
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 不聚合)。 */
@@ -79,7 +90,7 @@ export function Workbench() {
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 collapseHint = pendingHere > 0 ? `收起右栏(${pendingHere} 项待确认可从对话输入区的提示回到工作栏处理)` : '收起右栏'
// 离开文件入口时收回全屏展开,避免出现「工作」铺满主区的怪态。
const switchTab = (tab: 'work' | 'files') =>
store.setUI({ rightTab: tab, previewExpanded: tab === 'files' ? store.getState().ui.previewExpanded : false })
+9 -3
View File
@@ -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'
@@ -148,6 +148,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
@@ -227,6 +229,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: [],
@@ -659,7 +662,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 })
@@ -1466,6 +1470,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),
@@ -1474,7 +1479,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 {
+15
View File
@@ -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',