// SPEC: PJ2026-01060508 Web哨兵 draft-2026-06-27-p11-monitor-web-observability-dashboard.
// SPEC: PJ2026-01060508 Web哨兵 draft-2026-07-01-p15-cadence-otel.
// 职责：提供 Sentinel P5 quick-verify 的有界制品分析、子 CLI 与验证辅助能力。
import { createHash } from "node:crypto";
import { existsSync, readFileSync } from "node:fs";
import type { CommandResult } from "./command";
import { runCommand } from "./command";
import { resolveCliChildJsonCommandResult } from "./cli-child-json-recovery";
import { repoRoot, rootPath } from "./config";
import { readWebProbeSentinelConfigRefTarget } from "./hwlab-node-web-sentinel-config-ref";
import type { ChildCliResult, CompactCommandResult, SentinelCicdState } from "./hwlab-node-web-sentinel-cicd-shared";
import {
  arrayAt,
  compactCommand,
  displayPath,
  isRecord,
  numberAtNullable,
  parseEnvFile,
  parseJsonObject,
  record,
  recordTarget,
  secretSourcePaths,
  sentinelCliSuffix,
  shellQuote,
  stringAt,
  stringAtNullable,
  text,
} from "./hwlab-node-web-sentinel-cicd-shared";

export function analysisSummaryFromAnalyzeResult(analysis: ChildCliResult, fallbackStateDir: string | null): Record<string, unknown> | null {
  const payload = cliDataPayload(analysis.parsed);
  const source = record(payload.analysis);
  const analyzer = record(source.analyzer);
  const reportJsonSha256 = stringAtNullable(source, "reportJsonSha256") ?? stringAtNullable(analyzer, "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") ?? stringAtNullable(analyzer, "stateDir") ?? fallbackStateDir,
    reportJsonPath: stringAtNullable(source, "reportJsonPath") ?? stringAtNullable(analyzer, "reportJsonPath"),
    reportJsonSha256,
    reportJsonBytes: numberAtNullable(source, "reportJsonBytes") ?? numberAtNullable(analyzer, "reportJsonBytes"),
    reportUpdatedAt: stringAtNullable(source, "reportUpdatedAt") ?? stringAtNullable(analyzer, "reportUpdatedAt"),
    reportMdPath: stringAtNullable(source, "reportMdPath") ?? stringAtNullable(analyzer, "reportMdPath"),
    reportMdSha256: stringAtNullable(source, "reportMdSha256") ?? stringAtNullable(analyzer, "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),
    requestRate: record(source.requestRate),
    requestRateCurve: record(source.requestRateCurve),
    requestRatePeaks: Array.isArray(source.requestRatePeaks) ? source.requestRatePeaks.slice(0, 12).map(record) : [],
    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,
  };
}

export function readAnalysisSummaryFromWorkspace(state: SentinelCicdState, stateDir: string, timeoutSeconds: number, execute: (script: string) => CommandResult = (script) => runCommand(["trans", `${state.spec.nodeId}:${state.spec.workspace}`, "sh"], repoRoot, { input: script, timeoutMs: Math.min(timeoutSeconds, 60) * 1000 })): 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));
  const script = [
    "set -eu",
    `state_dir=${shellQuote(stateDir)}`,
    `wait_ms=${waitMs}`,
    "node - \"$state_dir\" \"$wait_ms\" <<'NODE'",
    "const fs=require('node:fs'); const path=require('node:path'); const crypto=require('node:crypto');",
    "const stateDir=process.argv[2]; const waitMs=Math.max(0, Math.min(60000, Number(process.argv[3]||0)||0)); const reportPath=path.join(stateDir,'analysis','report.json'); const reportMdPath=path.join(stateDir,'analysis','report.md');",
    "const read=(p)=>{try{return fs.readFileSync(p)}catch{return null}};",
    "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 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 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};});",
    "const compactRequestRateBucket=(value)=>{const v=rec(value); return {startAt:clip(v.startAt??v.t,40),endAt:clip(v.endAt??v.e,40),count:num(v.count??v.c),requestPerMinute:num(v.requestPerMinute??v.rpm),valuesRedacted:true};};",
    "const compactRequestRateCurve=(value)=>{const v=rec(value); const buckets=arr(v.buckets).map(compactRequestRateBucket).filter((bucket)=>bucket.startAt&&bucket.requestPerMinute!=null).slice(-120); return {pageKey:clip(v.pageKey,120),pageRole:clip(v.pageRole,32),pageId:clip(v.pageId,80),pageEpoch:num(v.pageEpoch),apiKey:clip(v.apiKey,140),method:clip(v.method,16),path:clip(v.path,160),count:num(v.count),bucketCount:num(v.bucketCount)??buckets.length,peakRequestPerMinute:num(v.peakRequestPerMinute),peakBucket:compactRequestRateBucket(v.peakBucket),buckets,valuesRedacted:true};};",
    "const compactRequestRate=(value)=>{const v=rec(value); const summary=rec(v.summary); const s=Object.keys(summary).length?summary:v; const buckets=arr(v.buckets).map(compactRequestRateBucket).filter((bucket)=>bucket.startAt&&bucket.requestPerMinute!=null).slice(-120); const pageCurves=arr(v.pageCurves).map(compactRequestRateCurve).filter((item)=>item.buckets.length>0).slice(0,12); const apiPathCurves=arr(v.apiPathCurves).map(compactRequestRateCurve).filter((item)=>item.buckets.length>0).slice(0,16); if(!s.requestCount&&!buckets.length&&!pageCurves.length&&!apiPathCurves.length)return null; return {summary:{bucketMs:num(s.bucketMs),bucketSeconds:num(s.bucketSeconds),requestCount:num(s.requestCount),bucketCount:num(s.bucketCount),pageCount:num(s.pageCount),apiPathCount:num(s.apiPathCount),firstAt:clip(s.firstAt,40),lastAt:clip(s.lastAt,40),totalPeakPerMinute:num(s.totalPeakPerMinute),pagePeakPerMinute:num(s.pagePeakPerMinute),apiPathPeakPerMinute:num(s.apiPathPeakPerMinute),overThresholdPeakCount:num(s.overThresholdPeakCount),valuesRedacted:true},buckets,pageCurves,apiPathCurves,peaks:arr(v.peaks).slice(0,12),valuesRedacted:true};};",
    "const num=(v)=>Number.isFinite(Number(v))?Number(v):null; const mb=(v)=>{const n=num(v); return n==null?null:Math.round((n/1024/1024)*100)/100};",
    "function pageMemory(page){const eff=rec(page.effectiveMemory); const heap=rec(page.heapUsage); const perf=rec(page.performance); const metrics=rec(perf.metrics); const heapUsed=num(eff.heapUsedMb)??mb(heap.usedSize); const jsHeap=num(eff.jsHeapUsedMb)??mb(metrics.JSHeapUsedSize); const memory=heapUsed??jsHeap; if(memory==null)return null; return {memoryMb:memory,heapUsedMb:heapUsed,jsHeapUsedMb:jsHeap,effectiveHeapUsedMb:num(eff.effectiveHeapUsedMb),effectiveJsHeapUsedMb:num(eff.effectiveJsHeapUsedMb),domNodes:num(eff.domNodes)??num(rec(page.domCounters).nodes)??num(metrics.Nodes)}; }",
    "function browserProcessPageSeries(){const p=path.join(stateDir,'browser-process.jsonl'); let lines=[]; try{lines=String(read(p)||'').split(/\\r?\\n/).filter(Boolean)}catch{} const map=new Map(); let firstTs=null; for(const line of lines){let row=null; try{row=JSON.parse(line)}catch{} if(!row||row.type!=='browser-process-sample')continue; const ts=row.ts||null; const tsMs=Date.parse(String(ts||'')); if(!ts||!Number.isFinite(tsMs))continue; for(const page of arr(row.pages)){const memory=pageMemory(rec(page)); if(!memory)continue; if(firstTs==null)firstTs=tsMs; const role=clip(page.pageRole||'page',32); const id=clip(page.pageId||`${role}-${page.pageEpoch??map.size}`,80); const key=`${role}:${id}`; const current=map.get(key)||{key,pageRole:role,pageId:id,label:`${role} ${String(id).slice(0,10)}`,url:clip(page.url,180),points:[],valuesRedacted:true}; current.points.push({ts,elapsedSeconds:Math.max(0,Math.round((tsMs-firstTs)/1000)),elapsedMinutes:Math.round(Math.max(0,(tsMs-firstTs)/60000)*100)/100,...memory,valuesRedacted:true}); map.set(key,current);} } return Array.from(map.values()).filter((item)=>item.points.length>0); }",
    "const browserPageSeries=browserProcessPageSeries();",
    "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),reportJsonBytes:jsonBuf.length,reportUpdatedAt:typeof report?.updatedAt==='string'?report.updatedAt:(typeof report?.analyzedAt==='string'?report.analyzedAt:(typeof report?.generatedAt==='string'?report.generatedAt:null)),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,browserProcess:{source:'analysis-browser-process-jsonl',unit:'MB',metric:'page-heap-used',pageCount:browserPageSeries.length,sampleCount:browserPageSeries.reduce((sum,item)=>sum+item.points.length,0),pageSeries:browserPageSeries,valuesRedacted:true},requestRate:compactRequestRate(report?.requestRate),valuesRedacted:true}));",
    "NODE",
  ].join("\n");
  const result = execute(script);
  const parsedResolution = resolveCliChildJsonCommandResult({
    result,
    requestedStdoutType: "web-probe sentinel quick-verify analysis summary JSON",
    acceptParsed: (value) => value.ok === true || value.ok === false || typeof value.reason === "string" || typeof value.reportJsonPath === "string" || Object.keys(record(value.counts)).length > 0,
  });
  const parsed = parsedResolution.parsed;
  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),
    stdoutRecovery: parsedResolution.diagnostics,
    valuesRedacted: true,
  };
}

