fix(web-sentinel): record analyze raw findings

This commit is contained in:
Codex
2026-07-01 11:21:30 +00:00
parent 1ddaca2100
commit d2ac68053d
2 changed files with 277 additions and 7 deletions
@@ -310,11 +310,12 @@ export function runSentinelQuickVerify(state: SentinelCicdState, reason: string,
}
}
printQuickVerifyProgress(state, runId, "observe-analyze", "running", { observerId, remainingSeconds: remainingSeconds(deadline, 120) });
const analysis = runChildCli(["web-probe", "observe", "analyze", observerId, "--node", state.spec.nodeId, "--lane", state.spec.lane, "--command-timeout-seconds", String(remainingSeconds(deadline, 120))], remainingSeconds(deadline, 120));
const analysis = runChildCli(["web-probe", "observe", "analyze", observerId, "--node", state.spec.nodeId, "--lane", state.spec.lane, "--command-timeout-seconds", String(remainingSeconds(deadline, 120)), "--raw", "--compact-raw"], remainingSeconds(deadline, 120));
steps.push({ phase: "observe-analyze", ok: analysis.ok, result: analysis.result });
printQuickVerifyProgress(state, runId, "observe-analyze", analysis.ok ? "succeeded" : "failed", { observerId, exitCode: record(analysis.result).exitCode ?? null, timedOut: record(analysis.result).timedOut === true, elapsedMs: elapsedMs() });
const indexEntry = readLocalObserveIndex(observerId);
const artifactSummary = indexEntry === null ? { ok: false, reason: "observe-index-entry-missing", observerId, valuesRedacted: true } : readAnalysisSummaryFromWorkspace(state, indexEntry.stateDir, QUICK_VERIFY_ANALYSIS_SUMMARY_TIMEOUT_SECONDS);
const artifactSummary = analysisSummaryFromAnalyzeResult(analysis, indexEntry?.stateDir ?? null)
?? (indexEntry === null ? { ok: false, reason: "observe-index-entry-missing", observerId, valuesRedacted: true } : readAnalysisSummaryFromWorkspace(state, indexEntry.stateDir, QUICK_VERIFY_ANALYSIS_SUMMARY_TIMEOUT_SECONDS));
const turnSummary = collectObserveView(state, observerId, "turn-summary", null, remainingSeconds(deadline, 30));
const traceFrame = collectObserveView(state, observerId, "trace-frame", promptIndex > 0 ? promptIndex : null, remainingSeconds(deadline, 30));
const controlFindings = quickVerifyControlFindings(null, promptIndex, turnSummary, traceFrame);
@@ -507,12 +508,14 @@ function finalizeQuickVerifyFailure(state: SentinelCicdState, input: {
"--node", state.spec.nodeId,
"--lane", state.spec.lane,
"--command-timeout-seconds", "55",
"--raw", "--compact-raw",
], 60);
cleanupSteps.push({ phase: "observe-analyze-after-failure", ok: analysis.ok, result: analysis.result });
const indexEntry = readLocalObserveIndex(input.observerId);
const artifactSummary = indexEntry === null
const artifactSummary = analysisSummaryFromAnalyzeResult(analysis, indexEntry?.stateDir ?? null)
?? (indexEntry === null
? { ok: false, reason: "observe-index-entry-missing", observerId: input.observerId, valuesRedacted: true }
: readAnalysisSummaryFromWorkspace(state, indexEntry.stateDir, QUICK_VERIFY_ANALYSIS_SUMMARY_TIMEOUT_SECONDS);
: readAnalysisSummaryFromWorkspace(state, indexEntry.stateDir, QUICK_VERIFY_ANALYSIS_SUMMARY_TIMEOUT_SECONDS));
const turnSummary = collectObserveView(state, input.observerId, "turn-summary", null, 30);
const traceFrame = collectObserveView(state, input.observerId, "trace-frame", input.promptIndex > 0 ? input.promptIndex : null, 30);
const durableBusinessTurn = quickVerifyHasDurableBusinessTurn(input.promptIndex, turnSummary, traceFrame);
@@ -780,13 +783,19 @@ function compactQuickVerifyRecordAnalysis(value: unknown): Record<string, unknow
return {
ok: item.ok === true ? true : item.ok === false ? false : null,
reportOk: item.reportOk === true ? true : item.reportOk === false ? false : null,
source: stringAtNullable(item, "source"),
reason: stringAtNullable(item, "reason"),
stateDir: stringAtNullable(item, "stateDir"),
reportJsonPath: stringAtNullable(item, "reportJsonPath"),
reportJsonSha256: stringAtNullable(item, "reportJsonSha256"),
reportMdPath: stringAtNullable(item, "reportMdPath"),
reportMdSha256: stringAtNullable(item, "reportMdSha256"),
reportReadWaitMs: numberAtNullable(item, "reportReadWaitMs"),
reportParseError: stringAtNullable(item, "reportParseError"),
findingCount: numberAtNullable(item, "findingCount"),
artifactCount: numberAtNullable(item, "artifactCount"),
counts: compactQuickVerifyRecordCounts(record(item.counts)),
readResult: compactQuickVerifyRecordArtifactReadResult(record(item.result)),
screenshot: compactQuickVerifyRecordScreenshot(record(item.screenshot)),
findings: Array.isArray(item.findings) ? item.findings.slice(0, 16).map(compactQuickVerifyRecordFinding) : [],
pagePerformanceSlowApi: Array.isArray(item.pagePerformanceSlowApi) ? item.pagePerformanceSlowApi.slice(0, 6).map(record) : [],
@@ -795,6 +804,19 @@ function compactQuickVerifyRecordAnalysis(value: unknown): Record<string, unknow
};
}
function compactQuickVerifyRecordArtifactReadResult(value: Record<string, unknown>): Record<string, unknown> | null {
if (Object.keys(value).length === 0) return null;
return {
exitCode: numberAtNullable(value, "exitCode"),
timedOut: value.timedOut === true ? true : value.timedOut === false ? false : null,
stdoutBytes: numberAtNullable(value, "stdoutBytes"),
stderrBytes: numberAtNullable(value, "stderrBytes"),
stdoutPreview: boundQuickVerifyRecordText(value.stdoutPreview, 600),
stderrPreview: boundQuickVerifyRecordText(value.stderrPreview, 600),
valuesRedacted: true,
};
}
function compactQuickVerifyRecordBrowserProcess(value: Record<string, unknown>): Record<string, unknown> | null {
const pageSeries = Array.isArray(value.pageSeries) ? value.pageSeries.map(compactQuickVerifyRecordMemorySeries).filter((item) => item.points.length > 0) : [];
if (pageSeries.length === 0) return null;
@@ -965,6 +987,82 @@ function boundQuickVerifyRecordText(value: unknown, maxChars: number): string |
return `${value.slice(0, maxChars)}\n[truncated ${value.length - maxChars} chars]`;
}
function analysisSummaryFromAnalyzeResult(analysis: ChildCliResult, fallbackStateDir: string | null): Record<string, unknown> | null {
const payload = cliDataPayload(analysis.parsed);
const source = record(payload.analysis);
const reportJsonSha256 = stringAtNullable(source, "reportJsonSha256");
if (reportJsonSha256 === null) return null;
const counts = record(source.counts);
const archiveSummary = record(source.archiveSummary);
const findings = mergeFindingRecords(
compactAnalyzeFindings(source.findings),
compactAnalyzeFindings(source.archiveRedFindings ?? archiveSummary.redFindings),
);
const findingCount = Math.max(
findings.length,
numberAtNullable(source, "findingCount") ?? 0,
numberAtNullable(archiveSummary, "findingCount") ?? 0,
numberAtNullable(archiveSummary, "redFindingCount") ?? 0,
);
return {
ok: true,
source: "observe-analyze-raw",
reason: null,
reportOk: source.ok === true,
stateDir: stringAtNullable(source, "stateDir") ?? fallbackStateDir,
reportJsonPath: stringAtNullable(source, "reportJsonPath"),
reportJsonSha256,
reportMdPath: stringAtNullable(source, "reportMdPath"),
reportMdSha256: stringAtNullable(source, "reportMdSha256"),
findingCount,
artifactCount: numberAtNullable(source, "artifactCount") ?? numberAtNullable(counts, "artifacts") ?? 0,
findings,
counts,
analysisWindow: record(source.analysisWindow),
pagePerformanceSlowApi: Array.isArray(source.pagePerformanceSlowApi) ? source.pagePerformanceSlowApi.slice(0, 8).map(record) : [],
browserProcess: compactAnalyzeBrowserProcess(source.browserProcess),
result: record(payload.result),
valuesRedacted: true,
};
}
function compactAnalyzeFindings(value: unknown): Record<string, unknown>[] {
if (!Array.isArray(value)) return [];
return value.slice(0, 20).map(record).map((item) => ({
id: stringAtNullable(item, "id") ?? stringAtNullable(item, "kind") ?? stringAtNullable(item, "code"),
kind: stringAtNullable(item, "kind") ?? stringAtNullable(item, "id") ?? stringAtNullable(item, "code"),
code: stringAtNullable(item, "code") ?? stringAtNullable(item, "id") ?? stringAtNullable(item, "kind"),
severity: stringAtNullable(item, "severity") ?? stringAtNullable(item, "level"),
level: stringAtNullable(item, "level") ?? stringAtNullable(item, "severity"),
count: numberAtNullable(item, "count") ?? numberAtNullable(item, "sampleCount") ?? 1,
sampleCount: numberAtNullable(item, "sampleCount") ?? numberAtNullable(item, "count") ?? 1,
summary: stringAtNullable(item, "summary") ?? stringAtNullable(item, "message"),
message: stringAtNullable(item, "message") ?? stringAtNullable(item, "summary"),
rootCause: stringAtNullable(item, "rootCause"),
rootCauseStatus: stringAtNullable(item, "rootCauseStatus"),
rootCauseConfidence: stringAtNullable(item, "rootCauseConfidence"),
nextAction: stringAtNullable(item, "nextAction"),
evidenceSummary: stringAtNullable(item, "evidenceSummary"),
timingSourceOfTruth: stringAtNullable(item, "timingSourceOfTruth"),
timingStatus: stringAtNullable(item, "timingStatus"),
timingAlert: item.timingAlert === true,
valuesRedacted: true,
})).filter((item) => item.id !== null || item.kind !== null || item.code !== null);
}
function compactAnalyzeBrowserProcess(value: unknown): Record<string, unknown> | null {
const source = record(value);
if (Object.keys(source).length === 0) return null;
return {
source: stringAtNullable(source, "source") ?? "observe-analyze-raw",
unit: stringAtNullable(source, "unit"),
metric: stringAtNullable(source, "metric"),
pageCount: numberAtNullable(source, "pageCount"),
sampleCount: numberAtNullable(source, "sampleCount"),
valuesRedacted: true,
};
}
function readAnalysisSummaryFromWorkspace(state: SentinelCicdState, stateDir: string, timeoutSeconds: number): Record<string, unknown> {
if (!isSafeRelativeStateDir(stateDir)) return { ok: false, reason: "unsafe-state-dir", stateDir, valuesRedacted: true };
const waitMs = Math.max(0, Math.min(50_000, (Math.max(5, timeoutSeconds) * 1000) - 5000));
@@ -996,7 +1094,21 @@ function readAnalysisSummaryFromWorkspace(state: SentinelCicdState, stateDir: st
].join("\n");
const result = runCommand(["trans", `${state.spec.nodeId}:${state.spec.workspace}`, "sh"], repoRoot, { input: script, timeoutMs: Math.min(timeoutSeconds, 60) * 1000 });
const parsed = parseJsonObject(result.stdout);
return { ok: result.exitCode === 0 && parsed?.ok === true, ...record(parsed), result: compactCommand(result), valuesRedacted: true };
const parsedRecord = record(parsed);
const reason = stringAtNullable(parsedRecord, "reason")
?? (result.timedOut ? "workspace-artifact-read-timeout"
: result.exitCode !== 0 ? "workspace-artifact-read-command-failed"
: parsed === null ? "workspace-artifact-read-output-not-json"
: null);
return {
ok: result.exitCode === 0 && parsed?.ok === true,
...parsedRecord,
source: "workspace-trans",
reason,
stateDir: stringAtNullable(parsedRecord, "stateDir") ?? stateDir,
result: compactCommand(result),
valuesRedacted: true,
};
}
function waitForQuickVerifyObserverStartup(state: SentinelCicdState, observerId: string, deadline: number, pollIntervalMs: number, budgetSeconds: number): Record<string, unknown> {