diff --git a/web/hwlab-cloud-web/app.mjs b/web/hwlab-cloud-web/app.mjs index 68c852ad..130c0068 100644 --- a/web/hwlab-cloud-web/app.mjs +++ b/web/hwlab-cloud-web/app.mjs @@ -847,6 +847,7 @@ function normalizePort(port) { } function trustedM3LiveEvidenceFrom(summary) { + if (!isDurableRuntimeReady(summary)) return null; const candidates = [ summary?.m3TrustedLoop, summary?.m3HardwareLoop, @@ -858,6 +859,28 @@ function trustedM3LiveEvidenceFrom(summary) { return candidates.find(hasTrustedM3LiveEvidence) ?? null; } +function liveRecordTone(record, live) { + const runtimeSummary = runtimeSummaryFrom(live); + return isDurableRuntimeReady(runtimeSummary) && hasTrustedM3LiveEvidence(record) ? "dev-live" : "blocked"; +} + +function liveRecordDurabilityBlocker(live) { + const runtimeSummary = runtimeSummaryFrom(live); + if (isDurableRuntimeReady(runtimeSummary)) return null; + if (runtimeSummary?.durable !== true) { + return "runtime.durable=false;只读 audit/evidence 不能标为 durable ready 或 DEV-LIVE。"; + } + return `runtime durable adapter 未 ready(status=${runtimeSummary?.status ?? "unknown"});可信记录保持 BLOCKED。`; +} + +function isDurableRuntimeReady(summary) { + if (!summary || typeof summary !== "object") return false; + if (summary.durable !== true) return false; + if (summary.ready === false) return false; + if (summary.liveRuntimeEvidence !== true) return false; + return !["blocked", "degraded", "failed"].includes(String(summary.status ?? "").toLowerCase()); +} + function hasTrustedM3LiveEvidence(candidate) { if (!candidate || typeof candidate !== "object") return false; if (candidate.dryRun === true || candidate.fixture === true || candidate.sourceKind === "SOURCE" || candidate.sourceKind === "DRY-RUN") return false; @@ -1053,7 +1076,7 @@ function auditRecordCards(live) { tone: "source" })); } else { - cards.push(...liveEvents.map((event) => auditCard(event, hasTrustedM3LiveEvidence(event) ? "dev-live" : "blocked"))); + cards.push(...liveEvents.map((event) => auditCard(event, liveRecordTone(event, live), liveRecordDurabilityBlocker(live)))); } } else { cards.push(liveFailureCard("audit.event.query", live.audit.error)); @@ -1082,7 +1105,7 @@ function evidenceRecordCards(live) { tone: "source" })); } else { - cards.push(...liveRecords.map((record) => evidenceCard(record, hasTrustedM3LiveEvidence(record) ? "dev-live" : "blocked"))); + cards.push(...liveRecords.map((record) => evidenceCard(record, liveRecordTone(record, live), liveRecordDurabilityBlocker(live)))); } } else { cards.push(liveFailureCard("evidence.record.query", live.evidence.error)); @@ -1099,11 +1122,12 @@ function evidenceRecordCards(live) { return cards; } -function auditCard(event, tone) { +function auditCard(event, tone, durabilityBlocker = null) { return infoCard({ title: event.auditId, detail: [ sourceKindLabel(tone), + durabilityBlocker, tone === "blocked" ? "只读 audit 缺少 M3 patch-panel live report / operation / trace / evidence 绑定" : null, recordField("action", event.action), recordField("target", event.targetId), @@ -1115,11 +1139,12 @@ function auditCard(event, tone) { }); } -function evidenceCard(record, tone) { +function evidenceCard(record, tone, durabilityBlocker = null) { return infoCard({ title: record.evidenceId, detail: [ sourceKindLabel(tone), + durabilityBlocker, tone === "blocked" ? "只读 evidence 缺少 M3 patch-panel live report / operation / trace / audit 绑定" : null, recordKindLabel(record.kind ?? "record"), recordField("operation", record.operationId), diff --git a/web/hwlab-cloud-web/scripts/check.mjs b/web/hwlab-cloud-web/scripts/check.mjs index 445fdc6a..141b5169 100644 --- a/web/hwlab-cloud-web/scripts/check.mjs +++ b/web/hwlab-cloud-web/scripts/check.mjs @@ -52,6 +52,7 @@ const requiredTrustedRecordTerms = Object.freeze([ "conversationId、traceId 和 messageId", "provider/model/trace/conversation", "失败原因=", + "runtime.durable=false;只读 audit/evidence 不能标为 durable ready 或 DEV-LIVE。", "只读 audit 缺少 M3 patch-panel live report / operation / trace / evidence 绑定", "只读 evidence 缺少 M3 patch-panel live report / operation / trace / audit 绑定", "当前显示 SOURCE 回退,不能冒充 DEV-LIVE" @@ -128,6 +129,13 @@ assert.match(app, /function auditRecordCards/); assert.match(app, /function evidenceRecordCards/); assert.match(app, /function auditCard/); assert.match(app, /function evidenceCard/); +assert.match(app, /function liveRecordTone/); +assert.match(app, /function liveRecordDurabilityBlocker/); +assert.match(app, /function isDurableRuntimeReady/); +assert.match(app, /runtimeSummary\?\.durable !== true/); +assert.match(app, /summary\.liveRuntimeEvidence !== true/); +assert.match(app, /auditCard\(event, liveRecordTone\(event, live\), liveRecordDurabilityBlocker\(live\)\)/); +assert.match(app, /evidenceCard\(record, liveRecordTone\(record, live\), liveRecordDurabilityBlocker\(live\)\)/); assert.match(app, /function recordGroup/); assert.match(app, /function recordField/); assert.match(app, /function sourceKindLabel/);