feat(governance): 顯示任務批准邊界與進度彙總
All checks were successful
CD Pipeline / tests (push) Successful in 1m31s
Code Review / ai-code-review (push) Successful in 14s
CD Pipeline / build-and-deploy (push) Successful in 5m12s
CD Pipeline / post-deploy-checks (push) Successful in 2m21s

This commit is contained in:
Your Name
2026-06-05 09:23:41 +08:00
parent b133f76af5
commit 4f0787f869
17 changed files with 2368 additions and 105 deletions

View File

@@ -357,6 +357,10 @@ export function AutomationInventoryTab() {
const actionRequiredOffsiteCards = offsiteEscrow.rollups.by_readiness.action_required ?? 0
const blockedEscrowCards = offsiteEscrow.rollups.by_readiness.blocked ?? 0
const executionBlockedCards = offsiteEscrow.rollups.execution_blocked_card_ids.length
const backlogProgressPercent = backlog.progress_summary.overall_percent
const explicitApprovalItemCount = backlog.item_approval_boundary_rollup.items_requiring_explicit_approval.length
const taskBoundaryCount = snapshot.task_approval_boundary_rollup.total_tasks
const explicitApprovalTaskCount = snapshot.task_approval_boundary_rollup.tasks_requiring_explicit_approval.length
const blockedApprovals = Object.entries(snapshot.approval_boundaries)
.filter(([, allowed]) => allowed === false)
.map(([key]) => key)
@@ -377,6 +381,14 @@ export function AutomationInventoryTab() {
}
}
const boundaryActionLabel = (value: string) => {
try {
return t(`tasks.boundaryActions.${value}` as never)
} catch {
return value
}
}
return (
<div style={{ padding: 20, display: 'flex', flexDirection: 'column', gap: 16 }}>
<GlassCard variant="subtle" padding="md">
@@ -427,13 +439,65 @@ export function AutomationInventoryTab() {
gap: 12,
}} className="automation-inventory-kpi-grid">
<MetricCard label={t('metrics.progress')} value={`${snapshot.program_status.overall_completion_percent}%`} tone="ok" icon={<ShieldCheck size={16} />} />
<MetricCard label={t('metrics.backlogProgress')} value={`${backlogProgressPercent}%`} tone="warn" icon={<PackageCheck size={16} />} />
<MetricCard label={t('metrics.assets')} value={snapshot.assets.length} icon={<Boxes size={16} />} />
<MetricCard label={t('metrics.backlog')} value={backlog.rollups.total_items} tone="warn" icon={<PackageCheck size={16} />} />
<MetricCard label={t('metrics.p1Backlog')} value={p1BacklogCount} icon={<Boxes size={16} />} />
<MetricCard label={t('metrics.blocked')} value={blockedAssets} tone={blockedAssets > 0 ? 'warn' : 'ok'} icon={<AlertTriangle size={16} />} />
<MetricCard label={t('metrics.critical')} value={criticalAssets} tone="danger" icon={<Lock size={16} />} />
<MetricCard label={t('metrics.taskBoundaries')} value={taskBoundaryCount} icon={<Lock size={16} />} />
<MetricCard label={t('metrics.explicitApprovalTasks')} value={explicitApprovalTaskCount} tone={explicitApprovalTaskCount > 0 ? 'warn' : 'ok'} icon={<ShieldCheck size={16} />} />
</div>
<GlassCard variant="subtle" padding="md">
<div style={{ display: 'flex', flexDirection: 'column', gap: 13, minWidth: 0 }}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 12, flexWrap: 'wrap' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 7, minWidth: 0 }}>
<PackageCheck size={14} style={{ color: '#d97757' }} />
<span style={{ fontFamily: 'Syne, sans-serif', fontSize: 13, fontWeight: 700, color: '#141413' }}>
{t('backlogProgress.title')}
</span>
</div>
<div style={{ fontFamily: "'DM Mono', monospace", fontSize: 10, color: '#87867f', overflowWrap: 'anywhere' }}>
{backlog.progress_summary.formula}
</div>
</div>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, minmax(0, 1fr))', gap: 12 }} className="automation-inventory-progress-kpi-grid">
<MetricCard label={t('backlogProgress.metrics.overall')} value={`${backlog.progress_summary.overall_percent}%`} tone="ok" icon={<ShieldCheck size={16} />} />
<MetricCard label={t('backlogProgress.metrics.done')} value={`${backlog.progress_summary.done_items}/${backlog.progress_summary.total_items}`} icon={<PackageCheck size={16} />} />
<MetricCard label={t('backlogProgress.metrics.planned')} value={backlog.progress_summary.planned_items} tone={backlog.progress_summary.planned_items > 0 ? 'warn' : 'ok'} icon={<AlertTriangle size={16} />} />
<MetricCard label={t('backlogProgress.metrics.requiresApproval')} value={explicitApprovalItemCount} tone={explicitApprovalItemCount > 0 ? 'warn' : 'ok'} icon={<Lock size={16} />} />
</div>
<div style={{ display: 'grid', gridTemplateColumns: 'minmax(0, 0.8fr) minmax(0, 1.2fr)', gap: 12 }} className="automation-inventory-progress-grid">
<div style={{ padding: 12, border: '0.5px solid #e0ddd4', borderRadius: 7, background: '#fff', display: 'flex', flexDirection: 'column', gap: 10, minWidth: 0 }}>
<span style={{ fontFamily: 'Syne, sans-serif', fontSize: 13, fontWeight: 700, color: '#141413' }}>
{t('backlogProgress.priorityTitle')}
</span>
{backlog.progress_summary.by_priority.map(row => (
<ProgressRow
key={row.priority}
label={`${row.priority} · ${row.done_items}/${row.total_items}`}
percent={row.completion_percent}
nextTask={t('backlogProgress.doneLabel')}
/>
))}
</div>
<div style={{ padding: 12, border: '0.5px solid #e0ddd4', borderRadius: 7, background: '#fff', display: 'grid', gridTemplateColumns: 'repeat(2, minmax(0, 1fr))', gap: 10, minWidth: 0 }} className="automation-inventory-progress-workstream-grid">
{backlog.progress_summary.by_workstream.slice(0, 6).map(row => (
<ProgressRow
key={row.workstream_id}
label={`${row.display_name} · ${row.done_items}/${row.total_items}`}
percent={row.completion_percent}
nextTask={row.next_task_id}
/>
))}
</div>
</div>
</div>
</GlassCard>
<GlassCard variant="subtle" padding="md">
<div style={{ display: 'flex', flexDirection: 'column', gap: 13, minWidth: 0 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 7 }}>
@@ -504,6 +568,23 @@ export function AutomationInventoryTab() {
}}>
{item.acceptance_criteria[0]}
</div>
<div style={{
fontFamily: "'DM Mono', monospace",
fontSize: 10,
color: '#87867f',
lineHeight: 1.45,
overflowWrap: 'anywhere',
}}>
{item.approval_boundary.display_summary}
</div>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 6, minWidth: 0 }}>
<Chip value={`${t('tasks.boundaryLabels.mode')}: ${statusLabel(item.approval_boundary.mode)}`} muted />
<Chip value={`${t('tasks.boundaryLabels.allowed')}: ${item.approval_boundary.allowed_actions[0]}`} muted />
<Chip value={`${t('tasks.boundaryLabels.blocked')}: ${item.approval_boundary.blocked_actions.slice(0, 3).map(boundaryActionLabel).join(' / ')}`} />
{item.approval_boundary.requires_operator_approval_for.length > 0 ? (
<Chip value={`${t('tasks.boundaryLabels.requiresApproval')}: ${item.approval_boundary.requires_operator_approval_for[0]}`} muted />
) : null}
</div>
</div>
))}
{group.items.length > 5 ? (
@@ -786,6 +867,25 @@ export function AutomationInventoryTab() {
{task.title}
</span>
<ProgressRow label={task.owner_agent} percent={task.completion_percent} nextTask={task.gate_status} />
<div style={{ display: 'flex', flexDirection: 'column', gap: 7, minWidth: 0 }}>
<div style={{
fontFamily: "'DM Mono', monospace",
fontSize: 10,
color: '#87867f',
lineHeight: 1.45,
overflowWrap: 'anywhere',
}}>
{task.approval_boundary.display_summary}
</div>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 6, minWidth: 0 }}>
<Chip value={`${t('tasks.boundaryLabels.mode')}: ${statusLabel(task.approval_boundary.mode)}`} muted />
<Chip value={`${t('tasks.boundaryLabels.allowed')}: ${task.approval_boundary.allowed_actions[0]}`} muted />
<Chip value={`${t('tasks.boundaryLabels.blocked')}: ${task.approval_boundary.blocked_actions.slice(0, 3).map(boundaryActionLabel).join(' / ')}`} />
{task.approval_boundary.requires_operator_approval_for.length > 0 ? (
<Chip value={`${t('tasks.boundaryLabels.requiresApproval')}: ${task.approval_boundary.requires_operator_approval_for[0]}`} muted />
) : null}
</div>
</div>
</div>
</div>
))}
@@ -813,6 +913,9 @@ export function AutomationInventoryTab() {
<style jsx global>{`
@media (max-width: 900px) {
.automation-inventory-kpi-grid,
.automation-inventory-progress-kpi-grid,
.automation-inventory-progress-grid,
.automation-inventory-progress-workstream-grid,
.automation-inventory-workstream-grid,
.automation-inventory-domain-grid,
.automation-inventory-backlog-grid,

View File

@@ -679,8 +679,21 @@ export interface AiAgentAutomationInventorySnapshot {
title: string
output: string
gate_status: string
approval_boundary: {
mode: string
display_summary: string
allowed_actions: string[]
blocked_actions: string[]
requires_operator_approval_for: string[]
}
next_action: string
}>
task_approval_boundary_rollup: {
total_tasks: number
by_mode: Record<string, number>
tasks_requiring_explicit_approval: string[]
tasks_with_blocked_operations: string[]
}
evidence: Array<{
evidence_id: string
kind: 'schema' | 'test' | 'browser' | 'api' | 'build' | 'doc' | 'runtime'
@@ -715,6 +728,27 @@ export interface AiAgentAutomationBacklogSnapshot {
by_gate_status: Record<string, number>
by_owner_agent: Record<string, number>
}
progress_summary: {
overall_percent: number
done_items: number
planned_items: number
total_items: number
formula: string
by_priority: Array<{
priority: 'P0' | 'P1' | 'P2' | 'P3'
completion_percent: number
done_items: number
total_items: number
}>
by_workstream: Array<{
workstream_id: string
display_name: string
completion_percent: number
done_items: number
total_items: number
next_task_id: string
}>
}
backlog_items: Array<{
item_id: string
priority: 'P0' | 'P1' | 'P2' | 'P3'
@@ -730,8 +764,21 @@ export interface AiAgentAutomationBacklogSnapshot {
risk_level: 'low' | 'medium' | 'high' | 'critical'
evidence_refs: string[]
acceptance_criteria: string[]
approval_boundary: {
mode: string
display_summary: string
allowed_actions: string[]
blocked_actions: string[]
requires_operator_approval_for: string[]
}
next_review: string
}>
item_approval_boundary_rollup: {
total_items: number
by_mode: Record<string, number>
items_requiring_explicit_approval: string[]
items_with_blocked_operations: string[]
}
approval_boundaries: Record<
| 'sdk_installation_allowed'
| 'paid_api_call_allowed'