export function waitForQuickVerifyObserverStartup(state: SentinelCicdState, observerId: string, deadline: number, pollIntervalMs: number, budgetSeconds: number): Record<string, unknown> {
  const observations: Record<string, unknown>[] = [];
  const indexEntry = readLocalObserveIndex(observerId);
  if (indexEntry === null) {
    return {
      ok: false,
      failure: "observe-index-entry-missing",
      observerId,
      valuesRedacted: true,
    };
  }
  const pollSleepMs = Math.max(250, Math.min(500, Math.trunc(pollIntervalMs / 2) || 250));
  while (Date.now() < deadline) {
    const waitMs = Math.max(1000, Math.min(55_000, deadline - Date.now()));
    const script = quickVerifyObserverStartupWaitScript(indexEntry.stateDir, waitMs, pollSleepMs);
    const result = runCommand(["trans", `${state.spec.nodeId}:${state.spec.workspace}`, "sh"], repoRoot, { input: script, timeoutMs: waitMs + 5000 });
    const payloadResolution = resolveCliChildJsonCommandResult({
      result,
      requestedStdoutType: "quick-verify observer startup wait JSON",
      acceptParsed: (value) => typeof value.status === "string" || typeof value.heartbeatStatus === "string" || Array.isArray(value.observations) || Object.keys(record(value.startup)).length > 0,
    });
    const payload = payloadResolution.parsed;
    if (Array.isArray(payload?.observations)) observations.push(...payload.observations.map(record));
    const terminalPayload = {
      observerId,
      stateDir: indexEntry.stateDir,
      status: typeof payload?.status === "string" ? payload.status : null,
      heartbeatStatus: typeof payload?.heartbeatStatus === "string" ? payload.heartbeatStatus : null,
      startup: record(payload?.startup),
      observations: observations.slice(-6),
      waitResult: { ...compactCommand(result), stdoutRecovery: payloadResolution.diagnostics },
      valuesRedacted: true,
    };
    if (result.exitCode !== 0 || payload === null || payload.ok === false && payload.failure !== "quick-verify-startup-wait-chunk-timeout") {
      return {
        ok: false,
        failure: text(payload?.failure ?? "quick-verify-startup-artifact-wait-failed"),
        ...terminalPayload,
      };
    }
    if (payload.ok === true) return { ok: true, ...terminalPayload };
  }
  return {
    ok: false,
    failure: "quick-verify-timeout-over-budget",
    observerId,
    stateDir: indexEntry.stateDir,
    observations: observations.slice(-6),
    warnings: [`quick verify exceeded the configured ${budgetSeconds}s targetValidation budget while waiting for the observe runner startup to finish before sending the first command.`],
    valuesRedacted: true,
  };
}

function quickVerifyObserverStartupWaitScript(stateDir: string, timeoutMs: number, pollSleepMs: number): string {
  return [
    "set -eu",
    `state_dir=${shellQuote(stateDir)}`,
    `timeout_ms=${shellQuote(String(Math.max(1, Math.trunc(timeoutMs))))}`,
    `poll_ms=${shellQuote(String(Math.max(250, Math.trunc(pollSleepMs))))}`,
    "test -d \"$state_dir\" || { printf '{\"ok\":false,\"failure\":\"state-dir-missing\",\"stateDir\":\"%s\",\"valuesRedacted\":true}\\n' \"$state_dir\"; exit 0; }",
    "node - \"$state_dir\" \"$timeout_ms\" \"$poll_ms\" <<'NODE'",
    "const fs = require('node:fs');",
    "const path = require('node:path');",
    "const dir = process.argv[2];",
    "const timeoutMs = Number(process.argv[3]);",
    "const pollMs = Number(process.argv[4]);",
    "const startedAt = Date.now();",
    "const startupIds = ['startup-login', 'startup-goto', 'startup-observer-goto'];",
    "const readJson = (rel) => { try { return JSON.parse(fs.readFileSync(path.join(dir, rel), 'utf8')); } catch { return null; } };",
    "const readJsonl = (rel) => { try { return fs.readFileSync(path.join(dir, rel), 'utf8').split(/\\r?\\n/u).filter(Boolean).map((line) => { try { return JSON.parse(line); } catch { return null; } }).filter(Boolean); } catch { return []; } };",
    "const clip = (value, limit = 160) => value == null ? null : String(value).replace(/\\s+/gu, ' ').trim().slice(0, limit);",
    "const norm = (value) => String(value || '').trim().toLowerCase().replace(/_/gu, '-');",
    "const terminal = new Set(['failed', 'force-stopped', 'stopped', 'abandoned', 'completed']);",
    "function commandEvents(control, id) { return control.filter((item) => item && item.commandId === id); }",
    "function lastPhase(control, id) { return commandEvents(control, id).filter((item) => typeof item.phase === 'string').slice(-1)[0]?.phase || null; }",
    "function firstFailedStartup(control) { return control.filter((item) => item && startupIds.includes(item.commandId) && item.phase === 'failed').slice(-1)[0] || null; }",
    "function rowFor() {",
    "  const heartbeat = readJson('heartbeat.json') || {};",
    "  const manifest = readJson('manifest.json') || {};",
    "  const control = readJsonl('control.jsonl');",
    "  const phases = Object.fromEntries(startupIds.map((id) => [id, lastPhase(control, id)]));",
    "  const failed = firstFailedStartup(control);",
    "  const heartbeatStatus = norm(heartbeat.status || manifest.status);",
    "  const ready = startupIds.every((id) => phases[id] === 'completed') && heartbeatStatus === 'running';",
    "  const terminalBeforeReady = !ready && terminal.has(heartbeatStatus);",
    "  const degraded = control.filter((item) => item && item.type === 'observer-startup-degraded').slice(-1)[0] || null;",
    "  return {",
    "    ok: ready,",
    "    status: ready ? 'startup-ready' : terminalBeforeReady ? 'startup-terminal' : 'startup-waiting',",
    "    heartbeatStatus,",
    "    startup: { phases, failedCommandId: failed?.commandId || null, failedType: failed?.type || null, failedMessage: clip(failed?.detail?.error?.message || failed?.detail?.error || failed?.error?.message), observerStartupDegraded: !!degraded, degradedReason: clip(degraded?.reason || degraded?.result?.failureKind || degraded?.result?.reason), sampleSeq: heartbeat.sampleSeq ?? null, commandSeq: heartbeat.commandSeq ?? null, currentUrl: clip(heartbeat.currentUrl, 180), observerUrl: clip(heartbeat.observerUrl, 180), valuesRedacted: true },",
    "    valuesRedacted: true",
    "  };",
    "}",
    "const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));",
    "(async () => {",
    "  const observations = [];",
    "  while (Date.now() - startedAt <= timeoutMs) {",
    "    const row = rowFor();",
    "    observations.push(row);",
    "    if (row.ok === true) { console.log(JSON.stringify({ ok: true, ...row, observations: observations.slice(-6), elapsedMs: Date.now() - startedAt, valuesRedacted: true })); return; }",
    "    if (row.startup.failedCommandId) { console.log(JSON.stringify({ ok: false, failure: 'observer-startup-command-failed', ...row, observations: observations.slice(-6), elapsedMs: Date.now() - startedAt, valuesRedacted: true })); return; }",
    "    if (row.status === 'startup-terminal') { console.log(JSON.stringify({ ok: false, failure: 'observer-startup-terminal', ...row, observations: observations.slice(-6), elapsedMs: Date.now() - startedAt, valuesRedacted: true })); return; }",
    "    await sleep(Math.min(pollMs, Math.max(0, timeoutMs - (Date.now() - startedAt))));",
    "  }",
    "  const row = rowFor();",
    "  observations.push(row);",
    "  console.log(JSON.stringify({ ok: false, failure: 'quick-verify-startup-wait-chunk-timeout', ...row, observations: observations.slice(-6), elapsedMs: Date.now() - startedAt, valuesRedacted: true }));",
    "})().catch((error) => { console.log(JSON.stringify({ ok: false, failure: 'quick-verify-startup-wait-script-error', error: error instanceof Error ? error.message : String(error), valuesRedacted: true })); });",
    "NODE",
  ].join("\n");
}

export function collectObserveView(state: SentinelCicdState, observerId: string, view: "turn-summary" | "trace-frame", turn: number | null, timeoutSeconds: number): Record<string, unknown> {
  const args = ["web-probe", "observe", "collect", observerId, "--node", state.spec.nodeId, "--lane", state.spec.lane, "--view", view, "--command-timeout-seconds", String(Math.max(5, Math.min(timeoutSeconds, 55))), "--raw", "--compact-raw"];
  if (turn !== null) args.push("--turn", String(turn));
  const result = runChildCli(args, timeoutSeconds);
  const payload = cliDataPayload(result.parsed);
  const collect = observeCollectPayload(payload);
  const payloadData = record(payload.data);
  return {
    ok: result.ok && result.parsed !== null && payload.ok !== false && collect.ok !== false,
    view,
    renderedText: typeof collect.renderedText === "string"
      ? collect.renderedText
      : typeof payload.renderedText === "string"
        ? payload.renderedText
        : typeof payloadData.renderedText === "string"
          ? payloadData.renderedText
          : String(record(result.result).stdoutTail ?? record(result.result).stdoutPreview ?? ""),
    collect,
    payload,
    collectShape: observeCollectShape(payload, collect),
    result: result.result,
    valuesRedacted: true,
  };
}

function observeCollectPayload(payload: Record<string, unknown>): Record<string, unknown> {
  const candidates = [
    record(payload.collect),
    record(record(payload.data).collect),
    record(record(record(payload.data).data).collect),
  ];
  return candidates.find((item) => Object.keys(item).length > 0) ?? {};
}

