feat(governance): surface km completion state in details
All checks were successful
CD Pipeline / tests (push) Successful in 1m10s
Code Review / ai-code-review (push) Successful in 11s
CD Pipeline / build-and-deploy (push) Successful in 4m12s
CD Pipeline / post-deploy-checks (push) Successful in 1m49s

This commit is contained in:
Your Name
2026-05-24 23:31:16 +08:00
parent ede2b3752b
commit ac4686615f
5 changed files with 180 additions and 2 deletions

View File

@@ -433,6 +433,114 @@ def _format_awooop_status_chain_lines(
return lines
def _format_km_stale_completion_lines(summary: dict[str, object] | None) -> list[str]:
"""Render KM owner-review completion state for Telegram detail/history replies."""
if not summary:
return []
status = str(summary.get("status") or "ok")
if status == "fetch_failed":
return [
"",
"📚 <b>KM Owner Review</b>",
"狀態: <code>fetch_failed</code>",
"下一步: <code>open_awooop_work_items</code>",
]
items = summary.get("items") if isinstance(summary.get("items"), list) else []
first_item = items[0] if items and isinstance(items[0], dict) else None
lines = [
"",
"📚 <b>KM Owner Review</b>",
(
"Completion queue: "
f"ready <code>{_safe_int(summary.get('ready_count'))}</code> / "
f"blocked <code>{_safe_int(summary.get('blocked_count'))}</code> / "
f"completed <code>{_safe_int(summary.get('completed_count'))}</code> / "
f"failed <code>{_safe_int(summary.get('failed_count'))}</code>"
),
(
"Guardrail: "
f"read-only <code>{html.escape(_bool_code(not bool(summary.get('writes_on_read'))))}</code> / "
f"batch-write <code>{html.escape(_bool_code(summary.get('batch_writes_allowed')))}</code>"
),
]
if first_item:
lines += [
(
"此事件: "
f"<code>{html.escape(str(first_item.get('entry_id') or '--'))}</code> / "
f"<code>{html.escape(str(first_item.get('readiness') or '--'))}</code>"
),
(
"下一步: "
f"<code>{html.escape(str(first_item.get('next_action') or '--'))}</code>"
),
]
else:
lines.append("此事件: <code>no_related_owner_review</code>")
lines.append("下一步: <code>review_awooop_completion_queue</code>")
return lines
async def _fetch_km_stale_completion_summary_for_incident(
*,
incident_id: str,
project_id: str,
) -> dict[str, object] | None:
"""Fetch read-only completion queue state related to one incident."""
if not incident_id:
return None
try:
from src.services.governance_km_stale_review_service import (
query_km_stale_owner_review_completion_queue,
)
queue = await asyncio.wait_for(
query_km_stale_owner_review_completion_queue(
project_id=project_id or "awoooi",
status_bucket="all",
limit=100,
),
timeout=2.5,
)
items = [
item.model_dump(mode="json")
for item in queue.items
if item.related_incident_id == incident_id
]
return {
"schema_version": "km_stale_owner_review_completion_telegram_summary_v1",
"status": "ok",
"project_id": project_id or "awoooi",
"incident_id": incident_id,
"total": queue.total,
"returned": queue.returned,
"pending_count": queue.pending_count,
"ready_count": queue.ready_count,
"blocked_count": queue.blocked_count,
"completed_count": queue.completed_count,
"failed_count": queue.failed_count,
"writes_on_read": queue.writes_on_read,
"batch_writes_allowed": queue.batch_writes_allowed,
"manual_review_required": queue.manual_review_required,
"items": items[:5],
}
except Exception as exc:
logger.debug(
"telegram_km_stale_completion_summary_fetch_failed",
incident_id=incident_id,
project_id=project_id,
error=str(exc),
)
return {
"schema_version": "km_stale_owner_review_completion_telegram_summary_v1",
"status": "fetch_failed",
"project_id": project_id or "awoooi",
"incident_id": incident_id,
"items": [],
}
async def _fetch_remediation_summary_for_card(
*,
approval_id: str,
@@ -5855,17 +5963,23 @@ class TelegramGateway:
error=str(remediation_exc),
)
project_id = getattr(incident, "project_id", None) or "awoooi"
km_completion_summary = await _fetch_km_stale_completion_summary_for_incident(
incident_id=incident_id,
project_id=project_id,
)
try:
from src.services.awooop_truth_chain_service import fetch_truth_chain
truth_chain = await fetch_truth_chain(
source_id=incident_id,
project_id=getattr(incident, "project_id", None) or "awoooi",
project_id=project_id,
)
lines += _format_awooop_status_chain_lines(
truth_chain=truth_chain,
remediation_history=remediation_history,
)
lines += _format_km_stale_completion_lines(km_completion_summary)
lines += _format_remediation_history_lines(remediation_history)
gateway_summary = (
(truth_chain.get("mcp") or {})
@@ -5884,6 +5998,7 @@ class TelegramGateway:
lines += _format_awooop_status_chain_lines(
remediation_history=remediation_history,
)
lines += _format_km_stale_completion_lines(km_completion_summary)
lines += _format_remediation_history_lines(remediation_history)
await self._send_html_line_message(
@@ -6005,17 +6120,23 @@ class TelegramGateway:
error=str(remediation_exc),
)
project_id = getattr(incident, "project_id", None) or "awoooi"
km_completion_summary = await _fetch_km_stale_completion_summary_for_incident(
incident_id=incident_id,
project_id=project_id,
)
try:
from src.services.awooop_truth_chain_service import fetch_truth_chain
truth_chain = await fetch_truth_chain(
source_id=incident_id,
project_id=getattr(incident, "project_id", None) or "awoooi",
project_id=project_id,
)
lines += _format_awooop_status_chain_lines(
truth_chain=truth_chain,
remediation_history=remediation_history,
)
lines += _format_km_stale_completion_lines(km_completion_summary)
lines += _format_remediation_history_lines(remediation_history)
lines += _format_automation_quality_lines(
truth_chain.get("automation_quality")
@@ -6029,6 +6150,7 @@ class TelegramGateway:
lines += _format_awooop_status_chain_lines(
remediation_history=remediation_history,
)
lines += _format_km_stale_completion_lines(km_completion_summary)
lines += _format_remediation_history_lines(remediation_history)
await self._send_html_line_message(

View File

@@ -177,6 +177,35 @@ def test_awooop_status_chain_lines_show_read_only_manual_gate() -> None:
assert "pending_human_approval" in joined
def test_km_stale_completion_lines_show_owner_review_queue_state() -> None:
"""詳情/歷史要顯示 KM owner-review completion queue 是否卡住或可處理。"""
lines = telegram_gateway_module._format_km_stale_completion_lines({
"schema_version": "km_stale_owner_review_completion_telegram_summary_v1",
"status": "ok",
"incident_id": "INC-20260513-205814",
"ready_count": 10,
"blocked_count": 0,
"completed_count": 1,
"failed_count": 0,
"writes_on_read": False,
"batch_writes_allowed": False,
"items": [
{
"entry_id": "bf81a30d-6abe-4c0c-b4ba-9c0ba0d761bf",
"readiness": "ready",
"next_action": "preview_stale_km_review_completion",
}
],
})
joined = "\n".join(lines)
assert "KM Owner Review" in joined
assert "ready <code>10</code>" in joined
assert "batch-write <code>no</code>" in joined
assert "bf81a30d-6abe-4c0c-b4ba-9c0ba0d761bf" in joined
assert "preview_stale_km_review_completion" in joined
def test_awooop_runs_url_for_incident_uses_public_incident_filter() -> None:
"""Telegram URL button 必須導到公開 AwoooP Run list並帶 incident filter。"""
url = telegram_gateway_module._awooop_runs_url_for_incident("INC-20260514-F85F21")

View File

@@ -2022,6 +2022,8 @@
"knowledgeNext": "Next action: {action}",
"knowledgeDrafts": "KM review drafts: {drafts}; duplicate drafts: {duplicates}",
"knowledgeStaleCandidates": "Stale KM priority queue: {total}; top {top} / {tier}",
"knowledgeCompletionQueue": "Completion queue: ready {ready}; blocked {blocked}; completed {completed}; failed {failed}",
"knowledgeCompletionLatest": "Latest completion: {entry} / {readiness}; next {next}",
"knowledgeEmpty": "No recent knowledge_degradation dispatch trail",
"frontendConsole": "This page now reads production APIs instead of a static list",
"mcpReady": "MCP Gateway gate is not currently a top gap",

View File

@@ -2023,6 +2023,8 @@
"knowledgeNext": "下一步:{action}",
"knowledgeDrafts": "KM 審核草稿:{drafts};重複草稿:{duplicates}",
"knowledgeStaleCandidates": "陳舊 KM 優先清單:{total} 筆;最高 {top} / {tier}",
"knowledgeCompletionQueue": "Completion queue可處理 {ready};卡住 {blocked};完成 {completed};失敗 {failed}",
"knowledgeCompletionLatest": "最新 completion{entry} / {readiness};下一步 {next}",
"knowledgeEmpty": "近期沒有 knowledge_degradation dispatch trail",
"frontendConsole": "本頁已改讀 production API而非靜態清單",
"mcpReady": "MCP Gateway gate 目前未列為主要缺口",

View File

@@ -1513,6 +1513,8 @@ function buildWorkItems(
telemetry.knowledgeReviewDedupe?.duplicate_draft_total ?? knowledgeDuplicateDrafts;
const knowledgeStaleTotal = telemetry.knowledgeStaleCandidates?.total_stale ?? 0;
const topKnowledgeStaleCandidate = telemetry.knowledgeStaleCandidates?.items?.[0] ?? null;
const knowledgeCompletionQueue = telemetry.knowledgeStaleOwnerReviewCompletionQueue;
const latestKnowledgeCompletion = knowledgeCompletionQueue?.items?.[0] ?? null;
const remediationQueue = telemetry.slo?.adr100?.verification_coverage?.remediation_queue;
const remediationTotal = remediationQueue?.total ?? 0;
const remediationReadyForAi = remediationQueue?.ready_for_ai ?? 0;
@@ -1766,6 +1768,21 @@ function buildWorkItems(
top: topKnowledgeStaleCandidate?.entry_id ?? "--",
tier: topKnowledgeStaleCandidate?.priority_tier ?? "--",
}),
t("evidence.knowledgeCompletionQueue", {
ready: knowledgeCompletionQueue?.ready_count ?? 0,
blocked: knowledgeCompletionQueue?.blocked_count ?? 0,
completed: knowledgeCompletionQueue?.completed_count ?? 0,
failed: knowledgeCompletionQueue?.failed_count ?? 0,
}),
t("evidence.knowledgeCompletionLatest", {
entry: latestKnowledgeCompletion?.entry_id ?? "--",
readiness: latestKnowledgeCompletion
? t(
`knowledgeGovernance.staleCandidates.completionQueue.readiness.${kmStaleCompletionReadinessKey(latestKnowledgeCompletion.readiness)}` as never
)
: "--",
next: latestKnowledgeCompletion?.next_action ?? "--",
}),
]
: [
t("evidence.knowledgeEmpty"),
@@ -1774,6 +1791,12 @@ function buildWorkItems(
top: topKnowledgeStaleCandidate?.entry_id ?? "--",
tier: topKnowledgeStaleCandidate?.priority_tier ?? "--",
}),
t("evidence.knowledgeCompletionQueue", {
ready: knowledgeCompletionQueue?.ready_count ?? 0,
blocked: knowledgeCompletionQueue?.blocked_count ?? 0,
completed: knowledgeCompletionQueue?.completed_count ?? 0,
failed: knowledgeCompletionQueue?.failed_count ?? 0,
}),
],
href: "/awooop/work-items",
},