feat(governance): surface km completion state in details
This commit is contained in:
@@ -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(
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 目前未列為主要缺口",
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user