fix: make sentinel trend chart show duration
This commit is contained in:
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user