fix: split quick verify control findings

This commit is contained in:
Codex
2026-07-02 07:33:19 +00:00
parent a5644314cd
commit 1c9132e7c6
5 changed files with 403 additions and 33 deletions
+67 -3
View File
@@ -8,6 +8,7 @@ import { repoRoot } from "./config";
import { startJob } from "./jobs";
import type { RenderedCliResult } from "./output";
import { runWebProbeRemoteArtifactJob } from "./web-probe-remote-artifact";
import { readWebProbeSentinelConfigRefTarget } from "./hwlab-node-web-sentinel-config-ref";
import type { SentinelCicdState, WebProbeSentinelOptions } from "./hwlab-node-web-sentinel-cicd";
import {
clipTail,
@@ -32,7 +33,7 @@ import {
text,
withWarnings,
} from "./hwlab-node-web-sentinel-cicd";
import { metricNames, runSentinelQuickVerify, sentinelP5Next, serviceUnavailableBlocker, validationBlocker } from "./hwlab-node-web-sentinel-p5-observe";
import { metricNames, reclassifyQuickVerifyControlFindings, runSentinelQuickVerify, sentinelP5Next, serviceUnavailableBlocker, validationBlocker } from "./hwlab-node-web-sentinel-p5-observe";
const SENTINEL_REPORT_ARTIFACT_READ_TIMEOUT_SECONDS = 55;
@@ -264,6 +265,7 @@ function compactSentinelReportRawPayload(
const findings = Array.isArray(body.findings) ? body.findings.map(record) : [];
const artifactFindings = Array.isArray(artifact.findings) ? artifact.findings.map(record) : [];
const visibleFindings = mergeSentinelReportFindings(findings, artifactFindings);
const offlineReclassify = offlineQuickVerifyReclassify(state, run, visibleFindings);
const storedFindingCount = numberAtNullable(run, "finding_count") ?? numberAtNullable(run, "findingCount") ?? findings.length;
const artifactFindingCount = numberAtNullable(artifact, "findingCount") ?? artifactFindings.length;
const visibleFindingCount = Math.max(storedFindingCount, artifactFindingCount, visibleFindings.length);
@@ -306,6 +308,7 @@ function compactSentinelReportRawPayload(
},
summary: pickFields(record(body.summary), ["reason", "status", "businessStatus", "failure", "valuesRedacted"]),
findings: visibleFindings.slice(0, 12).map(compactSentinelReportFinding),
offlineReclassify,
artifactSummary: Object.keys(artifact).length === 0 ? null : {
ok: artifact.ok === true,
reason: artifact.reason ?? null,
@@ -333,6 +336,53 @@ function compactSentinelReportRawPayload(
};
}
function offlineQuickVerifyReclassify(state: SentinelCicdState, run: Record<string, unknown>, findings: readonly Record<string, unknown>[]): Record<string, unknown> | null {
const hasLegacyQuickVerifyControl = findings.some((item) => sentinelReportFindingIdentityCandidates(item).includes("quick-verify-no-business-turn"));
if (!hasLegacyQuickVerifyControl) return null;
const catalog = sentinelReportCheckCatalogById(state);
const result = reclassifyQuickVerifyControlFindings(state, {
runId: stringAtNullable(run, "id"),
scenarioId: stringAtNullable(run, "scenario_id") ?? stringAtNullable(run, "scenarioId"),
observerId: stringAtNullable(run, "observer_id") ?? stringAtNullable(run, "observerId"),
failure: stringAtNullable(record(run.summary), "failure"),
timeoutSeconds: SENTINEL_REPORT_ARTIFACT_READ_TIMEOUT_SECONDS,
});
const resultFindings = Array.isArray(result.findings) ? result.findings.map(record) : [];
return {
...pickFields(result, ["ok", "reason", "runId", "scenarioId", "observerId", "promptIndex", "findingCount", "turnSummary", "traceFrame", "valuesRedacted"]),
source: "offline-existing-observe-artifact",
note: "This is a local CLI reclassification of existing turn-summary/trace-frame artifacts; it does not mutate the runner index.",
findings: resultFindings.slice(0, 8).map((item) => compactSentinelReportFinding(enrichSentinelReportFindingWithCatalog(item, catalog))),
valuesRedacted: true,
};
}
function sentinelReportCheckCatalogById(state: SentinelCicdState): Map<string, Record<string, unknown>> {
try {
const reportViews = record(readWebProbeSentinelConfigRefTarget(state.spec, state.configRefs.reportViews));
const catalogRef = stringAtNullable(reportViews, "checkCatalogRef");
if (catalogRef === null) return new Map();
const catalog = record(readWebProbeSentinelConfigRefTarget(state.spec, catalogRef));
const items = Array.isArray(catalog.items) ? catalog.items.map(record) : Array.isArray(catalog.checks) ? catalog.checks.map(record) : [];
return new Map(items.map((item) => [stringAtNullable(item, "id") ?? "", item]).filter((item): item is [string, Record<string, unknown>] => item[0].length > 0));
} catch {
return new Map();
}
}
function enrichSentinelReportFindingWithCatalog(item: Record<string, unknown>, catalog: ReadonlyMap<string, Record<string, unknown>>): Record<string, unknown> {
const id = stringAtNullable(item, "id") ?? stringAtNullable(item, "kind") ?? stringAtNullable(item, "code");
const check = id === null ? null : catalog.get(id) ?? null;
if (check === null) return item;
return {
...item,
check,
checkCode: stringAtNullable(check, "code"),
checkTitleZh: stringAtNullable(check, "titleZh"),
valuesRedacted: true,
};
}
function mergeSentinelReportFindings(primary: readonly Record<string, unknown>[], artifact: readonly Record<string, unknown>[]): Record<string, unknown>[] {
const merged: Record<string, unknown>[] = [];
const seen = new Set<string>();
@@ -423,8 +473,12 @@ function compactSentinelReportFinding(value: Record<string, unknown>): Record<st
const rootCauseSignals = compactRootCauseSignals(value.rootCauseSignals);
if (rootCauseSignals !== null) result.rootCauseSignals = rootCauseSignals;
const check = record(value.check);
const checkCodeFromValue = stringAtNullable(value, "checkCode");
const checkTitleFromValue = stringAtNullable(value, "checkTitleZh");
const checkCode = stringAtNullable(check, "code");
const checkTitle = stringAtNullable(check, "titleZh");
if (checkCodeFromValue !== null) result.checkCode = checkCodeFromValue;
if (checkTitleFromValue !== null) result.checkTitleZh = checkTitleFromValue;
if (checkCode !== null || checkTitle !== null) {
result.check = pickFields(check, ["id", "code", "level", "titleZh", "blocking", "registered"]);
}
@@ -471,7 +525,9 @@ function renderSentinelReportSummary(payload: Record<string, unknown>, state: Se
const run = record(payload.run);
const summary = record(payload.summary);
const artifactSummary = record(payload.artifactSummary);
const offlineReclassify = record(payload.offlineReclassify);
const findings = Array.isArray(payload.findings) ? payload.findings.map(record) : [];
const offlineFindings = Array.isArray(offlineReclassify.findings) ? offlineReclassify.findings.map(record) : [];
const reportSha = stringAtNullable(run, "reportJsonSha256") ?? stringAtNullable(artifactSummary, "reportJsonSha256");
const findingCount = run.findingCount ?? findings.length;
const analyzerArtifactCount = numberAtNullable(artifactSummary, "artifactCount") ?? numberAtNullable(record(artifactSummary.counts), "artifacts");
@@ -498,26 +554,34 @@ function renderSentinelReportSummary(payload: Record<string, unknown>, state: Se
"",
"Findings",
findingRows,
offlineFindings.length === 0 ? "" : "",
offlineFindings.length === 0 ? "" : "Offline Reclassify",
offlineFindings.length === 0 ? "" : offlineFindings.map(formatSentinelReportFindingLine).join("\n"),
].join("\n");
}
function renderSentinelReportFindings(payload: Record<string, unknown>): string {
const run = record(payload.run);
const artifactSummary = record(payload.artifactSummary);
const offlineReclassify = record(payload.offlineReclassify);
const findings = Array.isArray(payload.findings) ? payload.findings.map(record) : [];
const offlineFindings = Array.isArray(offlineReclassify.findings) ? offlineReclassify.findings.map(record) : [];
const reportSha = stringAtNullable(run, "reportJsonSha256") ?? stringAtNullable(artifactSummary, "reportJsonSha256");
return [
"Web Probe Sentinel Findings",
"=======================================================",
`run=${run.id ?? "-"} report=${reportSha ?? "-"} findings=${run.findingCount ?? findings.length}`,
findings.length === 0 ? "-" : findings.map(formatSentinelReportFindingLine).join("\n"),
offlineFindings.length === 0 ? "" : "",
offlineFindings.length === 0 ? "" : "Offline Reclassify",
offlineFindings.length === 0 ? "" : offlineFindings.map(formatSentinelReportFindingLine).join("\n"),
].join("\n");
}
function formatSentinelReportFindingLine(item: Record<string, unknown>): string {
const check = record(item.check);
const code = stringAtNullable(check, "code") ?? stringAtNullable(item, "id") ?? "-";
const title = stringAtNullable(check, "titleZh") ?? "";
const code = stringAtNullable(item, "checkCode") ?? stringAtNullable(check, "code") ?? stringAtNullable(item, "id") ?? "-";
const title = stringAtNullable(item, "checkTitleZh") ?? stringAtNullable(check, "titleZh") ?? "";
const summary = reportText(item.summary, 180) ?? "";
const rootCause = reportText(item.rootCause, 180);
const evidence = reportText(item.evidenceSummary, 180);