fix: make sentinel trend chart show duration

This commit is contained in:
Codex
2026-07-01 01:23:38 +00:00
parent b41a261ebb
commit 473b4f2300
6 changed files with 487 additions and 208 deletions
+74 -1
View File
@@ -1,6 +1,8 @@
// SPEC: PJ2026-01060508 Web哨兵 draft-2026-06-25-p0-web-probe-sentinel.
// SPEC: PJ2026-01060508 Web哨兵 draft-2026-06-26-p9-multi-web-probe-sentinel.
// Responsibility: Redacted YAML configRef graph for web-probe sentinel plan/status.
import { readFileSync } from "node:fs";
import { rootPath } from "./config";
import { readWebProbeSentinelConfigRef } from "./hwlab-node-web-sentinel-config-ref";
import { HWLAB_WEB_PROBE_SENTINEL_CONFIG_REF_KEYS, type HwlabRuntimeLaneSpec, type HwlabRuntimeWebProbeSentinelConfigRefKey } from "./hwlab-node-lanes";
import { effectiveWebProbeSentinelPublicExposure, resolveWebProbeSentinel, webProbeSentinelRegistryRows, type WebProbeSentinelRegistryRow } from "./hwlab-node-web-sentinel-resolver";
@@ -100,7 +102,7 @@ const REQUIRED_TARGET_SHAPES: Record<HwlabRuntimeWebProbeSentinelConfigRefKey, R
},
reportViews: {
kind: "object",
requiredPaths: ["defaultView", "views[0]", "pageSize", "maxPageSize", "rawAccess", "redaction.prompt", "redaction.secrets"],
requiredPaths: ["defaultView", "views[0]", "pageSize", "maxPageSize", "rawAccess", "checkCatalogRef", "redaction.prompt", "redaction.secrets"],
},
publicExposure: {
kind: "object",
@@ -320,6 +322,7 @@ function crossReferenceConflicts(spec: HwlabRuntimeLaneSpec, refs: readonly Inte
const runtime = recordTarget(byKey.get("runtime"));
const scenarios = scenarioTargets(byKey.get("scenarios"));
const promptSet = recordTarget(byKey.get("promptSet"));
const reportViews = recordTarget(byKey.get("reportViews"));
const cicd = recordTarget(byKey.get("cicd"));
const secrets = recordTarget(byKey.get("secrets"));
@@ -350,6 +353,8 @@ function crossReferenceConflicts(spec: HwlabRuntimeLaneSpec, refs: readonly Inte
conflicts.push(`${byKey.get("cicd")?.file}#${byKey.get("cicd")?.path}.targetValidation.scenarioId=${validationScenarioId} is not declared in scenarios`);
}
validateReportViewsCheckCatalog(spec, conflicts, byKey.get("reportViews"), reportViews);
const runtimeNamespace = runtime === null ? null : stringAt(runtime, "namespace");
const runtimeSecrets = secrets === null ? [] : arrayAt(secrets, "runtimeSecrets");
if (runtimeNamespace !== null) {
@@ -363,6 +368,74 @@ function crossReferenceConflicts(spec: HwlabRuntimeLaneSpec, refs: readonly Inte
return conflicts;
}
function validateReportViewsCheckCatalog(spec: HwlabRuntimeLaneSpec, conflicts: string[], ref: InternalConfigRefStatus | undefined, reportViews: Record<string, unknown> | null): void {
if (reportViews === null) return;
const catalogRef = stringAt(reportViews, "checkCatalogRef");
if (catalogRef === null) {
conflicts.push(`${ref?.file ?? "-"}#${ref?.path ?? "-"}.checkCatalogRef is required`);
return;
}
let catalog: Record<string, unknown>;
try {
const target = readWebProbeSentinelConfigRef(spec, catalogRef).target;
catalog = isRecord(target) ? target : {};
} catch (error) {
conflicts.push(`${ref?.file ?? "-"}#${ref?.path ?? "-"}.checkCatalogRef=${catalogRef} is not readable: ${error instanceof Error ? error.message : String(error)}`);
return;
}
const items = arrayAt(catalog, "items");
if (items.length === 0) {
conflicts.push(`${catalogRef}.items is empty`);
return;
}
const byId = new Map<string, Record<string, unknown>>();
const codes = new Set<string>();
for (const [index, item] of items.entries()) {
const itemId = stringAt(item, "id");
const code = stringAt(item, "code");
if (itemId === null) conflicts.push(`${catalogRef}.items[${index}].id is required`);
else if (byId.has(itemId)) conflicts.push(`${catalogRef}.items id duplicated: ${itemId}`);
else byId.set(itemId, item);
if (code === null) conflicts.push(`${catalogRef}.items[${index}].code is required`);
else if (codes.has(code)) conflicts.push(`${catalogRef}.items code duplicated: ${code}`);
else codes.add(code);
for (const field of ["titleZh", "summaryZh", "actionZh"]) {
const text = stringAt(item, field);
if (text === null) {
conflicts.push(`${catalogRef}.items[${index}].${field} is required`);
} else if (!containsChinese(text)) {
conflicts.push(`${catalogRef}.items[${index}].${field} must contain Chinese text`);
}
}
}
const requiredIds = knownWebProbeFindingIds();
const missing = requiredIds.filter((id) => !byId.has(id));
if (missing.length > 0) {
conflicts.push(`${catalogRef}.items missing analyzer finding ids: ${missing.slice(0, 24).join(",")}${missing.length > 24 ? `,+${missing.length - 24}` : ""}`);
}
}
function knownWebProbeFindingIds(): string[] {
const ids = new Set<string>();
for (const file of [
"scripts/src/hwlab-node-web-observe-analyzer-source.ts",
"scripts/src/hwlab-node-web-sentinel-p5-observe.ts",
]) {
let source = "";
try {
source = readFileSync(rootPath(file), "utf8");
} catch {
continue;
}
for (const match of source.matchAll(/\bid:\s*["`]([A-Za-z0-9_.:-]+)["`]/gu)) ids.add(match[1]);
}
return Array.from(ids).sort();
}
function containsChinese(value: string): boolean {
return /[\u3400-\u9fff]/u.test(value);
}
function requireReadableConfigRef(spec: HwlabRuntimeLaneSpec, conflicts: string[], ref: InternalConfigRefStatus | undefined, path: string, value: unknown): void {
if (typeof value !== "string" || value.length === 0) return;
try {