function observeCollectShape(payload: Record<string, unknown>, collect: Record<string, unknown>): Record<string, unknown> {
  const direct = record(payload.collect);
  const nested = record(record(payload.data).collect);
  return {
    payloadKeys: Object.keys(payload).slice(0, 12),
    directCollect: Object.keys(direct).length > 0,
    nestedDataCollect: Object.keys(nested).length > 0,
    collectKeys: Object.keys(collect).slice(0, 12),
    rowCount: Array.isArray(collect.rows) ? collect.rows.length : null,
    traceId: stringAtNullable(collect, "traceId"),
    valuesRedacted: true,
  };
}

export function runChildCli(args: string[], timeoutSeconds: number, input?: string, env?: NodeJS.ProcessEnv): ChildCliResult {
  const result = runCommand(["bun", "scripts/cli.ts", ...args], repoRoot, {
    input,
    env: env === undefined ? undefined : { ...process.env, ...env },
    timeoutMs: Math.max(5, Math.min(timeoutSeconds, 120)) * 1000,
  });
  return {
    ok: result.exitCode === 0 && !result.timedOut,
    parsed: resolveCliChildJsonCommandResult({
      result,
      requestedStdoutType: `child CLI ${args.join(" ")} JSON`,
    }).parsed,
    result: compactCommandWithTail(result),
  };
}

export function waitForQuickVerifyPromptTurn(state: SentinelCicdState, observerId: string, promptIndex: number, deadline: number, pollIntervalMs: number, budgetSeconds: number, chunkSeconds = 45): Record<string, unknown> {
  const observations: Record<string, unknown>[] = [];
  const indexEntry = readLocalObserveIndex(observerId);
  if (indexEntry === null) {
    return {
      ok: false,
      failure: "observe-index-entry-missing",
      round: promptIndex,
      observerId,
      valuesRedacted: true,
    };
  }
  const pollSleepMs = Math.max(1000, Math.min(3000, Math.trunc(pollIntervalMs * 2) || 1000));
  while (Date.now() < deadline) {
    const waitMs = Math.max(1000, Math.min(Math.max(1000, Math.trunc(chunkSeconds * 1000)), deadline - Date.now()));
    const script = quickVerifyPromptWaitScript(indexEntry.stateDir, promptIndex, waitMs, pollSleepMs);
    const result = runCommand(["trans", `${state.spec.nodeId}:${state.spec.workspace}`, "sh"], repoRoot, { input: script, timeoutMs: waitMs + 8000 });
    const payloadResolution = resolveCliChildJsonCommandResult({
      result,
      requestedStdoutType: "quick-verify prompt wait JSON",
      acceptParsed: (value) => typeof value.status === "string" || typeof value.traceId === "string" || Array.isArray(value.observations) || value.finalResponseEmpty === true || value.composerReadyForTurn === true,
    });
    const payload = payloadResolution.parsed;
    if (Array.isArray(payload?.observations)) observations.push(...payload.observations.map(record));
    const status = typeof payload?.status === "string" ? payload.status : null;
    const terminalPayload = {
      round: promptIndex,
      status,
      traceId: payload?.traceId ?? null,
      finalResponseEmpty: payload?.finalResponseEmpty === true,
      composerReadyForTurn: payload?.composerReadyForTurn === true,
      composerAction: typeof payload?.composerAction === "string" ? payload.composerAction : null,
      observations: observations.slice(-6),
      waitResult: { ...compactCommand(result), stdoutRecovery: payloadResolution.diagnostics },
      valuesRedacted: true,
    };
    if (result.exitCode !== 0 || payload === null || payload.ok === false && payload.failure !== "quick-verify-wait-chunk-timeout") {
      const fallback = quickVerifyTurnSummaryFallback(state, observerId, promptIndex);
      if (fallback.ok === true) {
        return {
          ok: true,
          ...terminalPayload,
          status: stringAtNullable(fallback, "status") ?? status,
          traceId: stringAtNullable(fallback, "traceId") ?? payload?.traceId ?? null,
          finalResponseEmpty: false,
          fallback,
          warnings: ["quick verify artifact wait command failed, but bounded turn-summary artifacts show this round completed; continuing validation."],
        };
      }
      return {
        ok: false,
        failure: text(payload?.failure ?? "quick-verify-artifact-wait-failed"),
        ...terminalPayload,
        fallback,
      };
    }
    if (payload.ok === false && payload.failure === "quick-verify-wait-chunk-timeout") {
      const fallback = quickVerifyTurnSummaryFallback(state, observerId, promptIndex);
      if (fallback.ok === true) {
        return {
          ok: true,
          ...terminalPayload,
          status: stringAtNullable(fallback, "status") ?? status,
          traceId: stringAtNullable(fallback, "traceId") ?? payload.traceId ?? null,
          finalResponseEmpty: false,
          fallback,
          warnings: ["quick verify wait chunk timed out, but bounded turn-summary artifacts show this round completed; continuing validation."],
        };
      }
    }
    if (payload.ok === true) return { ok: true, ...terminalPayload };
    if (isQuickVerifyTurnSuccessful(status)) {
      if (payload?.finalResponseEmpty !== true) return { ok: true, ...terminalPayload };
      continue;
    }
    if (isQuickVerifyTurnTerminal(status)) {
      return {
        ok: false,
        failure: "observe-turn-terminal-non-success",
        ...terminalPayload,
      };
    }
  }
  return {
    ok: false,
    failure: "quick-verify-timeout-over-budget",
    round: promptIndex,
    observations: observations.slice(-6),
    warnings: [`quick verify exceeded the configured ${budgetSeconds}s targetValidation budget while waiting for a submitted turn to become terminal; investigate Code Agent multi-round continuity before retrying.`],
    valuesRedacted: true,
  };
}

