Why: Project members need to understand who changed what, why, and with what impact without reading operation codes or raw audit payloads. Evidence: A RED presentation test first failed on the missing humanization layer; 276 frontend tests passed, design guard 10 checks passed, knip stayed at baseline 43, and npm run build passed. Impact: Humanize tree, relation, state, and operation vocabulary; render structured reason, impact, source, and outcome fields; raw event detail is no longer shown in the audit timeline. Refs: TT-U01, TT-U02, TT-U03, TT-U04; two trees one graph stage-five plan
135 lines
4.8 KiB
TypeScript
135 lines
4.8 KiB
TypeScript
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: '资料切片',
|
||
requirement: '要求', risk: '风险', proposal: '变更提案', relation: '追溯关系',
|
||
template: '模板', project: '项目', conclusion: '结论', issue: '问题',
|
||
}
|
||
|
||
const STATE_LABELS: Record<string, string> = {
|
||
active: '已生效', candidate: '待确认', pending: '待处理', baselined: '已建立基线',
|
||
approved: '已批准', rejected: '已拒绝', revoked: '已撤销', superseded: '已被取代',
|
||
failed: '失败', committed: '已生效', accepted: '已接受', cancelled: '已取消',
|
||
unknown: '结果未知', restricted: '部分信息受限',
|
||
}
|
||
|
||
const OP_LABELS: Record<string, string> = {
|
||
'doc.instantiate': '创建文档实例',
|
||
'doc.section.add': '新增文档章节',
|
||
'doc.section.update': '更新文档章节',
|
||
'doc.node.add': '新增文档节点',
|
||
'doc.node.update': '更新文档节点',
|
||
'doc.approve': '批准文档版本',
|
||
'note.add': '新增项目知识',
|
||
'note.update': '更新项目知识',
|
||
'relation.add': '建立追溯关系',
|
||
'relation.invalidate': '标记关系待复核',
|
||
'proposal.create': '提出结构变更',
|
||
'proposal.apply': '应用结构变更',
|
||
'proposal.reject': '拒绝结构变更',
|
||
'risk.create': '发现项目风险',
|
||
'risk.answer': '回答风险澄清',
|
||
'risk.resolve': '完成风险处理',
|
||
'req.add': '新增项目要求',
|
||
'req.update': '更新项目要求',
|
||
}
|
||
|
||
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 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('note.') || op.startsWith('knowledge.')) return '更新项目知识'
|
||
if (op.startsWith('relation.') || op.startsWith('trace.')) 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.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}”` : ''
|
||
return {
|
||
action: input.summary || humanizeOperation(input.op),
|
||
subject: humanizeActor(input.actor),
|
||
object: `${humanizeKind(input.target?.kind)}${targetTitle}`,
|
||
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 || ''),
|
||
}
|
||
}
|