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
185 lines
7.1 KiB
TypeScript
185 lines
7.1 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: '资料切片', 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
|
||
}
|