function quickVerifyPromptWaitScript(stateDir: string, promptIndex: number, timeoutMs: number, pollSleepMs: number): string {
  return [
    "set -eu",
    `state_dir=${shellQuote(stateDir)}`,
    `prompt_index=${shellQuote(String(promptIndex))}`,
    `timeout_ms=${shellQuote(String(Math.max(1, Math.trunc(timeoutMs))))}`,
    `poll_ms=${shellQuote(String(Math.max(250, Math.trunc(pollSleepMs))))}`,
    "test -d \"$state_dir\" || { printf '{\"ok\":false,\"failure\":\"state-dir-missing\",\"stateDir\":\"%s\",\"valuesRedacted\":true}\\n' \"$state_dir\"; exit 0; }",
    "node - \"$state_dir\" \"$prompt_index\" \"$timeout_ms\" \"$poll_ms\" <<'NODE'",
    "const fs = require('node:fs');",
    "const path = require('node:path');",
    "const dir = process.argv[2];",
    "const promptIndex = Number(process.argv[3]);",
    "const timeoutMs = Number(process.argv[4]);",
    "const pollMs = Number(process.argv[5]);",
    "const startedAt = Date.now();",
    "const short = (value, limit = 160) => String(value || '').replace(/\\s+/gu, ' ').trim().slice(0, limit);",
    "const textOf = (value) => String(value?.text || value?.textPreview || value?.preview || '');",
    "const arr = (value) => Array.isArray(value) ? value : [];",
    "const unique = (values) => Array.from(new Set(values.filter(Boolean)));",
    "const numOrNull = (value) => { const n = Number(value); return Number.isFinite(n) ? n : null; };",
    "const tsMs = (value) => { const ms = Date.parse(String(value || '')); return Number.isFinite(ms) ? ms : null; };",
    "const readJson = (rel) => { try { return JSON.parse(fs.readFileSync(path.join(dir, rel), 'utf8')); } catch { return null; } };",
    "const readJsonl = (rel) => { try { return fs.readFileSync(path.join(dir, rel), 'utf8').split(/\\r?\\n/u).filter(Boolean).map((line) => { try { return JSON.parse(line); } catch { return null; } }).filter(Boolean); } catch { return []; } };",
    "const readJsonlTail = (rel, maxBytes = 2000000) => {",
    "  try {",
    "    const file = path.join(dir, rel);",
    "    const stat = fs.statSync(file);",
    "    const start = Math.max(0, stat.size - maxBytes);",
    "    const length = stat.size - start;",
    "    const fd = fs.openSync(file, 'r');",
    "    try {",
    "      const buffer = Buffer.alloc(length);",
    "      fs.readSync(fd, buffer, 0, length, start);",
    "      const lines = buffer.toString('utf8').split(/\\r?\\n/u);",
    "      if (start > 0) lines.shift();",
    "      return lines.filter(Boolean).map((line) => { try { return JSON.parse(line); } catch { return null; } }).filter(Boolean);",
    "    } finally {",
    "      fs.closeSync(fd);",
    "    }",
    "  } catch {",
    "    return [];",
    "  }",
    "};",
    "const readDone = (id) => id ? readJson(path.join('commands', 'done', `${id}.json`)) : null;",
    "const readFailed = (id) => id ? readJson(path.join('commands', 'failed', `${id}.json`)) : null;",
    "function sessionIdFromUrl(value) { const match = String(value || '').match(/\\/workbench\\/sessions\\/(ses_[A-Za-z0-9_-]+)/u); return match ? match[1] : null; }",
    "function commandSessionId(item) { const done = readDone(item?.commandId); return item?.sessionId || item?.detail?.sessionId || item?.input?.sessionId || item?.result?.sessionId || done?.result?.sessionId || done?.result?.observer?.sessionId || sessionIdFromUrl(item?.afterUrl) || sessionIdFromUrl(item?.detail?.afterUrl) || sessionIdFromUrl(done?.result?.afterUrl) || null; }",
    "function commandTextHash(item) { const done = readDone(item?.commandId); return item?.detail?.textHash || item?.input?.textHash || item?.result?.textHash || done?.result?.textHash || null; }",
    "function firstTraceId(value) { const match = String(value || '').match(/\\btrc_[A-Za-z0-9_-]+\\b/u); return match ? match[0] : null; }",
    "function commandTraceId(item) { const done = readDone(item?.commandId); return item?.traceId || item?.detail?.chatSubmit?.traceId || item?.detail?.traceId || item?.input?.traceId || item?.result?.chatSubmit?.traceId || done?.result?.chatSubmit?.traceId || done?.result?.traceId || null; }",
    "function itemTraceId(item) { return item?.traceId || firstTraceId(textOf(item)) || null; }",
    "function completedNewSessionIdsBefore(control, ts) { const limit = tsMs(ts); return control.filter((item) => item.type === 'newSession' && item.phase === 'completed').filter((item) => limit === null || tsMs(item.ts) === null || tsMs(item.ts) <= limit).map(commandSessionId).filter(Boolean); }",
    "function authoritativeSessionIdForPrompts(control, prompts) { const ids = completedNewSessionIdsBefore(control, prompts[0]?.firstTs || null); return ids.slice(-1)[0] || unique(prompts.map((item) => item.sessionId))[0] || null; }",
    "function promptCommands(control) {",
    "  const map = new Map();",
    "  for (const item of control) {",
    "    if (item.type !== 'sendPrompt' || !['started', 'completed', 'failed'].includes(item.phase)) continue;",
    "    const id = item.commandId || item.seq || String(map.size + 1);",
    "    const existing = map.get(id) || {};",
    "    map.set(id, { ...existing, ...item, input: { ...(existing.input || {}), ...(item.input || {}) }, sessionId: existing.sessionId || commandSessionId(item), traceId: existing.traceId || commandTraceId(item), textHash: existing.textHash || commandTextHash(item), firstTs: existing.firstTs || item.ts, lastTs: item.ts });",
    "  }",
    "  const prompts = Array.from(map.values()).filter((item) => tsMs(item.firstTs) !== null).sort((a, b) => tsMs(a.firstTs) - tsMs(b.firstTs));",
    "  const sessionId = authoritativeSessionIdForPrompts(control, prompts);",
    "  if (!sessionId) return prompts;",
    "  const scoped = prompts.filter((item) => item.sessionId === sessionId);",
    "  return scoped.length > 0 ? scoped : prompts;",
    "}",
    "function segmentFor(samples, prompts, index) { const start = tsMs(prompts[index]?.firstTs); const end = index + 1 < prompts.length ? tsMs(prompts[index + 1].firstTs) : Infinity; return samples.filter((sample) => { const ms = tsMs(sample.ts); return ms !== null && ms >= start && ms < end; }); }",
    "function entryGroups(sample) { return [...arr(sample.turns).map((item) => ({ group: 'turn', item })), ...arr(sample.traceRows).map((item) => ({ group: 'traceRow', item })), ...arr(sample.messages).map((item) => ({ group: 'message', item }))]; }",
    "function traceIdsFromSamples(items) { const ids = []; for (const sample of items) for (const entry of entryGroups(sample)) { const id = itemTraceId(entry.item); if (id) ids.push(id); } return unique(ids); }",
    "function chooseTraceId(segment, prompt) { const promptTraceId = commandTraceId(prompt) || prompt?.traceId || null; const ids = traceIdsFromSamples(segment); if (promptTraceId && (ids.length === 0 || ids.includes(promptTraceId))) return promptTraceId; return ids.slice(-1)[0] || promptTraceId || null; }",
    "function traceIdForPromptUserMessage(items, prompt) {",
    "  const hash = prompt?.textHash || commandTextHash(prompt);",
    "  if (!hash) return null;",
    "  for (const sample of items) {",
    "    for (const message of arr(sample.messages)) {",
    "      const role = String(message?.role || message?.dataRole || message?.messageRole || '').toLowerCase();",
    "      if (role && !/user/u.test(role)) continue;",
    "      if (message?.textHash === hash) {",
    "        const id = itemTraceId(message);",
    "        if (id) return id;",
    "      }",
    "    }",
    "  }",
    "  return null;",
    "}",
    "function traceEntries(items, traceId) { const entries = []; for (const sample of items) for (const entry of entryGroups(sample)) { const text = textOf(entry.item); const id = itemTraceId(entry.item); if (!traceId || id === traceId || text.includes(traceId)) entries.push({ ...entry, sample, text }); } return entries; }",
    "function normalizeLifecycleStatus(value) { const raw = String(value || '').trim().toLowerCase(); if (/^(canceled|cancelled)$/u.test(raw)) return 'canceled'; if (/^(failed|failure|error)$/u.test(raw)) return 'failed'; if (/^(completed|complete|succeeded|success|terminal)$/u.test(raw)) return 'completed'; if (/^(running|admitting|queued|pending|in_progress|in-progress)$/u.test(raw)) return 'running'; return null; }",
    "function statusFor(items, traceId) { const entries = traceId ? traceEntries(items, traceId) : items.flatMap((sample) => entryGroups(sample).map((entry) => ({ ...entry, sample, text: textOf(entry.item) }))); const lastTurn = entries.filter((entry) => entry.group === 'turn').slice(-1)[0]?.item || null; const turnStatus = normalizeLifecycleStatus(lastTurn?.status); if (turnStatus) return turnStatus; const lastMessage = entries.filter((entry) => entry.group === 'message').slice(-1)[0]?.item || null; return normalizeLifecycleStatus(lastMessage?.status) || 'unknown'; }",
    "function cleanFinalResponseText(value) { const raw = String(value || '').trim(); if (!raw) return ''; if (/^(completed|failed|canceled|cancelled|轮次完成|轮次失败|轮次取消|已记录)$/iu.test(raw.replace(/\\s+/gu, ' '))) return ''; if (/^(admitted|run|ok|error)\\s+/iu.test(raw)) return ''; return raw; }",
    "function finalResponseTextFromEntry(entry) {",
    "  const explicit = cleanFinalResponseText(entry.item?.finalResponse?.text || entry.item?.finalResponse?.preview || '');",
    "  if (explicit && !/^Code Agent\\s*耗时/iu.test(explicit)) return explicit;",
    "  if (entry.group !== 'message') return '';",
    "  const role = String(entry.item?.role || entry.item?.dataRole || entry.item?.messageRole || '').toLowerCase();",
    "  if (role && !/assistant|agent|system/u.test(role)) return '';",
    "  const text = cleanFinalResponseText(entry.text);",
    "  return text && !/^Code Agent\\s*耗时/iu.test(text) ? text : '';",
    "}",
    "function finalResponseEmpty(items, traceId) { if (!/^(completed|failed|canceled)$/u.test(statusFor(items, traceId))) return true; const entries = (traceId ? traceEntries(items, traceId) : items.flatMap((sample) => entryGroups(sample).map((entry) => ({ ...entry, sample, text: textOf(entry.item) })))).slice().reverse(); for (const entry of entries) { if (finalResponseTextFromEntry(entry)) return false; } return true; }",
    "function rowFor() {",
    "  const control = readJsonl('control.jsonl');",
    "  const samples = readJsonlTail('samples.jsonl');",
    "  const prompts = promptCommands(control);",
    "  const prompt = prompts[promptIndex - 1] || null;",
    "  if (!prompt) return { ok: true, round: promptIndex, status: 'command-pending', traceId: null, finalResponseEmpty: true, lastSeq: null, lastTs: null, promptMissing: true, valuesRedacted: true };",
    "  const done = readDone(prompt.commandId);",
    "  const failed = readFailed(prompt.commandId);",
    "  if (failed) return { ok: false, failure: 'observe-command-sendPrompt-failed', round: promptIndex, status: 'command-failed', commandId: prompt.commandId || null, traceId: null, finalResponseEmpty: true, commandFailure: short(failed.error?.message || failed.failure || failed.status || 'command failed'), valuesRedacted: true };",
    "  const promptTraceId = commandTraceId(prompt);",
    "  if (!done) return { ok: true, round: promptIndex, status: 'command-pending', commandId: prompt.commandId || null, traceId: promptTraceId || null, finalResponseEmpty: true, commandPhase: prompt.phase || null, traceMissing: !promptTraceId, valuesRedacted: true };",
    "  const segment = segmentFor(samples, prompts, promptIndex - 1);",
    "  const controlSegment = segment.filter((sample) => sample.pageRole === 'control');",
    "  const traceId = promptTraceId || traceIdForPromptUserMessage(segment, prompt) || chooseTraceId(controlSegment, prompt) || chooseTraceId(segment, prompt);",
    "  if (!traceId) return { ok: true, round: promptIndex, status: 'command-pending', commandId: prompt.commandId || null, traceId: null, finalResponseEmpty: true, commandPhase: prompt.phase || null, traceMissing: true, segmentSampleCount: segment.length, valuesRedacted: true };",
    "  const controlTraceSegment = traceId ? controlSegment.filter((sample) => traceIdsFromSamples([sample]).includes(traceId)) : [];",
    "  const statusSegment = controlTraceSegment.length > 0 ? controlSegment : segment;",
    "  const status = statusFor(statusSegment, traceId);",
    "  const sampleForTrace = traceId ? statusSegment.filter((sample) => traceIdsFromSamples([sample]).includes(traceId)).slice(-1)[0] || segment.filter((sample) => traceIdsFromSamples([sample]).includes(traceId)).slice(-1)[0] || null : null;",
    "  const lastSample = sampleForTrace || segment.slice(-1)[0] || null;",
    "  const composerSample = segment.filter((sample) => sample.pageRole === 'control').slice(-1)[0] || lastSample;",
    "  const composer = composerSample?.composer || {};",
    "  const composerReadyForTurn = composer.inputPresent === true && composer.inputDisabled !== true && composer.submitPresent === true && composer.warningPresent !== true && composer.submitAction === 'turn';",
    "  return { ok: true, round: promptIndex, status, traceId, finalResponseEmpty: finalResponseEmpty(statusSegment, traceId), lastSeq: numOrNull(lastSample?.seq), lastTs: lastSample?.ts || null, composerReadyForTurn, composerAction: composer.submitAction || null, sampleScope: controlSegment.length > 0 ? 'control' : 'all', segmentSampleCount: segment.length, statusSampleCount: statusSegment.length, source: 'observe-artifact-wait-script', valuesRedacted: true };",
    "}",
    "function norm(value) { return String(value || '').trim().toLowerCase().replace(/_/gu, '-'); }",
    "function successful(value) { return ['completed', 'succeeded', 'success'].includes(norm(value)); }",
    "function terminal(value) { return ['completed', 'succeeded', 'success', 'failed', 'error', 'blocked', 'timeout', 'canceled', 'cancelled', 'terminal'].includes(norm(value)); }",
    "const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));",
    "(async () => {",
    "  const observations = [];",
    "  while (Date.now() - startedAt <= timeoutMs) {",
    "    const row = rowFor();",
    "    observations.push(row);",
    "    if (row.ok === false) { console.log(JSON.stringify({ ...row, ok: false, failure: row.failure || 'quick-verify-artifact-row-failed', observations: observations.slice(-6), elapsedMs: Date.now() - startedAt, valuesRedacted: true })); return; }",
    "    if (successful(row.status) && row.finalResponseEmpty !== true) { console.log(JSON.stringify({ ...row, ok: true, observations: observations.slice(-6), elapsedMs: Date.now() - startedAt, valuesRedacted: true })); return; }",
    "    if (!successful(row.status) && terminal(row.status)) { console.log(JSON.stringify({ ...row, ok: false, failure: 'observe-turn-terminal-non-success', observations: observations.slice(-6), elapsedMs: Date.now() - startedAt, valuesRedacted: true })); return; }",
    "    await sleep(Math.min(pollMs, Math.max(0, timeoutMs - (Date.now() - startedAt))));",
    "  }",
    "  const row = rowFor();",
    "  observations.push(row);",
    "  console.log(JSON.stringify({ ...row, ok: false, failure: 'quick-verify-wait-chunk-timeout', observations: observations.slice(-6), elapsedMs: Date.now() - startedAt, valuesRedacted: true }));",
    "})().catch((error) => { console.log(JSON.stringify({ ok: false, failure: 'quick-verify-artifact-wait-script-error', error: error instanceof Error ? error.message : String(error), valuesRedacted: true })); });",
    "NODE",
  ].join("\n");
}

