Files
Agent-System-Frontend/src/rdmAuditPresentation.ts
weichen 95e1948a5f Distinguish retired and stale trace relations
Why: Retired edges and compliance edges awaiting review both fell back to a generic lifecycle label.

Evidence: Presentation tests, production build, knip, and diff checks pass.

Impact: The trace network now shows 已停用 and 待复核 as distinct human-readable states.

Refs: readable trace network
2026-10-15 06:52:42 +00:00

185 lines
7.1 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
export interface AuditEventViewInput {
seq: number
at: string
op: string
actor: string
source: string
target: { kind: string; id: string; title?: string }
detail?: string
operationId: string
disclosure: string
summary?: string
result?: string
reason?: string
impact?: string
}
interface AuditEventPresentation {
action: string
subject: string
object: string
outcome: string
reason: string
impact: string
source: string
}
const KIND_LABELS: Record<string, string> = {
doc: '文档', section: '章节', node: '节点', note: '知识', slice: '资料切片', spine: '知识主题', edge: '追溯边',
paragraph: '段落', table: '表格', row: '表格行', cell: '表格单元格',
field: '填写字段', attachment: '附件',
requirement: '要求', risk: '风险', proposal: '变更提案', relation: '追溯关系',
template: '模板', project: '项目', conclusion: '结论', issue: '问题',
}
const STATE_LABELS: Record<string, string> = {
active: '已生效', candidate: '待确认', pending: '待处理', baselined: '已建立基线',
retired: '已停用', stale: '待复核',
approved: '已批准', rejected: '已拒绝', revoked: '已撤销', superseded: '已被取代',
failed: '失败', committed: '已生效', accepted: '已接受', cancelled: '已取消',
delivered: '已记录',
unknown: '结果未知', restricted: '部分信息受限',
}
const OP_LABELS: Record<string, string> = {
'doc.instantiate': '创建文档实例',
'section.add': '新增文档章节',
'section.version': '更新文档内容',
'section.state': '变更章节状态',
'section.tombstone': '停用文档章节',
'slice.ingest': '新增项目资料',
'slice.retag': '更新资料标签',
'slice.tombstone': '停用项目资料',
'spine.add': '新增知识主题',
'mount.add': '挂接项目资料',
'mount.move': '移动项目资料',
'mount.remove': '移除资料挂接',
'edge.candidate': '提出追溯关系候选',
'edge.confirm': '确认追溯关系',
'edge.retire': '停用追溯关系',
'change.requested': '提出变更申请',
'change.approved': '批准变更申请',
'change.applied': '应用变更',
'open-item.add': '登记项目问题',
'open-item.close': '关闭项目问题',
'dictionary.revise': '修订项目字典',
'req.change': '更新项目要求',
}
const RELATION_LABELS: Record<string, string> = {
verifies: '验证', supports: '支持', derives: '派生自', impacts: '影响',
traces: '追溯到', replaces: '取代', conflicts: '存在冲突', references: '引用',
}
const LAYER_LABELS: Record<string, string> = {
compliance: '合规关系', knowledge: '知识关系', document: '文档关系', project: '项目关系',
}
export function humanizeKind(kind: string): string {
return KIND_LABELS[kind] || '项目对象'
}
export function describeTreeItem(item: {
kind: string
id: string
title?: string
order?: number
templateBlockId?: string
}): { kind: string; title: string } {
const title = item.title?.trim()
if (title) return { kind: humanizeKind(item.kind), title }
const position = typeof item.order === 'number' ? item.order + 1 : undefined
const parts = (item.templateBlockId || '').split('.').filter(Boolean)
const semantic = new Set(['placeholder', 'example', 'fixed'])
while (parts.length > 0 && semantic.has(parts[parts.length - 1])) parts.pop()
const hint = parts.at(-1)
const numbered = position ? `模板内容 ${position}` : '未命名模板内容'
return {
kind: humanizeKind(item.kind),
title: hint ? `${numbered}(${hint})` : numbered,
}
}
export function humanizeState(state: string): string {
return STATE_LABELS[state] || '状态已记录'
}
function humanizeOperation(op: string): string {
if (OP_LABELS[op]) return OP_LABELS[op]
if (op.startsWith('doc.')) return '更新文档内容'
if (op.startsWith('section.')) return '更新文档内容'
if (op.startsWith('slice.') || op.startsWith('spine.') || op.startsWith('mount.') || op.startsWith('knowledge.')) return '更新项目知识'
if (op.startsWith('edge.') || op.startsWith('relation.') || op.startsWith('trace.')) return '更新追溯关系'
if (op.startsWith('change.')) return '处理项目变更'
if (op.startsWith('open-item.')) return '处理项目问题'
if (op.startsWith('risk.')) return '处理项目风险'
if (op.startsWith('proposal.')) return '处理结构变更'
return '更新项目内容'
}
function humanizeActor(actor: string): string {
if (!actor) return '未知成员'
if (/^[a-z][a-z0-9._-]*$/i.test(actor) && !actor.includes('-') && !actor.includes('_')) {
return actor.charAt(0).toUpperCase() + actor.slice(1)
}
return actor
}
function humanizeSource(source: string): string {
if (!source) return '当时未记录来源'
const template = /^template:([^@]+)@(.+)$/.exec(source)
if (template) {
const title = template[1] === 'dhf-proposal' ? 'DHF 项目建议书' : template[1]
return `模板:${title}(版本 ${template[2]})`
}
if (source === 'host') return '系统受信操作记录'
if (source === 'human') return '项目成员操作'
if (source === 'rule') return '项目规则推导'
if (source === 'model') return '模型分析候选'
if (source.startsWith('message:')) return '项目对话'
if (source.startsWith('artifact:')) return '项目文件'
return '已登记项目来源'
}
function humanizeOutcome(input: AuditEventViewInput): string {
if (input.disclosure !== 'ok') return input.disclosure === 'model-candidate' ? '模型候选,尚未确认' : '部分信息受限'
return humanizeState(input.result || 'committed')
}
export function describeAuditEvent(input: AuditEventViewInput): AuditEventPresentation {
const targetTitle = input.target?.title ? `“${input.target.title}”` : ''
const targetIdentity = targetTitle || (input.target?.id ? `(${input.target.id})` : '')
return {
action: input.summary || humanizeOperation(input.op),
subject: humanizeActor(input.actor),
object: `${humanizeKind(input.target?.kind)}${targetIdentity}`,
outcome: humanizeOutcome(input),
reason: input.reason || '未记录具体原因',
impact: input.impact || '影响范围尚未记录',
source: humanizeSource(input.source),
}
}
export function humanizeRelation(input: { layer?: string; type?: string; kind?: string; state?: string }) {
const type = input.type || input.kind || ''
return {
layer: LAYER_LABELS[input.layer || ''] || '项目关系',
type: RELATION_LABELS[type] || '关联',
state: humanizeState(input.state || ''),
}
}
export function relationOtherEndpoint<T extends { kind?: string; id?: string }>(
relation: { from?: T; to?: T },
selected: { kind: string; id: string },
): T | undefined {
if (relation.to?.kind === selected.kind && relation.to.id === selected.id) return relation.from
if (relation.from?.kind === selected.kind && relation.from.id === selected.id) return relation.to
return relation.from || relation.to
}
export function isTreeItemExpandable(item: { kind: string; hasChildren?: boolean; id?: string; title?: string }): boolean {
return item.kind === 'spine' || item.hasChildren === true
}