feat(sentinel): restore JD01 cadence cronjob visibility

This commit is contained in:
Codex
2026-07-01 06:43:30 +00:00
parent 622804f95d
commit c7ea34f253
12 changed files with 503 additions and 28 deletions
@@ -1,4 +1,5 @@
// SPEC: PJ2026-01060508 Web哨兵 draft-2026-06-27-p11-monitor-web-observability-dashboard.
// SPEC: PJ2026-01060508 Web哨兵 draft-2026-07-01-p15-cadence-otel.
// Responsibility: Quick-verify observe orchestration and artifact interpretation for web-probe sentinel P5 validation.
import { createHash, randomUUID } from "node:crypto";
import { existsSync, readFileSync } from "node:fs";
@@ -32,6 +33,7 @@ import {
text,
withWarnings,
} from "./hwlab-node-web-sentinel-cicd";
import { emitWebProbeSentinelSpan } from "./hwlab-node-web-sentinel-otel";
function printQuickVerifyProgress(state: SentinelCicdState, runId: string | null, phase: string, status: string, extra: Record<string, unknown> = {}): void {
const compactExtra = Object.fromEntries(Object.entries(extra).map(([key, value]) => {
@@ -60,12 +62,30 @@ export function runSentinelQuickVerify(state: SentinelCicdState, reason: string,
const maxSeconds = numberAt(state.cicd, "targetValidation.maxSeconds");
const scenario = findScenario(state, scenarioId);
if (scenario === null) return { ok: false, status: "blocked", reason: "scenario-not-found", scenarioId, valuesRedacted: true };
const runId = `sentinel-run-${Date.now().toString(36)}-${randomUUID().slice(0, 8)}`;
emitWebProbeSentinelSpan(sentinelOtelContext(state), "web_probe_sentinel.quick_verify.job_start", {
scenarioId,
runId,
cadence: stringAtNullable(scenario, "cadence"),
status: "running",
valuesRedacted: true,
});
const commandSequence = arrayAt(scenario, "commandSequence").map(record);
const needsPromptSet = commandSequence.some((item) => stringAt(item, "type") === "sendPrompt" && inlinePromptText(item) === null);
const prompts = needsPromptSet
? readPromptSetForScenario(state, scenario)
: { ok: true as const, prompts: [], summary: { source: "not-required", promptCount: 0, valuesRedacted: true } };
if (!prompts.ok) return { ok: false, status: "blocked", reason: "prompt-source-unavailable", promptSource: prompts, valuesRedacted: true };
if (!prompts.ok) {
emitWebProbeSentinelSpan(sentinelOtelContext(state), "web_probe_sentinel.quick_verify.job_finish", {
scenarioId,
runId,
status: "blocked",
exitCode: 1,
failureKind: "prompt-source-unavailable",
valuesRedacted: true,
}, false);
return { ok: false, status: "blocked", reason: "prompt-source-unavailable", promptSource: prompts, valuesRedacted: true };
}
const accountEnv = quickVerifyAccountEnv(state);
if (!accountEnv.ok) {
const findings = [{
@@ -78,7 +98,7 @@ export function runSentinelQuickVerify(state: SentinelCicdState, reason: string,
}];
return recordQuickVerify(state, {
ok: false,
runId: `sentinel-run-${Date.now().toString(36)}-${randomUUID().slice(0, 8)}`,
runId,
scenarioId,
reason,
status: "blocked",
@@ -104,7 +124,6 @@ export function runSentinelQuickVerify(state: SentinelCicdState, reason: string,
const hardBudgetSeconds = Math.min(timeoutSeconds, Math.max(maxSeconds, numberAt(scenario, "maxRunSeconds")));
const elapsedWarnings = () => targetValidationElapsedWarnings(elapsedMs(), "quick verify confirm-wait", warningBudgetSeconds);
const deadline = Date.now() + hardBudgetSeconds * 1000;
const runId = `sentinel-run-${Date.now().toString(36)}-${randomUUID().slice(0, 8)}`;
printQuickVerifyProgress(state, runId, "start", "running", { scenarioId, reason, warningBudgetSeconds, hardBudgetSeconds, timeoutSeconds });
const steps: Record<string, unknown>[] = [];
const startArgs = [
@@ -659,9 +678,29 @@ function recordQuickVerify(state: SentinelCicdState, payload: Record<string, unk
maintenance: payload.reason === "maintenance-stop",
valuesRedacted: true,
}, 60);
emitWebProbeSentinelSpan(sentinelOtelContext(state), "web_probe_sentinel.quick_verify.job_finish", {
scenarioId: payload.scenarioId,
runId: payload.runId,
observerId: payload.observerId,
status: payload.status,
exitCode: payload.ok === true && recordResult.ok === true ? 0 : 1,
failureKind: payload.failure ?? (recordResult.ok === true ? null : "record-run-failed"),
valuesRedacted: true,
}, payload.ok === true && recordResult.ok === true);
return withWarnings({ ...payload, views, recordResult, valuesRedacted: true }, recordResult.ok === true ? [] : ["quick verify completed but sentinel report index record failed; report/dashboard may lag until record payload is reduced or retried."]);
}
function sentinelOtelContext(state: SentinelCicdState): { readonly node: string; readonly lane: string; readonly sentinelId: string; readonly namespace: string | null; readonly runtime: Record<string, unknown>; readonly cicd: Record<string, unknown> } {
return {
node: state.spec.nodeId,
lane: state.spec.lane,
sentinelId: state.sentinelId,
namespace: stringAtNullable(state.runtime, "namespace"),
runtime: state.runtime,
cicd: state.cicd,
};
}
function compactQuickVerifyRecordViews(views: Record<string, unknown>): Record<string, unknown> {
const compacted: Record<string, unknown> = {};
for (const [key, value] of Object.entries(views)) {