function isQuickVerifyTurnSuccessful(value: string | null): boolean {
  const status = normalizeQuickVerifyStatus(value);
  return status === "completed" || status === "succeeded" || status === "success";
}

function isQuickVerifyTurnTerminal(value: string | null): boolean {
  const status = normalizeQuickVerifyStatus(value);
  return status === "completed"
    || status === "succeeded"
    || status === "success"
    || status === "failed"
    || status === "error"
    || status === "blocked"
    || status === "timeout"
    || status === "canceled"
    || status === "cancelled"
    || status === "terminal";
}

function normalizeQuickVerifyStatus(value: string | null): string {
  return String(value ?? "").trim().toLowerCase().replace(/_/gu, "-");
}

export function cliDataPayload(parsed: Record<string, unknown> | null): Record<string, unknown> {
  const root = record(parsed);
  return isRecord(root.data) ? root.data : root;
}

export function findScenario(state: SentinelCicdState, scenarioId: string): Record<string, unknown> | null {
  const scenarios = readWebProbeSentinelConfigRefTarget(state.spec, state.configRefs.scenarios);
  const items = Array.isArray(scenarios) ? scenarios : isRecord(scenarios) ? [scenarios] : [];
  return items.map(record).find((item) => item.id === scenarioId) ?? null;
}

export function runSentinelExactProviderProfilePreflight(
  state: SentinelCicdState,
  timeoutSeconds: number,
  selectedScenario?: Record<string, unknown>,
): Record<string, unknown> & { ok: boolean } {
  const scenarioId = stringAt(state.cicd, "targetValidation.scenarioId");
  const scenario = selectedScenario ?? findScenario(state, scenarioId);
  if (scenario === null) {
    return {
      ok: false,
      status: "blocked",
      reason: "scenario-not-found",
      scenarioId,
      mutation: false,
      valuesRedacted: true,
    };
  }
  const profile = stringAtNullable(scenario, "providerProfile");
  const mode = stringAtNullable(scenario, "providerProfileMode");
  if (mode !== "exact") {
    return {
      ok: true,
      status: "not-required",
      profile,
      mode,
      mutation: false,
      valuesRedacted: true,
    };
  }
  if (profile === null) {
    return {
      ok: false,
      status: "blocked",
      reason: "exact-provider-profile-missing",
      profile,
      mode,
      mutation: false,
      valuesRedacted: true,
    };
  }
  const runtime = state.spec.codeAgentRuntime;
  if (runtime === undefined) {
    return {
      ok: false,
      status: "blocked",
      reason: "code-agent-runtime-target-unavailable",
      profile,
      mode,
      mutation: false,
      valuesRedacted: true,
    };
  }
  const target = runtime.controlPlaneTarget;
  const result = runChildCli([
    "agentrun",
    "provider-profile",
    "runtime-status",
    "--node",
    target.node,
    "--lane",
    target.lane,
    "--profile",
    profile,
    "--raw",
  ], Math.max(5, Math.min(timeoutSeconds, 55)));
  const payload = cliDataPayload(result.parsed);
  const runtimePayload = record(payload.runtime);
  const secrets = record(runtimePayload.secrets);
  const providerServices = record(runtimePayload.providerServices);
  const ready = result.ok
    && payload.ok === true
    && payload.ownerSourceInspected === false
    && payload.runtimeSecretValuesDecoded === false
    && secrets.ready === true;
  return {
    ok: ready,
    status: ready ? "ready" : "blocked",
    reason: ready ? null : "exact-provider-profile-unavailable",
    profile,
    mode,
    target: {
      node: target.node,
      lane: target.lane,
    },
    runtime: {
      ready: payload.ok === true,
      secretsReady: secrets.ready === true,
      providerServicesReady: providerServices.ready === true,
      count: numberAtNullable(secrets, "count"),
      presentCount: numberAtNullable(secrets, "presentCount"),
      keyPresentCount: numberAtNullable(secrets, "keyPresentCount"),
      missing: arrayAt(secrets, "items")
        .map(record)
        .filter((item) => item.present !== true || item.keyPresent !== true)
        .slice(0, 8)
        .map((item) => ({
          id: stringAtNullable(item, "id"),
          namespace: stringAtNullable(item, "namespace"),
          name: stringAtNullable(item, "name"),
          key: stringAtNullable(item, "key"),
          present: item.present === true,
          keyPresent: item.keyPresent === true,
          valuesRedacted: true,
        })),
      providerServices: arrayAt(providerServices, "items")
        .map(record)
        .slice(0, 8)
        .map((item) => ({
          providerId: stringAtNullable(item, "providerId"),
          namespace: stringAtNullable(item, "namespace"),
          name: stringAtNullable(item, "name"),
          port: numberAtNullable(item, "port"),
          present: item.present === true,
          portPresent: item.portPresent === true,
          valuesRedacted: true,
        })),
      valuesRedacted: true,
    },
    ownerSourceInspected: payload.ownerSourceInspected === true,
    runtimeSecretValuesDecoded: payload.runtimeSecretValuesDecoded === true,
    child: result.result,
    mutation: false,
    valuesRedacted: true,
  };
}

export function readPromptSetForScenario(state: SentinelCicdState, scenario: Record<string, unknown>): { ok: true; prompts: string[]; summary: Record<string, unknown> } | { ok: false; error: string; summary: Record<string, unknown> } {
  const promptSetRef = stringAt(scenario, "promptSetRef");
  const promptSet = recordTarget(readWebProbeSentinelConfigRefTarget(state.spec, promptSetRef), promptSetRef);
  const sourceRef = stringAt(promptSet, "promptSourceRef");
  const key = stringAt(promptSet, "promptSourceKey");
  const paths = secretSourcePaths(sourceRef);
  const sourcePath = paths.find((item) => existsSync(item)) ?? paths[0] ?? rootPath(".state", "secrets", sourceRef);
  const summary = { sourceRef, sourceKey: key, sourcePath: displayPath(sourcePath), valuesRedacted: true };
  const runtimeRaw = process.env[key];
  const values = existsSync(sourcePath) ? parseEnvFile(readFileSync(sourcePath, "utf8")) : {};
  const raw = values[key] ?? runtimeRaw;
  const sourceMode = values[key] !== undefined ? "secret-source-file" : runtimeRaw !== undefined && runtimeRaw.length > 0 ? "runtime-env" : null;
  if (!existsSync(sourcePath) && sourceMode === null) return { ok: false, error: "prompt-source-missing", summary };
  if (raw === undefined || raw.length === 0) return { ok: false, error: "prompt-key-missing", summary };
  const parsed = parsePromptJson(raw);
  if (parsed.length === 0) return { ok: false, error: "prompt-json-empty", summary };
  return {
    ok: true,
    prompts: parsed,
    summary: {
      ...summary,
      sourceMode,
      promptCount: parsed.length,
      promptMarkers: parsed.map((item) => Array.from(new Set(Array.from(item.matchAll(/\bsentinel-(?:0[1-9]|10)\b/giu)).map((match) => match[0].toLowerCase())))),
      promptTextHashes: parsed.map((item) => `sha256:${createHash("sha256").update(item).digest("hex").slice(0, 16)}`),
      promptTextBytes: parsed.map((item) => Buffer.byteLength(item)),
      valuesRedacted: true,
    },
  };
}

function parsePromptJson(raw: string): string[] {
  try {
    const parsed = JSON.parse(raw) as unknown;
    if (Array.isArray(parsed)) return parsed.filter((item): item is string => typeof item === "string" && item.length > 0);
    const recordValue = record(parsed);
    if (Array.isArray(recordValue.prompts)) return recordValue.prompts.filter((item): item is string => typeof item === "string" && item.length > 0);
    if (typeof recordValue.prompt === "string" && recordValue.prompt.length > 0) return [recordValue.prompt];
  } catch {
    if (raw.trim().length > 0) return [raw];
  }
  return [];
}

