feat: add JD01 YAML-first deployment support
This commit is contained in:
@@ -1,9 +1,7 @@
|
||||
// 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 { createHash } from "node:crypto";
|
||||
import { existsSync, 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";
|
||||
import type { RenderedCliResult } from "./output";
|
||||
@@ -212,7 +210,7 @@ export function webProbeSentinelConfigPlan(spec: HwlabRuntimeLaneSpec, action: W
|
||||
|
||||
const selected = resolveWebProbeSentinel(spec, sentinelId);
|
||||
const refs = HWLAB_WEB_PROBE_SENTINEL_CONFIG_REF_KEYS
|
||||
.map((key) => readSentinelConfigRef(key, selected.configRefs[key]))
|
||||
.map((key) => readSentinelConfigRef(spec, key, selected.configRefs[key]))
|
||||
.map((ref) => effectiveConfigRefStatus(spec, selected.id, ref));
|
||||
const conflicts = selected.enabled ? crossReferenceConflicts(spec, refs) : [];
|
||||
const refBlocked = refs.some((ref) => !ref.present || !ref.targetPresent || ref.missingFields.length > 0 || ref.conflicts.length > 0 || ref.error !== null);
|
||||
@@ -255,28 +253,22 @@ export function withWebProbeSentinelConfigRendered(value: WebProbeSentinelConfig
|
||||
};
|
||||
}
|
||||
|
||||
function readSentinelConfigRef(key: HwlabRuntimeWebProbeSentinelConfigRefKey, ref: string): InternalConfigRefStatus {
|
||||
const parsed = parseConfigRef(ref);
|
||||
if (parsed.error !== null) return emptyRefStatus(key, ref, parsed.file, parsed.path, parsed.error);
|
||||
const absPath = rootPath(parsed.file);
|
||||
if (!existsSync(absPath)) return emptyRefStatus(key, ref, parsed.file, parsed.path, `${parsed.file} does not exist`);
|
||||
function readSentinelConfigRef(spec: HwlabRuntimeLaneSpec, key: HwlabRuntimeWebProbeSentinelConfigRefKey, ref: string): InternalConfigRefStatus {
|
||||
try {
|
||||
const text = readFileSync(absPath, "utf8");
|
||||
const sha256 = `sha256:${createHash("sha256").update(text).digest("hex")}`;
|
||||
const doc = Bun.YAML.parse(text) as unknown;
|
||||
const target = valueAtPath(doc, parsed.path);
|
||||
const resolved = readWebProbeSentinelConfigRef(spec, ref);
|
||||
const target = resolved.target;
|
||||
const targetKind = target === undefined ? "missing" : targetKindOf(target);
|
||||
const missingFields = target === undefined ? ["target"] : missingFieldsForTarget(key, target);
|
||||
return {
|
||||
key,
|
||||
ref,
|
||||
file: parsed.file,
|
||||
path: parsed.path,
|
||||
file: resolved.file,
|
||||
path: resolved.path,
|
||||
present: true,
|
||||
targetPresent: target !== undefined,
|
||||
targetKind,
|
||||
sha256,
|
||||
byteCount: Buffer.byteLength(text),
|
||||
sha256: resolved.sha256,
|
||||
byteCount: resolved.byteCount,
|
||||
missingFields,
|
||||
conflicts: [],
|
||||
summary: summarizeTarget(key, target),
|
||||
@@ -285,21 +277,10 @@ function readSentinelConfigRef(key: HwlabRuntimeWebProbeSentinelConfigRefKey, re
|
||||
};
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
return emptyRefStatus(key, ref, parsed.file, parsed.path, message);
|
||||
return emptyRefStatus(key, ref, "", "", message);
|
||||
}
|
||||
}
|
||||
|
||||
function parseConfigRef(ref: string): { readonly file: string; readonly path: string; readonly error: string | null } {
|
||||
const [file, path, extra] = ref.split("#");
|
||||
if (extra !== undefined || file === undefined || path === undefined || file.length === 0 || path.length === 0) {
|
||||
return { file: file ?? "", path: path ?? "", error: "configRef must use path/to/file.yaml#object.path" };
|
||||
}
|
||||
if (file.startsWith("/") || file.includes("\0") || file.includes("..") || !file.startsWith("config/") || !file.endsWith(".yaml")) {
|
||||
return { file, path, error: "configRef file must be repo-relative config/*.yaml without .." };
|
||||
}
|
||||
return { file, path, error: null };
|
||||
}
|
||||
|
||||
function emptyRefStatus(key: HwlabRuntimeWebProbeSentinelConfigRefKey, ref: string, file: string, path: string, error: string): InternalConfigRefStatus {
|
||||
return {
|
||||
key,
|
||||
@@ -348,8 +329,6 @@ function crossReferenceConflicts(spec: HwlabRuntimeLaneSpec, refs: readonly Inte
|
||||
requireEquals(conflicts, byKey.get("runtime"), "namespace", spec.runtimeNamespace, `selected namespace ${spec.runtimeNamespace}`);
|
||||
}
|
||||
|
||||
const promptSetRef = byKey.get("promptSet")?.ref ?? null;
|
||||
const reportViewsRef = byKey.get("reportViews")?.ref ?? null;
|
||||
const scenarioIds = new Set<string>();
|
||||
const scenarioProviders = new Set<string>();
|
||||
for (const [index, scenario] of scenarios.entries()) {
|
||||
@@ -357,8 +336,8 @@ function crossReferenceConflicts(spec: HwlabRuntimeLaneSpec, refs: readonly Inte
|
||||
if (id !== null) scenarioIds.add(id);
|
||||
const provider = stringAt(scenario, "providerProfile");
|
||||
if (provider !== null) scenarioProviders.add(provider);
|
||||
if (promptSetRef !== null) requireEquals(conflicts, byKey.get("scenarios"), `[${index}].promptSetRef`, promptSetRef, "promptSet configRef");
|
||||
if (reportViewsRef !== null) requireEquals(conflicts, byKey.get("scenarios"), `[${index}].reportViewRef`, reportViewsRef, "reportViews configRef");
|
||||
requireReadableConfigRef(spec, conflicts, byKey.get("scenarios"), `[${index}].promptSetRef`, scenario.promptSetRef);
|
||||
requireReadableConfigRef(spec, conflicts, byKey.get("scenarios"), `[${index}].reportViewRef`, scenario.reportViewRef);
|
||||
}
|
||||
|
||||
const promptProvider = promptSet === null ? null : stringAt(promptSet, "providerProfile");
|
||||
@@ -384,6 +363,15 @@ function crossReferenceConflicts(spec: HwlabRuntimeLaneSpec, refs: readonly Inte
|
||||
return conflicts;
|
||||
}
|
||||
|
||||
function requireReadableConfigRef(spec: HwlabRuntimeLaneSpec, conflicts: string[], ref: InternalConfigRefStatus | undefined, path: string, value: unknown): void {
|
||||
if (typeof value !== "string" || value.length === 0) return;
|
||||
try {
|
||||
readWebProbeSentinelConfigRef(spec, value);
|
||||
} catch (error) {
|
||||
conflicts.push(`${ref?.file ?? "-"}#${ref?.path ?? "-"}.${path}=${value} is not readable: ${error instanceof Error ? error.message : String(error)}`);
|
||||
}
|
||||
}
|
||||
|
||||
function requireEquals(conflicts: string[], ref: InternalConfigRefStatus | undefined, path: string, expected: string, expectedLabel: string): void {
|
||||
if (ref === undefined) return;
|
||||
const actual = stringAt(ref.target, path);
|
||||
|
||||
Reference in New Issue
Block a user