fix: split web sentinel ambiguous check codes

This commit is contained in:
Codex
2026-07-06 16:17:07 +00:00
parent 79a35183aa
commit cc694e9d20
7 changed files with 453 additions and 19 deletions
@@ -5,7 +5,16 @@ export function nodeWebObserveAnalyzerFindingsSource(): string {
return String.raw`function buildFindings(samples, control, network, errors, sampleMetrics, promptNetwork, runtimeAlerts, pagePerformance, requestRate, pageProvenance, commandFailures = [], manifest = {}, apiDomLag = null, browserProcess = null, frontendPerformance = null) {
const findings = [];
const effectiveApiDomLag = apiDomLag || buildApiDomLagReport(samples, network);
if (commandFailures.length > 0) findings.push({ id: "observer-command-failed", severity: "red", summary: "observer control commands failed; analyze must surface command failure instead of hiding it in command artifacts", count: commandFailures.length, commands: commandFailures.slice(0, 20) });
const compactCommandFailures = commandFailures.map(compactObserverCommandFailure);
const firstCommandFailure = compactCommandFailures[0] || null;
if (commandFailures.length > 0) findings.push({
id: "observer-command-failed",
severity: "red",
summary: "observer control commands failed; analyze must surface the first failed command with phase/status/stdout/stderr evidence.",
count: commandFailures.length,
firstCommandFailure,
commands: compactCommandFailures.slice(0, 20)
});
findings.push(...buildFrontendFreezeFindings(errors, control));
findings.push(...buildBrowserProcessFindings(browserProcess, runtimeAlerts));
findings.push(...buildFrontendPerformanceFindings(frontendPerformance));
@@ -386,10 +395,56 @@ export function nodeWebObserveAnalyzerFindingsSource(): string {
groups: Array.isArray(effectiveApiDomLag?.groups) ? effectiveApiDomLag.groups.slice(0, 12) : [],
});
if (errors.length > 0) findings.push({ id: "browser-console-or-page-errors", severity: "amber", summary: "pageerror/runner errors were captured", count: errors.length, first: errors.slice(0, 5) });
if (samples.length === 0) findings.push({ id: "no-samples", severity: "red", summary: "observer produced no samples" });
if (samples.length === 0) {
if (commandFailures.length > 0) {
findings.push({
id: "no-samples-after-observer-command-failed",
severity: "red",
summary: "observer produced zero samples after a blocking control command failed; the command failure is the deterministic precondition.",
count: 1,
firstCommandFailure,
rootCause: "observer-command-failed-before-sampling",
rootCauseStatus: "confirmed",
rootCauseConfidence: "high",
evidenceSummary: firstCommandFailure ? "command=" + (firstCommandFailure.commandId || "-") + " type=" + (firstCommandFailure.type || "-") + " phase=" + (firstCommandFailure.phase || "-") + " status=" + (firstCommandFailure.status || "-") + " exit=" + (firstCommandFailure.exitCode ?? "-") : "commandFailures>0",
valuesRedacted: true
});
} else {
findings.push({
id: "no-samples-artifact-evidence-missing",
severity: "red",
summary: "observer produced zero samples and analyzer has no failed command precondition; sampling/source evidence is missing.",
count: 1,
rootCause: "sample-artifact-evidence-missing",
rootCauseStatus: "evidence-gap",
rootCauseConfidence: "medium",
evidenceSummary: "samples=0 control=" + (Array.isArray(control) ? control.length : 0) + " errors=" + (Array.isArray(errors) ? errors.length : 0),
valuesRedacted: true
});
}
}
return findings;
}
function compactObserverCommandFailure(item) {
const result = item && typeof item.result === "object" ? item.result : {};
const output = item && typeof item.output === "object" ? item.output : {};
const stdout = item?.stdoutPreview ?? item?.stdoutTail ?? result.stdoutPreview ?? result.stdoutTail ?? output.stdoutPreview ?? output.stdoutTail ?? null;
const stderr = item?.stderrPreview ?? item?.stderrTail ?? result.stderrPreview ?? result.stderrTail ?? output.stderrPreview ?? output.stderrTail ?? null;
return {
commandId: item?.commandId ?? item?.id ?? item?.command_id ?? null,
type: item?.type ?? item?.commandType ?? item?.command_type ?? null,
phase: item?.phase ?? item?.statusPhase ?? null,
status: item?.status ?? item?.resultStatus ?? null,
exitCode: item?.exitCode ?? result.exitCode ?? null,
timedOut: item?.timedOut === true || result.timedOut === true,
stdoutPreview: stdout == null ? null : String(stdout).slice(0, 240),
stderrPreview: stderr == null ? null : String(stderr).slice(0, 240),
artifactRef: item?.artifactRef ?? item?.artifactPath ?? item?.path ?? null,
valuesRedacted: true
};
}
function buildCrossPageProjectionDrilldown(rows) {
const sourceRows = Array.isArray(rows) ? rows.filter(Boolean) : [];
const traceIds = uniqueSorted(sourceRows.flatMap((row) => collectIdsFromUnknown(row, "trace")).filter(Boolean)).slice(0, 12);
@@ -37,10 +37,13 @@ test("quick verify cleanup downgrades abandoned graceful stop when force stop co
assert.equal(cleanup.forceStopOk, true);
assert.equal(cleanup.aliveAfter, false);
assert.equal(cleanup.pendingAbandoned, 1);
assert.deepEqual(quickVerifyCleanupFindings(step), []);
const findings = quickVerifyCleanupFindings(step);
assert.equal(findings.length, 1);
assert.equal(findings[0]?.id, "observer-graceful-stop-timeout-force-stop-succeeded");
assert.equal(findings[0]?.severity, "warning");
});
test("quick verify cleanup emits WBC-096 when force stop leaves observer alive", () => {
test("quick verify cleanup emits process-alive code when force stop leaves observer alive", () => {
const step = forceStopStep({
aliveBefore: true,
aliveAfter: true,
@@ -56,11 +59,11 @@ test("quick verify cleanup emits WBC-096 when force stop leaves observer alive",
assert.equal(cleanup.forceStopOk, false);
assert.equal(cleanup.aliveAfter, true);
assert.equal(findings.length, 1);
assert.equal(findings[0]?.id, "quick-verify-observer-stop-failed");
assert.equal(findings[0]?.id, "observer-force-stop-left-process-alive");
assert.match(String(findings[0]?.evidenceSummary), /aliveAfter=true/u);
});
test("quick verify cleanup emits WBC-096 when stopped state cannot be confirmed", () => {
test("quick verify cleanup emits evidence-gap code when stopped state cannot be confirmed", () => {
const step = forceStopStep({
aliveBefore: true,
termSent: true,
@@ -74,5 +77,5 @@ test("quick verify cleanup emits WBC-096 when stopped state cannot be confirmed"
assert.equal(cleanup.cleanupClass, "observer-stop-failed");
assert.equal(cleanup.aliveAfter, null);
assert.equal(findings.length, 1);
assert.equal(findings[0]?.id, "quick-verify-observer-stop-failed");
assert.equal(findings[0]?.id, "observer-cleanup-evidence-missing");
});
@@ -647,12 +647,30 @@ function stopQuickVerifyObserver(state: SentinelCicdState, observerId: string, p
export function quickVerifyCleanupFindings(cleanupStep: Record<string, unknown>): Record<string, unknown>[] {
const cleanup = classifyQuickVerifyCleanupStep(cleanupStep);
if (stringAtNullable(cleanup, "cleanupClass") !== "observer-stop-failed") return [];
const cleanupClass = stringAtNullable(cleanup, "cleanupClass");
if (cleanupClass === "bounded-force-stop") {
return [{
id: "observer-graceful-stop-timeout-force-stop-succeeded",
severity: "warning",
count: 1,
summary: "observer graceful stop did not drain before timeout, but force stop confirmed aliveAfter=false.",
cleanupPhase: cleanupStep.phase,
cleanup,
evidenceSummary: formatQuickVerifyCleanupLine(cleanup),
valuesRedacted: true,
}];
}
if (cleanupClass !== "observer-stop-failed") return [];
const aliveAfter = booleanAtNullable(cleanup, "aliveAfter");
const id = aliveAfter === true ? "observer-force-stop-left-process-alive" : "observer-cleanup-evidence-missing";
const summary = aliveAfter === true
? "observer force stop completed but process liveness evidence still reports aliveAfter=true."
: "observer cleanup failed because stop-state evidence is missing or incomplete; this is an evidence gap, not confirmed process leakage.";
return [{
id: "quick-verify-observer-stop-failed",
id,
severity: "red",
count: 1,
summary: "quick verify completed target analysis but failed to stop its observer runner; this can leak Chrome process trees on cadence runs.",
summary,
cleanupPhase: cleanupStep.phase,
cleanup,
evidenceSummary: formatQuickVerifyCleanupLine(cleanup),
@@ -1993,9 +2011,11 @@ function isQuickVerifyBlockingFinding(item: Record<string, unknown>): boolean {
"quick-verify-final-response-empty-status-unknown",
"quick-verify-diagnostics-inconclusive",
"quick-verify-analysis-summary-unreadable",
"quick-verify-command-sequence-failed",
"account-session-command-failed-before-observation",
"quick-verify-observer-start-failed",
"quick-verify-observer-stop-failed",
"observer-force-stop-left-process-alive",
"observer-cleanup-evidence-missing",
"observer-cleanup-index-mismatch",
"quick-verify-account-secret-missing",
"prompt-chat-submit-failed",
"workbench-turn-state-triad-inconsistent",
@@ -2004,6 +2024,9 @@ function isQuickVerifyBlockingFinding(item: Record<string, unknown>): boolean {
"round-completion-final-response-missing",
"turn-trace-id-missing",
"no-samples",
"no-samples-after-observer-command-failed",
"no-samples-artifact-evidence-missing",
"observer-command-failure-evidence-missing",
"jsonl-read-issues",
].includes(id);
}
@@ -2123,15 +2146,22 @@ function quickVerifyControlFindings(failure: string | null, promptIndex: number,
const observerStartFailure = failure === "observe-start-failed";
const displayTitleZh = stringAtNullable(display ?? {}, "titleZh");
return [{
id: observerStartFailure ? "quick-verify-observer-start-failed" : "quick-verify-command-sequence-failed",
id: observerStartFailure ? "quick-verify-observer-start-failed" : "account-session-command-failed-before-observation",
severity: "red",
count: 1,
summary: displayTitleZh ?? (observerStartFailure
? "quick verify observer failed to start before the no-prompt scenario could run."
: "quick verify no-prompt command sequence failed before the account/session workflow completed."),
: "account/session setup command failed before any business turn could be observed."),
displayTitleZh,
errorTitleZh: displayTitleZh,
timeoutDisplay: display,
rootCause: observerStartFailure ? "observer-start-failed" : `account-session-command-stage-failed:${failure}`,
rootCauseStatus: "confirmed",
rootCauseConfidence: "high",
evidenceSummary: observerStartFailure ? `failure=${failure}` : `stage=account-session-command failure=${failure}`,
nextAction: observerStartFailure
? "Inspect observer start output and fix the reported degradedReason before interpreting samples."
: "Inspect the first failed account/session/workbench command and rerun quick verify after that command succeeds.",
failure,
promptIndex,
valuesRedacted: true,
+142 -3
View File
@@ -264,7 +264,7 @@ function compactSentinelReportRawPayload(
const artifact = record(artifactSummary);
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 visibleFindings = reclassifySentinelReportFindings(state, run, 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;
@@ -337,7 +337,14 @@ 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"));
const hasLegacyQuickVerifyControl = findings.some((item) => sentinelReportFindingIdentityCandidates(item).some((id) => [
"quick-verify-no-business-turn",
"quick-verify-command-sequence-failed",
"account-session-command-failed-before-observation",
"no-samples",
"no-samples-after-observer-command-failed",
"no-samples-artifact-evidence-missing",
].includes(id)));
if (!hasLegacyQuickVerifyControl) return null;
const catalog = sentinelReportCheckCatalogById(state);
const result = reclassifyQuickVerifyControlFindings(state, {
@@ -357,6 +364,114 @@ function offlineQuickVerifyReclassify(state: SentinelCicdState, run: Record<stri
};
}
function reclassifySentinelReportFindings(state: SentinelCicdState, run: Record<string, unknown>, findings: readonly Record<string, unknown>[]): Record<string, unknown>[] {
const hasObserverCommandFailure = findings.some((item) => sentinelReportFindingIdentityCandidates(item).includes("observer-command-failed"));
const catalog = sentinelReportCheckCatalogById(state);
return findings.map((item) => enrichSentinelReportFindingWithCatalog(reclassifySentinelReportFinding(rowFailure(run), item, hasObserverCommandFailure), catalog));
}
function rowFailure(row: Record<string, unknown>): string | null {
return stringAtNullable(record(row.summary), "failure")
?? stringAtNullable(row, "failure")
?? stringAtNullable(record(row.businessStatus), "failure");
}
function reclassifySentinelReportFinding(failure: string | null, item: Record<string, unknown>, hasObserverCommandFailure: boolean): Record<string, unknown> {
const ids = sentinelReportFindingIdentityCandidates(item);
if (ids.includes("quick-verify-command-sequence-failed")) {
return retagSentinelReportFinding(item, "account-session-command-failed-before-observation", {
summary: "account/session setup command failed before any business turn could be observed.",
rootCause: `account-session-command-stage-failed:${failure ?? "unknown"}`,
rootCauseStatus: "confirmed",
rootCauseConfidence: "high",
evidenceSummary: `stage=account-session-command failure=${failure ?? "-"}`,
nextAction: "Inspect the first failed account/session/workbench command and rerun quick verify after that command succeeds.",
});
}
if (ids.includes("observer-command-failed") && !hasCommandFailureEvidence(item)) {
return retagSentinelReportFinding(item, "observer-command-failure-evidence-missing", {
summary: "report says observer control commands failed, but the first failed command evidence is missing.",
rootCause: "observer-command-failure-evidence-missing",
rootCauseStatus: "evidence-gap",
rootCauseConfidence: "high",
evidenceSummary: "firstCommandFailure missing from indexed and artifact findings",
nextAction: "Fix analyzer/report commandFailures compact output before using this finding as a page root cause.",
});
}
if (ids.includes("quick-verify-observer-stop-failed")) {
const evidence = `${stringAtNullable(item, "evidenceSummary") ?? ""} ${stringAtNullable(item, "summary") ?? ""}`;
if (/aliveAfter\s*=\s*true/u.test(evidence)) {
return retagSentinelReportFinding(item, "observer-force-stop-left-process-alive", {
summary: "observer force stop completed but process liveness evidence still reports aliveAfter=true.",
rootCause: "observer-process-still-alive-after-force-stop",
rootCauseStatus: "confirmed",
rootCauseConfidence: "high",
});
}
if (/aliveAfter\s*=\s*false/u.test(evidence)) {
return retagSentinelReportFinding(item, "observer-graceful-stop-timeout-force-stop-succeeded", {
severity: "warning",
level: "warning",
summary: "observer graceful stop did not drain before timeout, but force stop confirmed aliveAfter=false.",
rootCause: "observer-graceful-stop-timeout-force-stop-succeeded",
rootCauseStatus: "confirmed",
rootCauseConfidence: "high",
});
}
return retagSentinelReportFinding(item, "observer-cleanup-evidence-missing", {
summary: "observer cleanup stop-state evidence is missing or incomplete; this is an evidence gap, not confirmed process leakage.",
rootCause: "observer-cleanup-evidence-missing",
rootCauseStatus: "evidence-gap",
rootCauseConfidence: "medium",
evidenceSummary: stringAtNullable(item, "evidenceSummary") ?? "cleanup evidence missing",
});
}
if (ids.includes("no-samples")) {
if (hasObserverCommandFailure) {
return retagSentinelReportFinding(item, "no-samples-after-observer-command-failed", {
summary: "observer produced zero samples after a blocking control command failed; the command failure is the deterministic precondition.",
rootCause: "observer-command-failed-before-sampling",
rootCauseStatus: "confirmed",
rootCauseConfidence: "high",
});
}
return retagSentinelReportFinding(item, "no-samples-artifact-evidence-missing", {
summary: "observer produced zero samples and analyzer has no failed command precondition; sampling/source evidence is missing.",
rootCause: "sample-artifact-evidence-missing",
rootCauseStatus: "evidence-gap",
rootCauseConfidence: "medium",
});
}
return item;
}
function hasCommandFailureEvidence(item: Record<string, unknown>): boolean {
const first = record(item.firstCommandFailure ?? firstArrayRecord(item.commands));
return stringAtNullable(first, "type") !== null
|| stringAtNullable(first, "phase") !== null
|| stringAtNullable(first, "status") !== null
|| stringAtNullable(first, "stdoutPreview") !== null
|| stringAtNullable(first, "stderrPreview") !== null
|| numberAtNullable(first, "exitCode") !== null;
}
function retagSentinelReportFinding(item: Record<string, unknown>, id: string, extra: Record<string, unknown>): Record<string, unknown> {
return {
...item,
...extra,
id,
kind: id,
code: id,
finding_id: id,
findingId: id,
check: null,
checkId: null,
checkCode: null,
checkTitleZh: null,
valuesRedacted: true,
};
}
function sentinelReportCheckCatalogById(state: SentinelCicdState): Map<string, Record<string, unknown>> {
try {
const reportViews = record(readWebProbeSentinelConfigRefTarget(state.spec, state.configRefs.reportViews));
@@ -465,6 +580,8 @@ function compactSentinelReportFinding(value: Record<string, unknown>): Record<st
if (nextAction !== null) result.nextAction = nextAction;
const evidenceSummary = reportText(value.evidenceSummary, 240);
if (evidenceSummary !== null) result.evidenceSummary = evidenceSummary;
const firstCommandFailure = compactSentinelReportCommandFailure(value.firstCommandFailure ?? firstArrayRecord(value.commands));
if (firstCommandFailure !== null) result.firstCommandFailure = firstCommandFailure;
const timingSourceOfTruth = stringAtNullable(value, "timingSourceOfTruth");
if (timingSourceOfTruth !== null) result.timingSourceOfTruth = timingSourceOfTruth;
const timingStatus = stringAtNullable(value, "timingStatus");
@@ -485,6 +602,27 @@ function compactSentinelReportFinding(value: Record<string, unknown>): Record<st
return result;
}
function compactSentinelReportCommandFailure(value: unknown): Record<string, unknown> | null {
const item = record(value);
if (Object.keys(item).length === 0) return null;
return {
commandId: stringAtNullable(item, "commandId"),
type: stringAtNullable(item, "type"),
phase: stringAtNullable(item, "phase"),
status: stringAtNullable(item, "status"),
exitCode: numberAtNullable(item, "exitCode"),
timedOut: item.timedOut === true ? true : item.timedOut === false ? false : null,
stdoutPreview: reportText(item.stdoutPreview, 160),
stderrPreview: reportText(item.stderrPreview, 160),
artifactRef: stringAtNullable(item, "artifactRef"),
valuesRedacted: true,
};
}
function firstArrayRecord(value: unknown): Record<string, unknown> | null {
return Array.isArray(value) && value.length > 0 ? record(value[0]) : null;
}
function readSentinelReportArtifactSummary(state: SentinelCicdState, body: Record<string, unknown>, timeoutSeconds: number): Record<string, unknown> | null {
const run = record(body.run);
const stateDir = stringAtNullable(run, "state_dir") ?? stringAtNullable(run, "stateDir");
@@ -501,13 +639,14 @@ function readSentinelReportArtifactSummary(state: SentinelCicdState, body: Recor
"const sha=(buf)=>buf?`sha256:${crypto.createHash('sha256').update(buf).digest('hex')}`:null;",
"const rec=(v)=>v&&typeof v==='object'&&!Array.isArray(v)?v:{}; const arr=(v)=>Array.isArray(v)?v:[]; const clip=(v,n=180)=>v==null?null:String(v).slice(0,n);",
"const compactRootCauseSignals=(value)=>{const v=rec(value); const keys=['sessionListReadCount','traceEventsReadCount','webPerformanceBeaconFailureCount','eventSourceFailureCount','requestFailedCount','httpErrorCount','consoleAlertCount','requestfailedTop','httpStatusTop']; const out={}; for(const key of keys){if(v[key]!=null)out[key]=Array.isArray(v[key])?v[key].slice(0,8):v[key];} if(Object.keys(out).length===0)return null; out.valuesRedacted=true; return out;};",
"const compactCommandFailure=(value)=>{const v=rec(value); if(Object.keys(v).length===0)return null; return {commandId:clip(v.commandId,80),type:clip(v.type,48),phase:clip(v.phase,80),status:clip(v.status,80),exitCode:Number.isFinite(Number(v.exitCode))?Number(v.exitCode):null,timedOut:v.timedOut===true?true:v.timedOut===false?false:null,stdoutPreview:clip(v.stdoutPreview,160),stderrPreview:clip(v.stderrPreview,160),artifactRef:clip(v.artifactRef,160),valuesRedacted:true};};",
"const sleep=(ms)=>{try{Atomics.wait(new Int32Array(new SharedArrayBuffer(4)),0,0,ms)}catch{}};",
"let jsonBuf=read(reportPath); let report=null; let reportParseError=null; const deadline=Date.now()+waitMs;",
"while(true){ if(jsonBuf){try{report=JSON.parse(jsonBuf.toString('utf8')); break}catch(err){reportParseError=String(err&&err.message?err.message:err)}} if(Date.now()>=deadline)break; sleep(Math.min(500, Math.max(50, deadline-Date.now()))); jsonBuf=read(reportPath); }",
"let artifactCount=0; let screenshot=null;",
"function walk(dir){let entries=[]; try{entries=fs.readdirSync(dir,{withFileTypes:true})}catch{return}; for(const e of entries){const p=path.join(dir,e.name); if(e.isDirectory()) walk(p); else { artifactCount++; if(/\\.png$/i.test(e.name)){const b=read(p); screenshot={path:p,sha256:sha(b),bytes:b?b.length:0}; } } }}",
"walk(stateDir);",
"const findings=arr(report?.findings ?? report?.archiveSummary?.redFindings).slice(0,20).map((item)=>{const v=rec(item); return {id:clip(v.id??v.kind??v.code,80),kind:clip(v.kind??v.id??v.code,80),code:clip(v.code??v.kind??v.id,80),severity:clip(v.severity??v.level,32),level:clip(v.level??v.severity,32),count:Number(v.count??v.sampleCount??1),summary:clip(v.summary??v.message,220),message:clip(v.message??v.summary,220),rootCause:clip(v.rootCause,140),rootCauseStatus:clip(v.rootCauseStatus,90),rootCauseConfidence:clip(v.rootCauseConfidence,40),nextAction:clip(v.nextAction,240),evidenceSummary:v.evidence?clip(JSON.stringify(rec(v.evidence)),220):clip(v.evidenceSummary,220),timingSourceOfTruth:clip(v.timingSourceOfTruth??v.expectedElapsedSource??v.evidenceKind,100),timingStatus:clip(v.timingStatus,60),timingAlert:v.timingAlert===true,rootCauseSignals:compactRootCauseSignals(v.rootCauseSignals),blocking:v.blocking===true,afterRound:v.afterRound??null,canarySessionId:clip(v.canarySessionId,80),routeSessionId:clip(v.routeSessionId,80),activeSessionId:clip(v.activeSessionId,80),consecutiveUserMessageCount:v.consecutiveUserMessageCount??null,sentinelRange:clip(v.sentinelRange,80),sampleSeq:v.sampleSeq??null,traceIds:arr(v.traceIds).slice(0,8).map((x)=>clip(x,80)),pageRole:clip(v.pageRole,32),pageId:clip(v.pageId,80),observerId:clip(v.observerId,80),stateDir:clip(v.stateDir,160),commandId:clip(v.commandId,80),valuesRedacted:true};});",
"const findings=arr(report?.findings ?? report?.archiveSummary?.redFindings).slice(0,20).map((item)=>{const v=rec(item); return {id:clip(v.id??v.kind??v.code,80),kind:clip(v.kind??v.id??v.code,80),code:clip(v.code??v.kind??v.id,80),severity:clip(v.severity??v.level,32),level:clip(v.level??v.severity,32),count:Number(v.count??v.sampleCount??1),summary:clip(v.summary??v.message,220),message:clip(v.message??v.summary,220),rootCause:clip(v.rootCause,140),rootCauseStatus:clip(v.rootCauseStatus,90),rootCauseConfidence:clip(v.rootCauseConfidence,40),nextAction:clip(v.nextAction,240),evidenceSummary:v.evidence?clip(JSON.stringify(rec(v.evidence)),220):clip(v.evidenceSummary,220),firstCommandFailure:compactCommandFailure(v.firstCommandFailure || arr(v.commands)[0]),timingSourceOfTruth:clip(v.timingSourceOfTruth??v.expectedElapsedSource??v.evidenceKind,100),timingStatus:clip(v.timingStatus,60),timingAlert:v.timingAlert===true,rootCauseSignals:compactRootCauseSignals(v.rootCauseSignals),blocking:v.blocking===true,afterRound:v.afterRound??null,canarySessionId:clip(v.canarySessionId,80),routeSessionId:clip(v.routeSessionId,80),activeSessionId:clip(v.activeSessionId,80),consecutiveUserMessageCount:v.consecutiveUserMessageCount??null,sentinelRange:clip(v.sentinelRange,80),sampleSeq:v.sampleSeq??null,traceIds:arr(v.traceIds).slice(0,8).map((x)=>clip(x,80)),pageRole:clip(v.pageRole,32),pageId:clip(v.pageId,80),observerId:clip(v.observerId,80),stateDir:clip(v.stateDir,160),commandId:clip(v.commandId,80),valuesRedacted:true};});",
"const slow=arr(report?.pagePerformanceSlowApi ?? report?.archivePagePerformanceSlowApi).slice(0,8).map((item)=>{const v=rec(item); return {path:clip(v.path??v.route,120),sampleCount:v.sampleCount??null,p95Ms:v.p95Ms??null,maxMs:v.maxMs??null,overFiveSecondCount:v.overFiveSecondCount??null};});",
"console.log(JSON.stringify({ok:!!report,reason:report?null:(jsonBuf?'report-json-parse-failed':'report-json-missing'),reportReadWaitMs:waitMs,reportParseError:clip(reportParseError,220),reportOk:!!report&&report.ok!==false,stateDir,reportJsonPath:reportPath,reportJsonSha256:sha(jsonBuf),reportMdPath,reportMdSha256:sha(read(reportMdPath)),findingCount:Number(report?.findingCount??findings.length),artifactCount,screenshot,findings,counts:rec(report?.counts),analysisWindow:rec(report?.analysisWindow??report?.windows?.recent?.summary),pagePerformanceSlowApi:slow,valuesRedacted:true}));",
"NODE",
+144 -1
View File
@@ -1760,7 +1760,7 @@ function visibleFindingsForRun(
const artifactFindings = arrayRecords(artifactSummary.findings)
.slice(0, limit)
.map((item) => enrichFindingWithCheck(config, compactStoredFinding(item)));
return mergeVisibleFindings(config, storedFindings, artifactFindings, limit);
return reclassifyVisibleFindings(config, row, mergeVisibleFindings(config, storedFindings, artifactFindings, limit), limit);
}
function mergeVisibleFindings(config: WebProbeSentinelServiceConfig, primary: readonly Record<string, unknown>[], artifact: readonly Record<string, unknown>[], limit: number): readonly Record<string, unknown>[] {
@@ -1793,6 +1793,126 @@ function canonicalFindingIdentity(config: WebProbeSentinelServiceConfig, item: R
return stringOrNull(entry?.code) ?? stringOrNull(entry?.id) ?? findingId;
}
function reclassifyVisibleFindings(config: WebProbeSentinelServiceConfig, row: Record<string, unknown>, findings: readonly Record<string, unknown>[], limit: number): readonly Record<string, unknown>[] {
const hasObserverCommandFailure = findings.some((item) => findingIdentityCandidates(item).includes("observer-command-failed"));
const failure = stringOrNull(record(row.summary).failure) ?? stringOrNull(row.failure) ?? stringOrNull(record(row.businessStatus).failure);
return findings
.map((item) => enrichFindingWithCheck(config, reclassifyVisibleFinding(item, failure, hasObserverCommandFailure)))
.slice(0, limit);
}
function findingIdentityCandidates(item: Record<string, unknown>): readonly string[] {
const check = record(item.check);
const candidates = [
stringOrNull(item.finding_id),
stringOrNull(item.findingId),
stringOrNull(item.id),
stringOrNull(item.kind),
stringOrNull(item.code),
stringOrNull(item.checkId),
stringOrNull(item.checkCode),
stringOrNull(check.id),
stringOrNull(check.code),
];
return [...new Set(candidates.filter((value): value is string => value !== null))];
}
function reclassifyVisibleFinding(item: Record<string, unknown>, failure: string | null, hasObserverCommandFailure: boolean): Record<string, unknown> {
const ids = findingIdentityCandidates(item);
if (ids.includes("quick-verify-command-sequence-failed")) {
return retagVisibleFinding(item, "account-session-command-failed-before-observation", {
summary: "account/session setup command failed before any business turn could be observed.",
rootCause: `account-session-command-stage-failed:${failure ?? "unknown"}`,
rootCauseStatus: "confirmed",
rootCauseConfidence: "high",
evidenceSummary: `stage=account-session-command failure=${failure ?? "-"}`,
nextAction: "Inspect the first failed account/session/workbench command and rerun quick verify after that command succeeds.",
});
}
if (ids.includes("observer-command-failed") && !hasCommandFailureEvidence(item)) {
return retagVisibleFinding(item, "observer-command-failure-evidence-missing", {
summary: "report says observer control commands failed, but the first failed command evidence is missing.",
rootCause: "observer-command-failure-evidence-missing",
rootCauseStatus: "evidence-gap",
rootCauseConfidence: "high",
evidenceSummary: "firstCommandFailure missing from indexed and artifact findings",
nextAction: "Fix analyzer/report commandFailures compact output before using this finding as a page root cause.",
});
}
if (ids.includes("quick-verify-observer-stop-failed")) {
const evidence = `${stringOrNull(item.evidenceSummary) ?? ""} ${stringOrNull(item.summary) ?? ""}`;
if (/aliveAfter\s*=\s*true/u.test(evidence)) {
return retagVisibleFinding(item, "observer-force-stop-left-process-alive", {
summary: "observer force stop completed but process liveness evidence still reports aliveAfter=true.",
rootCause: "observer-process-still-alive-after-force-stop",
rootCauseStatus: "confirmed",
rootCauseConfidence: "high",
});
}
if (/aliveAfter\s*=\s*false/u.test(evidence)) {
return retagVisibleFinding(item, "observer-graceful-stop-timeout-force-stop-succeeded", {
severity: "warning",
level: "warning",
summary: "observer graceful stop did not drain before timeout, but force stop confirmed aliveAfter=false.",
rootCause: "observer-graceful-stop-timeout-force-stop-succeeded",
rootCauseStatus: "confirmed",
rootCauseConfidence: "high",
});
}
return retagVisibleFinding(item, "observer-cleanup-evidence-missing", {
summary: "observer cleanup stop-state evidence is missing or incomplete; this is an evidence gap, not confirmed process leakage.",
rootCause: "observer-cleanup-evidence-missing",
rootCauseStatus: "evidence-gap",
rootCauseConfidence: "medium",
evidenceSummary: stringOrNull(item.evidenceSummary) ?? "cleanup evidence missing",
});
}
if (ids.includes("no-samples")) {
if (hasObserverCommandFailure) {
return retagVisibleFinding(item, "no-samples-after-observer-command-failed", {
summary: "observer produced zero samples after a blocking control command failed; the command failure is the deterministic precondition.",
rootCause: "observer-command-failed-before-sampling",
rootCauseStatus: "confirmed",
rootCauseConfidence: "high",
});
}
return retagVisibleFinding(item, "no-samples-artifact-evidence-missing", {
summary: "observer produced zero samples and analyzer has no failed command precondition; sampling/source evidence is missing.",
rootCause: "sample-artifact-evidence-missing",
rootCauseStatus: "evidence-gap",
rootCauseConfidence: "medium",
});
}
return item;
}
function hasCommandFailureEvidence(item: Record<string, unknown>): boolean {
const first = record(item.firstCommandFailure ?? firstArrayRecord(item.commands));
return stringOrNull(first.type) !== null
|| stringOrNull(first.phase) !== null
|| stringOrNull(first.status) !== null
|| stringOrNull(first.stdoutPreview) !== null
|| stringOrNull(first.stderrPreview) !== null
|| numberOrNull(first.exitCode) !== null;
}
function retagVisibleFinding(item: Record<string, unknown>, id: string, extra: Record<string, unknown>): Record<string, unknown> {
return {
...item,
...extra,
id,
kind: id,
code: id,
finding_id: id,
findingId: id,
check: null,
checkId: null,
checkCode: null,
checkTitleZh: null,
valuesRedacted: true,
};
}
function findingIdentity(item: Record<string, unknown>): string | null {
return stringOrNull(item.finding_id)
?? stringOrNull(item.findingId)
@@ -1871,6 +1991,7 @@ function enrichFindingRowWithStoredDetail(config: WebProbeSentinelServiceConfig,
rootCauseConfidence: stringOrNull(detail?.rootCauseConfidence),
nextAction: stringOrNull(detail?.nextAction),
evidenceSummary: stringOrNull(detail?.evidenceSummary),
firstCommandFailure: compactCommandFailureEvidence(detail?.firstCommandFailure ?? firstArrayRecord(detail?.commands)),
rootCauseSignals: record(detail?.rootCauseSignals),
timingSourceOfTruth: stringOrNull(detail?.timingSourceOfTruth),
timingStatus: stringOrNull(detail?.timingStatus),
@@ -1918,6 +2039,7 @@ function compactStoredFinding(value: unknown): Record<string, unknown> {
rootCauseConfidence: stringOrNull(item.rootCauseConfidence),
nextAction: stringOrNull(item.nextAction),
evidenceSummary: stringOrNull(item.evidenceSummary) ?? compactFindingEvidenceSummary(item.evidence),
firstCommandFailure: compactCommandFailureEvidence(item.firstCommandFailure ?? firstArrayRecord(item.commands)),
rootCauseSignals: compactFindingRootCauseSignals(item.rootCauseSignals),
timingSourceOfTruth: stringOrNull(item.timingSourceOfTruth) ?? stringOrNull(item.expectedElapsedSource) ?? stringOrNull(item.evidenceKind),
timingStatus: stringOrNull(item.timingStatus),
@@ -1926,6 +2048,27 @@ function compactStoredFinding(value: unknown): Record<string, unknown> {
};
}
function compactCommandFailureEvidence(value: unknown): Record<string, unknown> | null {
const item = record(value);
if (Object.keys(item).length === 0) return null;
return {
commandId: stringOrNull(item.commandId),
type: stringOrNull(item.type),
phase: stringOrNull(item.phase),
status: stringOrNull(item.status),
exitCode: numberOrNull(item.exitCode),
timedOut: item.timedOut === true ? true : item.timedOut === false ? false : null,
stdoutPreview: stringOrNull(item.stdoutPreview)?.slice(0, 240) ?? null,
stderrPreview: stringOrNull(item.stderrPreview)?.slice(0, 240) ?? null,
artifactRef: stringOrNull(item.artifactRef),
valuesRedacted: true,
};
}
function firstArrayRecord(value: unknown): Record<string, unknown> | null {
return Array.isArray(value) && value.length > 0 ? record(value[0]) : null;
}
function compactFindingRootCauseSignals(value: unknown): Record<string, unknown> | null {
const item = record(value);
const keys = [