export function readLocalObserveIndex(observerId: string): { stateDir: string } | null {
  const path = rootPath(".state/web-observe/index.json");
  if (!existsSync(path)) return null;
  const parsed = parseJsonObject(readFileSync(path, "utf8"));
  const entry = record(parsed?.[observerId]);
  const stateDir = typeof entry.stateDir === "string" ? entry.stateDir : null;
  return stateDir === null ? null : { stateDir };
}

export function observerIdFromText(textValue: string): string | null {
  return /\bwebobs-[a-z0-9-]+\b/iu.exec(textValue)?.[0] ?? null;
}

export function remainingSeconds(deadline: number, cap: number): number {
  return Math.max(5, Math.min(cap, Math.ceil((deadline - Date.now()) / 1000)));
}

export function metricNames(textValue: unknown): string[] {
  if (typeof textValue !== "string") return [];
  return textValue.split(/\r?\n/u).map((line) => /^([A-Za-z_:][A-Za-z0-9_:]*)/u.exec(line)?.[1]).filter((item): item is string => typeof item === "string");
}

export function validationBlocker(health: Record<string, unknown>, metrics: Record<string, unknown>, report: Record<string, unknown>, publicExposure: Record<string, unknown>, publicDashboard: Record<string, unknown>, quickVerify: Record<string, unknown> | null): Record<string, unknown> {
  const blockers = [];
  if (!health.ok || record(health.bodyJson).ok !== true) blockers.push("health");
  if (!metrics.ok || !metricNames(metrics.bodyTextPreview).includes("web_probe_sentinel_health")) blockers.push("metrics");
  if (!report.ok) blockers.push("recent-report");
  if (publicExposure.ok !== true) blockers.push("public-exposure");
  if (publicDashboard.ok !== true) blockers.push("public-dashboard");
  if (quickVerify !== null && quickVerify.ok !== true) blockers.push("quick-verify");
  return { code: "sentinel-validation-failed", blockers, valuesRedacted: true };
}

export function serviceUnavailableBlocker(state: SentinelCicdState): Record<string, unknown> {
  return {
    code: "sentinel-service-unavailable",
    policy: stringAt(state.cicd, "targetValidation.serviceUnavailablePolicy"),
    reason: "sentinel service must be reachable through k3s internal Service DNS before quick verify can run; no public/fallback path is used.",
    retry: `bun scripts/cli.ts web-probe sentinel validate --node ${state.spec.nodeId} --lane ${state.spec.lane}${sentinelCliSuffix(state)}`,
    valuesRedacted: true,
  };
}

export function sentinelP5Next(state: SentinelCicdState): Record<string, string> {
  const node = state.spec.nodeId;
  const lane = state.spec.lane;
  const suffix = sentinelCliSuffix(state);
  return {
    validate: `bun scripts/cli.ts web-probe sentinel validate --node ${node} --lane ${lane}${suffix}`,
    quickVerify: `bun scripts/cli.ts web-probe sentinel validate --node ${node} --lane ${lane}${suffix} --quick-verify --confirm --wait`,
    maintenanceStart: `bun scripts/cli.ts web-probe sentinel maintenance start --node ${node} --lane ${lane}${suffix} --confirm --wait`,
    maintenanceStop: `bun scripts/cli.ts web-probe sentinel maintenance stop --node ${node} --lane ${lane}${suffix} --confirm --wait`,
    report: `bun scripts/cli.ts web-probe sentinel report --node ${node} --lane ${lane}${suffix} --view summary`,
  };
}

function isSafeRelativeStateDir(value: string): boolean {
  return value.startsWith(".state/web-observe/") && !value.includes("\0") && !value.includes("..");
}

export function mergeFindingRecords(primary: readonly Record<string, unknown>[], extra: readonly Record<string, unknown>[]): Record<string, unknown>[] {
  const merged: Record<string, unknown>[] = [];
  const seen = new Set<string>();
  for (const item of [...primary, ...extra]) {
    const id = stringAtNullable(item, "id") ?? stringAtNullable(item, "kind") ?? stringAtNullable(item, "code") ?? stringAtNullable(item, "finding_id") ?? "finding";
    const severity = stringAtNullable(item, "severity") ?? stringAtNullable(item, "level") ?? "unknown";
    const key = `${id}\0${severity}`;
    if (seen.has(key)) continue;
    seen.add(key);
    merged.push(item);
  }
  return merged;
}

export function isQuickVerifyBlockingFinding(item: Record<string, unknown>): boolean {
  const severity = (stringAtNullable(item, "severity") ?? stringAtNullable(item, "level") ?? "").toLowerCase();
  if (!["critical", "red", "fatal", "error", "failed", "blocked"].includes(severity)) return false;
  const id = (stringAtNullable(item, "id") ?? stringAtNullable(item, "kind") ?? stringAtNullable(item, "code") ?? "").toLowerCase();
  if (id === "observer-command-failed") return observerCommandFailureBlocks(item);
  return [
    "quick-verify-no-business-turn",
    "quick-verify-target-turn-missing",
    "quick-verify-target-turn-trace-id-missing",
    "quick-verify-trace-projection-missing",
    "quick-verify-final-response-empty-incomplete",
    "quick-verify-final-response-empty-terminal",
    "quick-verify-final-response-empty-status-unknown",
    "quick-verify-diagnostics-inconclusive",
    "quick-verify-analysis-summary-unreadable",
    "account-session-command-failed-before-observation",
    "quick-verify-observer-start-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",
    "route-active-session-mismatch",
    "final-response-flicker",
    "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);
}

function observerCommandFailureBlocks(item: Record<string, unknown>): boolean {
  const commands = Array.isArray(item.commands) ? item.commands.map(record) : [];
  if (commands.length === 0) return true;
  return commands.some((command) => {
    const type = (stringAtNullable(command, "type") ?? "").toLowerCase();
    if (["stop", "cancel", "mark", "screenshot"].includes(type)) return false;
    return true;
  });
}

export function quickVerifyPrimaryControlFailure(controlFindings: Record<string, unknown>[]): string | null {
  const first = controlFindings[0];
  return first === undefined ? null : stringAtNullable(first, "id") ?? stringAtNullable(first, "kind") ?? stringAtNullable(first, "code");
}

function quickVerifyControlEvidenceSummary(turnDiagnostics: Record<string, unknown>, traceDiagnostics: Record<string, unknown>, targetStatus: Record<string, unknown> | null): string {
  const targetStatusText = targetStatus === null ? "-" : stringAtNullable(targetStatus, "status") ?? "-";
  const targetMarks = targetStatus === null ? "-" : stringAtNullable(targetStatus, "marks") ?? "-";
  const targetTrace = targetStatus === null ? "-" : targetStatus.traceIdPresent === true ? "present" : "missing";
  const targetFinalBytes = targetStatus === null ? "-" : String(targetStatus.finalResponseBytes ?? "-");
  const traceBlocker = stringAtNullable(traceDiagnostics, "blocker") ?? "-";
  const traceRowCount = traceDiagnostics.traceRowCount === null || traceDiagnostics.traceRowCount === undefined ? "-" : String(traceDiagnostics.traceRowCount);
  return [
    `turn-summary rows=${String(turnDiagnostics.rowCount ?? 0)}`,
    `scoped=${String(turnDiagnostics.scopedRowCount ?? 0)}`,
    `targetStatus=${targetStatusText}`,
    `targetMarks=${targetMarks}`,
    `targetTrace=${targetTrace}`,
    `targetFinalBytes=${targetFinalBytes}`,
    `traceFrameTrace=${traceDiagnostics.traceIdPresent === true ? "present" : "missing"}`,
    `traceFrameBlocker=${traceBlocker}`,
    `traceRows=${traceRowCount}`,
    `traceFinalBytes=${String(traceDiagnostics.finalResponseBytes ?? "-")}`,
  ].join(" ");
}

function quickVerifyScopedStatuses(turnDiagnostics: Record<string, unknown>): Record<string, unknown>[] {
  return Array.isArray(turnDiagnostics.scopedStatuses) ? turnDiagnostics.scopedStatuses.map(record) : [];
}

function quickVerifyLatestScopedStatus(turnDiagnostics: Record<string, unknown>): Record<string, unknown> | null {
  const rows = quickVerifyScopedStatuses(turnDiagnostics);
  return rows.length === 0 ? null : rows[rows.length - 1] ?? null;
}

function quickVerifyFinalResponseEmpty(targetStatus: Record<string, unknown> | null, traceDiagnostics: Record<string, unknown>): boolean {
  return targetStatus?.finalResponseEmpty === true || traceDiagnostics.finalResponseEmpty === true;
}

function quickVerifyTargetTracePresent(targetStatus: Record<string, unknown> | null, traceDiagnostics: Record<string, unknown>): boolean {
  return targetStatus?.traceIdPresent === true || traceDiagnostics.traceIdPresent === true;
}

function quickVerifyTraceProjectionMissing(targetStatus: Record<string, unknown> | null, traceDiagnostics: Record<string, unknown>): boolean {
  if (!quickVerifyTargetTracePresent(targetStatus, traceDiagnostics)) return false;
  const blocker = stringAtNullable(traceDiagnostics, "blocker");
  if (blocker === "trace-rows-missing" || blocker === "sample-not-found") return true;
  if (traceDiagnostics.collectOk !== true) return true;
  const traceRowCount = numberAtNullable(traceDiagnostics, "traceRowCount");
  return traceRowCount !== null && traceRowCount === 0;
}

function quickVerifyTargetStatusKind(targetStatus: Record<string, unknown> | null): "incomplete" | "terminal" | "unknown" {
  const status = (targetStatus === null ? "" : stringAtNullable(targetStatus, "status") ?? "").toLowerCase();
  const marks = (targetStatus === null ? "" : stringAtNullable(targetStatus, "marks") ?? "").toLowerCase();
  if (/running|pending|queued|starting|submitting|processing|in[-_ ]?progress|waiting/u.test(status)) return "incomplete";
  if (/cancel/u.test(status) || /cancel/u.test(marks)) return "incomplete";
  if (/completed|succeeded|success|done|failed|failure|error|terminal|command-failed|timeout|stopped|aborted/u.test(status)) return "terminal";
  return "unknown";
}

function quickVerifyControlFinding(
  id: string,
  titleZh: string,
  summary: string,
  rootCause: string,
  nextAction: string,
  promptIndex: number,
  failure: string | null,
  turnDiagnostics: Record<string, unknown>,
  traceDiagnostics: Record<string, unknown>,
  targetStatus: Record<string, unknown> | null,
  display: Record<string, unknown> | null,
): Record<string, unknown> {
  return {
    id,
    severity: "red",
    count: 1,
    summary,
    displayTitleZh: titleZh,
    errorTitleZh: titleZh,
    timeoutDisplay: display,
    rootCause,
    rootCauseStatus: "confirmed",
    rootCauseConfidence: "high",
    evidenceSummary: quickVerifyControlEvidenceSummary(turnDiagnostics, traceDiagnostics, targetStatus),
    nextAction,
    turnSummaryDiagnostics: turnDiagnostics,
    traceFrameDiagnostics: traceDiagnostics,
    failure: failure ?? null,
    promptIndex,
    valuesRedacted: true,
  };
}

export function quickVerifyControlFindings(failure: string | null, promptIndex: number, turnSummary: Record<string, unknown> | null, traceFrame: Record<string, unknown> | null, display: Record<string, unknown> | null = null): Record<string, unknown>[] {
  if (quickVerifyHasDurableBusinessTurn(promptIndex, turnSummary, traceFrame)) return [];
  const turnDiagnostics = quickVerifyTurnSummaryDiagnostics(promptIndex, turnSummary);
  const traceDiagnostics = quickVerifyTraceFrameDiagnostics(traceFrame);
  const noPromptScenario = promptIndex <= 0;
  if (noPromptScenario && failure === null) return [];
  if (noPromptScenario && failure !== null) {
    const observerStartFailure = failure === "observe-start-failed";
    const displayTitleZh = stringAtNullable(display ?? {}, "titleZh");
    return [{
      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."
        : "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,
    }];
  }
  const rowCount = numberAtNullable(turnDiagnostics, "rowCount") ?? 0;
  const scopedRowCount = numberAtNullable(turnDiagnostics, "scopedRowCount") ?? 0;
  const targetStatus = quickVerifyLatestScopedStatus(turnDiagnostics);
  if (rowCount === 0) {
    return [quickVerifyControlFinding(
      "quick-verify-no-business-turn",
      "快速验证没有采集到业务轮次",
      "quick verify collected zero business turn rows; this is not a trace/final-response failure.",
      `turn-summary rowCount=0 for promptIndex=${promptIndex}.`,
      "Inspect observe command execution and turn-summary collection before checking trace-frame or final response.",
      promptIndex,
      failure,
      turnDiagnostics,
      traceDiagnostics,
      targetStatus,
      display,
    )];
  }
  if (scopedRowCount === 0) {
    return [quickVerifyControlFinding(
      "quick-verify-target-turn-missing",
      "快速验证缺少目标业务轮次",
      "quick verify collected turn rows, but none matched the target prompt index.",
      `turn-summary rowCount=${rowCount} but scopedRowCount=0 for promptIndex=${promptIndex}.`,
      "Check promptIndex/command segmentation and rerun the bounded turn-summary view for the observer.",
      promptIndex,
      failure,
      turnDiagnostics,
      traceDiagnostics,
      targetStatus,
      display,
    )];
  }

  const findings: Record<string, unknown>[] = [];
  if (!quickVerifyTargetTracePresent(targetStatus, traceDiagnostics)) {
    findings.push(quickVerifyControlFinding(
      "quick-verify-target-turn-trace-id-missing",
      "目标业务轮次缺少 traceId",
      "quick verify found the target business turn, but the turn has no traceId.",
      `target turn exists for promptIndex=${promptIndex}, but traceIdPresent=false.`,
      "Fix Workbench/Code Agent traceId projection for the target turn before inspecting trace rows.",
      promptIndex,
      failure,
      turnDiagnostics,
      traceDiagnostics,
      targetStatus,
      display,
    ));
    return findings;
  }

  if (quickVerifyTraceProjectionMissing(targetStatus, traceDiagnostics)) {
    findings.push(quickVerifyControlFinding(
      "quick-verify-trace-projection-missing",
      "目标 trace 投影缺失",
      "quick verify found a target turn traceId, but trace-frame did not contain trace rows/projection for it.",
      `target turn has traceId, but trace-frame collectOk=${traceDiagnostics.collectOk === true} blocker=${stringAtNullable(traceDiagnostics, "blocker") ?? "-"} traceRowCount=${String(traceDiagnostics.traceRowCount ?? "-")}.`,
      "Inspect trace-frame for the target traceId, then check Workbench trace projection/read-model hydration before treating the run as a business failure.",
      promptIndex,
      failure,
      turnDiagnostics,
      traceDiagnostics,
      targetStatus,
      display,
    ));
  }

  if (quickVerifyFinalResponseEmpty(targetStatus, traceDiagnostics)) {
    const statusKind = quickVerifyTargetStatusKind(targetStatus);
    if (statusKind === "incomplete") {
      findings.push(quickVerifyControlFinding(
        "quick-verify-final-response-empty-incomplete",
        "目标轮次未完成且最终回复为空",
        "quick verify found the target business turn, but it is still incomplete/cancel-marked and Final Response is empty.",
        `target turn status=${stringAtNullable(targetStatus ?? {}, "status") ?? "-"} marks=${stringAtNullable(targetStatus ?? {}, "marks") ?? "-"} finalResponseEmpty=true.`,
        "Inspect the same traceId in Workbench and Code Agent runtime; this is an unfinished/cancelled turn, not a zero-dialog case.",
        promptIndex,
        failure,
        turnDiagnostics,
        traceDiagnostics,
        targetStatus,
        display,
      ));
    } else if (statusKind === "terminal") {
      findings.push(quickVerifyControlFinding(
        "quick-verify-final-response-empty-terminal",
        "目标轮次已终止但最终回复为空",
        "quick verify found a terminal target business turn, but Final Response is empty.",
        `target turn terminal status=${stringAtNullable(targetStatus ?? {}, "status") ?? "-"} finalResponseEmpty=true.`,
        "Inspect terminal event persistence and final-response projection for the target traceId; do not classify this as no dialog.",
        promptIndex,
        failure,
        turnDiagnostics,
        traceDiagnostics,
        targetStatus,
        display,
      ));
    } else {
      findings.push(quickVerifyControlFinding(
        "quick-verify-final-response-empty-status-unknown",
        "目标轮次状态未知且最终回复为空",
        "quick verify found the target business turn and empty Final Response, but the turn status is not classifiable.",
        `target turn status=${stringAtNullable(targetStatus ?? {}, "status") ?? "-"} finalResponseEmpty=true.`,
        "Fix the turn-summary status projection so quick verify can classify incomplete versus terminal final-response failures.",
        promptIndex,
        failure,
        turnDiagnostics,
        traceDiagnostics,
        targetStatus,
        display,
      ));
    }
  }

  if (findings.length > 0) return findings;
  return [quickVerifyControlFinding(
    "quick-verify-diagnostics-inconclusive",
    "快速验证诊断信息不足",
    "quick verify did not prove a durable completed turn, but structured diagnostics did not match a specific failure code.",
    `quick verify has rowCount=${rowCount} scopedRowCount=${scopedRowCount}, traceIdPresent=${traceDiagnostics.traceIdPresent === true}, finalResponseEmpty=${traceDiagnostics.finalResponseEmpty === true}.`,
    "Improve turn-summary/trace-frame diagnostics before making a business recovery decision.",
    promptIndex,
    failure,
    turnDiagnostics,
    traceDiagnostics,
    targetStatus,
    display,
  )];
}

export function enrichObserveStartFailureFinding(finding: Record<string, unknown>, result: Record<string, unknown>): Record<string, unknown> {
  const stdout = stringAtNullable(result, "stdoutPreview") ?? stringAtNullable(result, "stdoutTail");
  const payload = stdout === null ? null : parseJsonObject(stdout);
  const data = record(record(payload).data);
  const degradedReason = stringAtNullable(data, "degradedReason");
  const command = stringAtNullable(data, "command");
  if (degradedReason === null) {
    return {
      ...finding,
      evidenceSummary: `observe start exited ${String(result.exitCode ?? "-")}; timedOut=${result.timedOut === true}`,
      valuesRedacted: true,
    };
  }
  return {
    ...finding,
    rootCause: `observe-start failed: ${degradedReason}`,
    rootCauseStatus: "confirmed",
    rootCauseConfidence: "high",
    nextAction: degradedReason === "web_login_secret_missing"
      ? "Align YAML-declared bootstrap secret env injection with web-probe observe credential materialization."
      : "Inspect observe-start failure payload and fix the reported degradedReason.",
    evidenceSummary: `${command ?? "web-probe observe start"} degradedReason=${degradedReason}`,
    valuesRedacted: true,
  };
}

function quickVerifyCompletedTurnSummaryRow(promptIndex: number, turnSummary: Record<string, unknown> | null): Record<string, unknown> | null {
  const rows = quickVerifyTurnSummaryRows(turnSummary);
  const scopedRows = promptIndex > 0 ? rows.filter((row) => numberAtNullable(row, "round") === promptIndex) : rows;
  return scopedRows.find((row) => {
    const finalResponse = record(row.finalResponse);
    return isQuickVerifyTurnSuccessful(stringAtNullable(row, "status"))
      && stringAtNullable(row, "traceId") !== null
      && finalResponse.empty !== true;
  }) ?? null;
}

function quickVerifyTurnSummaryFallback(state: SentinelCicdState, observerId: string, promptIndex: number): Record<string, unknown> {
  const turnSummary = collectObserveView(state, observerId, "turn-summary", null, 25);
  const row = quickVerifyCompletedTurnSummaryRow(promptIndex, turnSummary);
  const rows = quickVerifyTurnSummaryRows(turnSummary);
  if (row === null) {
    return {
      ok: false,
      source: "turn-summary-fallback",
      collectOk: turnSummary.ok === true,
      rowCount: rows.length,
      promptIndex,
      diagnostics: quickVerifyTurnSummaryDiagnostics(promptIndex, turnSummary),
      result: turnSummary.result ?? null,
      valuesRedacted: true,
    };
  }
  const finalResponse = record(row.finalResponse);
  return {
    ok: true,
    source: "turn-summary-fallback",
    collectOk: turnSummary.ok === true,
    rowCount: rows.length,
    promptIndex,
    status: stringAtNullable(row, "status"),
    traceId: stringAtNullable(row, "traceId"),
    finalResponseEmpty: finalResponse.empty === true,
    finalResponseBytes: numberAtNullable(finalResponse, "textBytes"),
    result: turnSummary.result ?? null,
    valuesRedacted: true,
  };
}

export function quickVerifyHasDurableBusinessTurn(promptIndex: number, turnSummary: Record<string, unknown> | null, traceFrame: Record<string, unknown> | null): boolean {
  if (quickVerifyCompletedTurnSummaryRow(promptIndex, turnSummary) !== null) return true;
  if (quickVerifyTraceFrameHasDurableTurn(traceFrame)) return true;
  const renderedTrace = typeof traceFrame?.renderedText === "string" ? traceFrame.renderedText : "";
  if (!renderedTrace) return false;
  if (/Final Response\s*\n\s*\(空内容\)/iu.test(renderedTrace)) return false;
  return /Code Agent[^\n]*completed|轮次完成（总耗时/iu.test(renderedTrace)
    && /Final Response\s*\n\s*\S/iu.test(renderedTrace)
    && !/无\s*trace\s*rows|no\s+trace\s+rows|traceId=-|routeSession=-|activeSession=-/iu.test(renderedTrace);
}

function quickVerifyTurnSummaryRows(turnSummary: Record<string, unknown> | null): Record<string, unknown>[] {
  const collect = record(turnSummary?.collect);
  return Array.isArray(collect.rows) ? collect.rows.map(record) : [];
}

function quickVerifyTraceFrameHasDurableTurn(traceFrame: Record<string, unknown> | null): boolean {
  const collect = record(traceFrame?.collect);
  if (collect.ok === false) return false;
  if (stringAtNullable(collect, "traceId") === null) return false;
  const finalResponse = record(collect.finalResponse);
  if (finalResponse.empty === true) return false;
  const textBytes = numberAtNullable(finalResponse, "textBytes");
  return textBytes === null || textBytes > 0;
}

function quickVerifyTraceFrameDiagnostics(traceFrame: Record<string, unknown> | null): Record<string, unknown> {
  const collect = record(traceFrame?.collect);
  const finalResponse = record(collect.finalResponse);
  return {
    collectOk: traceFrame?.ok === true,
    collectShape: record(traceFrame?.collectShape),
    collectView: stringAtNullable(collect, "view"),
    traceIdPresent: stringAtNullable(collect, "traceId") !== null,
    finalResponseEmpty: finalResponse.empty === true,
    finalResponseBytes: numberAtNullable(finalResponse, "textBytes"),
    blockerPresent: collect.blocker !== null && collect.blocker !== undefined,
    result: traceFrame?.result ?? null,
    valuesRedacted: true,
  };
}

function quickVerifyTurnSummaryDiagnostics(promptIndex: number, turnSummary: Record<string, unknown> | null): Record<string, unknown> {
  const collect = record(turnSummary?.collect);
  const rows = quickVerifyTurnSummaryRows(turnSummary);
  const scopedRows = promptIndex > 0 ? rows.filter((row) => numberAtNullable(row, "round") === promptIndex) : rows;
  const scopedStatuses = scopedRows.slice(0, 4).map((row) => {
    const finalResponse = record(row.finalResponse);
    return {
      round: numberAtNullable(row, "round"),
      status: stringAtNullable(row, "status"),
      marks: stringAtNullable(row, "marks"),
      traceIdPresent: stringAtNullable(row, "traceId") !== null,
      finalResponseEmpty: finalResponse.empty === true,
      finalResponseBytes: numberAtNullable(finalResponse, "textBytes"),
      valuesRedacted: true,
    };
  });
  return {
    collectOk: turnSummary?.ok === true,
    collectShape: record(turnSummary?.collectShape),
    collectView: stringAtNullable(collect, "view"),
    rowCount: rows.length,
    scopedRowCount: scopedRows.length,
    promptIndex,
    scopedStatuses,
    result: turnSummary?.result ?? null,
    valuesRedacted: true,
  };
}

export function quickVerifyBusinessStatus(
  failure: string | null,
  promptIndex: number,
  turnSummary: Record<string, unknown> | null,
  traceFrame: Record<string, unknown> | null,
  elapsedMs: unknown,
  budgetSeconds: number,
): Record<string, unknown> {
  const durableBusinessTurn = quickVerifyHasDurableBusinessTurn(promptIndex, turnSummary, traceFrame);
  const elapsed = typeof elapsedMs === "number" && Number.isFinite(elapsedMs) ? elapsedMs : null;
  const budgetMs = Math.max(0, budgetSeconds) * 1000;
  const budgetExceeded = elapsed !== null && budgetMs > 0 && elapsed > budgetMs;
  const observerTimeout = budgetExceeded || (failure !== null && isRecoverableQuickVerifyWaitFailure(failure));
  const status = durableBusinessTurn
    ? "business-turn-completed"
    : observerTimeout
      ? "observer-timeout"
      : "scenario-incomplete";
  return {
    status,
    durableBusinessTurn,
    observerTimeout,
    scenarioComplete: durableBusinessTurn,
    failure: failure ?? null,
    errorTitleZh: observerTimeout ? stringAtNullable(timeoutDisplayForQuickVerifyFailure(failure, promptIndex, budgetSeconds, null), "titleZh") : null,
    timeoutDisplay: observerTimeout ? timeoutDisplayForQuickVerifyFailure(failure, promptIndex, budgetSeconds, null) : null,
    promptIndex,
    elapsedMs: elapsed,
    budgetSeconds,
    sourceOfTruth: durableBusinessTurn ? "turn-summary-or-trace-frame" : observerTimeout ? "runner-wait-budget" : "control-findings",
    valuesRedacted: true,
  };
}

export function isRecoverableQuickVerifyWaitFailure(failure: string): boolean {
  return failure === "quick-verify-wait-chunk-timeout"
    || failure === "quick-verify-timeout-over-budget"
    || failure === "observe-turn-terminal-wait-failed";
}

function timeoutDisplayForFailure(failure: string | null, phase: string, targetValidationSeconds: number | null, runnerTimeoutSeconds: number | null): Record<string, unknown> {
  const targetSeconds = typeof targetValidationSeconds === "number" && Number.isFinite(targetValidationSeconds) ? Math.max(1, Math.trunc(targetValidationSeconds)) : null;
  const runnerSeconds = typeof runnerTimeoutSeconds === "number" && Number.isFinite(runnerTimeoutSeconds) ? Math.max(1, Math.trunc(runnerTimeoutSeconds)) : null;
  const effectiveSeconds = targetSeconds ?? runnerSeconds;
  const timeout = isTimeoutFailure(failure);
  const phaseTitle = phase === "observe-wait-turn-terminal" ? "Workbench 等待终态" : phase === "observe-wait-startup-ready" ? "Workbench 启动等待" : "quick-verify";
  const titleZh = timeout && effectiveSeconds !== null ? `${phaseTitle} 超时（${effectiveSeconds} 秒）` : failure === null ? "" : `${phaseTitle} 失败`;
  return {
    titleZh,
    budgetKind: targetSeconds !== null ? "targetValidation" : runnerSeconds !== null ? "runnerTimeoutSeconds" : "unknown",
    budgetSeconds: effectiveSeconds,
    targetValidationSeconds: targetSeconds,
    runnerTimeoutSeconds: runnerSeconds,
    phase,
    failure,
    valuesRedacted: true,
  };
}

export function timeoutDisplayForQuickVerifyFailure(failure: string | null, promptIndex: number, targetValidationSeconds: number | null, runnerTimeoutSeconds: number | null): Record<string, unknown> {
  if (!isTimeoutFailure(failure) || promptIndex <= 0) return timeoutDisplayForFailure(failure, "quick-verify", targetValidationSeconds, runnerTimeoutSeconds);
  const base = timeoutDisplayForFailure(failure, "observe-wait-turn-terminal", targetValidationSeconds, runnerTimeoutSeconds);
  const budgetSeconds = typeof base.budgetSeconds === "number" && Number.isFinite(base.budgetSeconds) ? Math.trunc(base.budgetSeconds) : null;
  return {
    ...base,
    titleZh: budgetSeconds === null ? `Workbench 第 ${promptIndex} 轮等待终态超时` : `Workbench 第 ${promptIndex} 轮等待终态超时（${budgetSeconds} 秒）`,
    round: promptIndex,
  };
}

function isTimeoutFailure(failure: string | null): boolean {
  return failure === "quick-verify-timeout-over-budget"
    || failure === "quick-verify-wait-chunk-timeout"
    || failure === "observe-turn-terminal-wait-failed";
}

function compactCommandWithTail(result: CommandResult): CompactCommandResult & { stdoutTail: string; stderrTail: string } {
  return {
    ...compactCommand(result),
    stdoutPreview: result.stdout.trim().slice(0, 1200),
    stderrPreview: result.stderr.trim().slice(0, 1200),
    stdoutTail: result.stdout.trim().slice(-4000),
    stderrTail: result.stderr.trim().slice(-4000),
  };
}
