468 lines
19 KiB
JavaScript
468 lines
19 KiB
JavaScript
export const CODE_AGENT_M3_IO_ROUTE = "/v1/m3/io";
|
|
export const CODE_AGENT_M3_TRUSTED_ROUTE = Object.freeze({
|
|
fromResourceId: "res_boxsimu_1",
|
|
fromPort: "DO1",
|
|
patchPanelServiceId: "hwlab-patch-panel",
|
|
toResourceId: "res_boxsimu_2",
|
|
toPort: "DI1"
|
|
});
|
|
export const CODE_AGENT_M3_SKILL_PROVIDER = "legacy-skill-cli";
|
|
export const CODE_AGENT_M3_SKILL_RUNNER_KIND = "legacy-m3-io-skill-cli";
|
|
export const CODE_AGENT_M3_SKILL_NAME = "hwlab-agent-runtime.m3-io";
|
|
export const CODE_AGENT_M3_PATH_LABEL = "Code Agent -> Skill CLI -> HWLAB API";
|
|
|
|
const missingProofLabel = "未产生/不可证明";
|
|
const nonProofIds = new Set(["", "n/a", "none", "null", "undefined", "not_observed", "not-observed"]);
|
|
const directPathFields = Object.freeze([
|
|
"directGatewayCalls",
|
|
"directBoxCalls",
|
|
"directPatchPanelCalls",
|
|
"directGatewayCallsAllowed",
|
|
"directBoxCallsAllowed",
|
|
"directBoxSimuCallsAllowed",
|
|
"directPatchPanelCallsAllowed"
|
|
]);
|
|
const safeInternalLinkPattern = /^\/(?:v1\/(?:m3|audit|evidence|operations?)|json-rpc\b|gate\b|diagnostics\/gate\b)/u;
|
|
|
|
export function codeAgentM3EvidenceContractSummary() {
|
|
return [
|
|
"Code Agent M3 evidence renderer consumes Skill CLI / /v1/m3/io facts from providerTrace, runnerTrace, and toolCalls.",
|
|
"It renders operation/audit/evidence metadata: operationId, traceId, auditId, evidenceId, route, target, readback, accepted/status/blocker, and direct-path validity.",
|
|
"Missing proof IDs render as 未产生/不可证明; direct gateway/box/patch-panel paths never render as trusted pass."
|
|
].join(" ");
|
|
}
|
|
|
|
export function isCodeAgentM3SkillCompletion(value) {
|
|
const evidence = extractCodeAgentM3Evidence(value);
|
|
return Boolean(
|
|
evidence &&
|
|
value?.status === "completed" &&
|
|
evidence.route === CODE_AGENT_M3_IO_ROUTE &&
|
|
evidence.directPathInvalid !== true &&
|
|
evidence.provider === CODE_AGENT_M3_SKILL_PROVIDER &&
|
|
evidence.runnerKind === CODE_AGENT_M3_SKILL_RUNNER_KIND
|
|
);
|
|
}
|
|
|
|
export function extractCodeAgentM3Evidence(message) {
|
|
const toolCall = findM3ToolCall(message);
|
|
const m3Io = objectOrNull(message?.m3Io);
|
|
const source = toolCall ?? message?.providerTrace ?? message?.runnerTrace ?? null;
|
|
if (!source && !m3Io && !isM3ProviderMessage(message)) return null;
|
|
|
|
const providerTrace = objectOrNull(message?.providerTrace);
|
|
const runnerTrace = objectOrNull(message?.runnerTrace);
|
|
const route = firstNonEmpty(source?.route, m3Io?.trace?.route, m3Io?.path?.hwlabApi?.route, providerTrace?.route, runnerTrace?.route, CODE_AGENT_M3_IO_ROUTE);
|
|
const provider = firstNonEmpty(message?.provider, source?.provider, providerTrace?.provider);
|
|
const runnerKind = firstNonEmpty(message?.runner?.kind, source?.runnerKind, providerTrace?.runnerKind, runnerTrace?.runnerKind);
|
|
const command = objectOrNull(source?.command) ?? objectOrNull(message?.command);
|
|
const result = objectOrNull(source?.result) ?? objectOrNull(message?.result);
|
|
const response = parseToolStdout(source?.stdout);
|
|
const blocker = firstBlocker(source, response, providerTrace, runnerTrace, message);
|
|
const trustBlocker = firstTrustBlocker(m3Io, source, response, providerTrace, runnerTrace, message);
|
|
const target = normalizeTarget(source, response, command, m3Io);
|
|
const readback = normalizeReadback(source, response, result, m3Io);
|
|
const action = firstNonEmpty(m3Io?.action, source?.action, response?.action, command?.action);
|
|
const trust = normalizeTrust(m3Io, source, response, trustBlocker);
|
|
const ids = {
|
|
operationId: firstNonEmpty(m3Io?.operation?.operationId, source?.operationId, response?.operationId, providerTrace?.operationId, runnerTrace?.operationId, message?.operationId),
|
|
traceId: firstNonEmpty(m3Io?.trace?.traceId, source?.traceId, response?.traceId, providerTrace?.traceId, runnerTrace?.traceId, message?.traceId),
|
|
auditId: firstNonEmpty(m3Io?.operation?.auditId, source?.audit?.auditId, response?.audit?.auditId, message?.auditId, message?.audit?.auditId),
|
|
evidenceId: firstNonEmpty(m3Io?.operation?.evidenceId, source?.evidence?.evidenceId, response?.evidence?.evidenceId, message?.evidenceId, message?.evidence?.evidenceId)
|
|
};
|
|
const directPathInvalid = classifyDirectPathInvalid(source, response, message);
|
|
const readbackMismatch = classifyReadbackMismatch({
|
|
action,
|
|
target,
|
|
readback,
|
|
blocker
|
|
});
|
|
const accepted = normalizeBoolean(firstDefined(m3Io?.accepted, source?.accepted, response?.accepted, providerTrace?.accepted));
|
|
const status = firstNonEmpty(m3Io?.status, source?.status, response?.status, providerTrace?.status, runnerTrace?.status, message?.status);
|
|
const links = normalizeEvidenceLinks(source, response, providerTrace, message);
|
|
const path = normalizePath(m3Io, route);
|
|
const wiring = normalizeWiring(m3Io);
|
|
|
|
if (!isM3EvidenceShape({ route, provider, runnerKind, source, m3Io })) return null;
|
|
|
|
return {
|
|
kind: "m3-io",
|
|
responseType: firstNonEmpty(m3Io?.type, source?.responseType, message?.responseType),
|
|
provider,
|
|
runnerKind,
|
|
route,
|
|
action,
|
|
status,
|
|
accepted,
|
|
blocker,
|
|
trustBlocker,
|
|
trust,
|
|
directPathInvalid,
|
|
readbackMismatch,
|
|
target,
|
|
readback,
|
|
path,
|
|
wiring,
|
|
ids,
|
|
links,
|
|
verdict: classifyM3Verdict({
|
|
accepted,
|
|
status,
|
|
blocker,
|
|
trustBlocker,
|
|
trust,
|
|
directPathInvalid,
|
|
readbackMismatch,
|
|
ids
|
|
}),
|
|
source
|
|
};
|
|
}
|
|
|
|
export function m3EvidenceRows(evidence) {
|
|
if (!evidence) return [];
|
|
const idRows = [
|
|
evidenceValueRow("operationId", evidence.ids.operationId, { copyable: true, href: evidence.links.operationId }),
|
|
evidenceValueRow("traceId", evidence.ids.traceId, { copyable: true, href: evidence.links.traceId }),
|
|
evidenceValueRow("auditId", evidence.ids.auditId, { copyable: true, href: evidence.links.auditId }),
|
|
evidenceValueRow("evidenceId", evidence.ids.evidenceId, { copyable: true, href: evidence.links.evidenceId })
|
|
];
|
|
const target = evidence.target;
|
|
const readback = evidence.readback;
|
|
return [
|
|
evidenceValueRow("状态", evidence.verdict.label, { tone: evidence.verdict.tone }),
|
|
evidenceValueRow("类型", evidence.responseType ?? evidence.kind, { tone: evidence.responseType === "m3_io_blocker" ? "blocked" : "source" }),
|
|
evidenceValueRow("动作", evidence.action ?? missingProofLabel),
|
|
evidenceValueRow("accepted", evidence.accepted === null ? "unknown" : String(evidence.accepted), { tone: evidence.accepted === true ? "source" : "blocked" }),
|
|
evidenceValueRow("status", evidence.status ?? missingProofLabel, { tone: evidence.status === "blocked" ? "blocked" : "source" }),
|
|
evidenceValueRow("路径", evidence.path.summary, { copyable: true }),
|
|
evidenceValueRow("接线", evidence.wiring.label, { copyable: true }),
|
|
evidenceValueRow("route", evidence.route ?? missingProofLabel, { copyable: true, href: evidence.links.route }),
|
|
evidenceValueRow("DO1", resourcePortValueLabel(target), { copyable: false }),
|
|
evidenceValueRow("DI1", resourcePortValueLabel(readback), { tone: evidence.readbackMismatch ? "blocked" : "source" }),
|
|
evidenceValueRow("trusted", booleanStateLabel(evidence.trust.trusted), { tone: evidence.trust.trusted === true ? "dev-live" : evidence.trust.trusted === false ? "warn" : "source" }),
|
|
evidenceValueRow("durable", booleanStateLabel(evidence.trust.durable), { tone: evidence.trust.durable === true ? "dev-live" : evidence.trust.durable === false ? "warn" : "source" }),
|
|
...idRows,
|
|
evidenceValueRow("blocker", blockerLabel(evidence.blocker), { tone: evidence.blocker ? "blocked" : "source" })
|
|
];
|
|
}
|
|
|
|
export function m3EvidenceSummaryText(evidence) {
|
|
if (!evidence) return "";
|
|
return `${evidence.verdict.label} / ${evidence.path.summary} / DO1=${formatValue(evidence.target.value)} / DI1=${formatValue(evidence.readback.value)} / operation=${proofLabel(evidence.ids.operationId)}`;
|
|
}
|
|
|
|
export function proofLabel(value) {
|
|
const text = stringOrNull(value);
|
|
return text && !nonProofIds.has(text.toLowerCase()) ? text : missingProofLabel;
|
|
}
|
|
|
|
function evidenceValueRow(label, value, options = {}) {
|
|
const proofValue = label.endsWith("Id") ? proofLabel(value) : String(value ?? missingProofLabel);
|
|
return {
|
|
label,
|
|
value: proofValue,
|
|
tone: options.tone ?? (proofValue === missingProofLabel ? "blocked" : "source"),
|
|
copyable: options.copyable === true && proofValue !== missingProofLabel,
|
|
href: safeInternalHref(options.href)
|
|
};
|
|
}
|
|
|
|
function classifyM3Verdict({ accepted, status, blocker, trustBlocker, trust, directPathInvalid, readbackMismatch, ids }) {
|
|
if (directPathInvalid) {
|
|
return {
|
|
key: "direct-path-invalid",
|
|
tone: "blocked",
|
|
label: "direct-path-invalid",
|
|
summary: "直接 gateway/box/patch-panel 路径无效,不能显示为可信闭环通过。"
|
|
};
|
|
}
|
|
if (readbackMismatch) {
|
|
return {
|
|
key: "readback-mismatch",
|
|
tone: "blocked",
|
|
label: "readback-mismatch",
|
|
summary: "DI1 readback 与 DO1 target value 不一致,不能证明闭环。"
|
|
};
|
|
}
|
|
if (blocker || accepted === false || String(status ?? "").toLowerCase() === "blocked") {
|
|
return {
|
|
key: "blocked",
|
|
tone: "blocked",
|
|
label: "blocked",
|
|
summary: blockerLabel(blocker)
|
|
};
|
|
}
|
|
if (accepted === true && ["succeeded", "completed", "accepted"].includes(String(status ?? "").toLowerCase())) {
|
|
if (trustBlocker || trust?.trusted === false) {
|
|
return {
|
|
key: "accepted-untrusted",
|
|
tone: "degraded",
|
|
label: "accepted / 可信持久化未通过",
|
|
summary: trustBlocker ? blockerLabel(trustBlocker) : "受控路径已返回,但 trusted/durable 未 green。"
|
|
};
|
|
}
|
|
const missingIds = Object.entries(ids).filter(([, value]) => proofLabel(value) === missingProofLabel);
|
|
if (missingIds.length > 0) {
|
|
return {
|
|
key: "accepted-missing-proof",
|
|
tone: "degraded",
|
|
label: "accepted / 缺少证据ID",
|
|
summary: `accepted 但缺少 ${missingIds.map(([key]) => key).join(", ")},不能证明完整可信闭环。`
|
|
};
|
|
}
|
|
return {
|
|
key: "accepted",
|
|
tone: "dev-live",
|
|
label: "accepted",
|
|
summary: "受控 Skill CLI 已通过 HWLAB API /v1/m3/io 返回 operation/audit/evidence facts。"
|
|
};
|
|
}
|
|
return {
|
|
key: "unknown",
|
|
tone: "blocked",
|
|
label: "未产生/不可证明",
|
|
summary: "没有足够的 M3 IO 事实证明该回复执行了受控路径。"
|
|
};
|
|
}
|
|
|
|
function classifyReadbackMismatch({ action, target, readback, blocker }) {
|
|
if (String(action ?? "").toLowerCase() !== "do.write") return false;
|
|
if (target.value === undefined || target.value === null || readback.value === undefined || readback.value === null) return false;
|
|
if (normalizeBoolean(target.value) === normalizeBoolean(readback.value)) return false;
|
|
return true;
|
|
}
|
|
|
|
function classifyDirectPathInvalid(...sources) {
|
|
for (const source of sources) {
|
|
if (!source || typeof source !== "object") continue;
|
|
const safety = objectOrNull(source.safety) ?? {};
|
|
const hwlabApi = objectOrNull(source.hwlabApi) ?? {};
|
|
const controlPath = objectOrNull(source.controlPath) ?? {};
|
|
const blockerText = [
|
|
source.blocker?.code,
|
|
source.blocker?.message,
|
|
source.blocker?.zh,
|
|
source.capabilityBlocker?.code,
|
|
source.capabilityBlocker?.message,
|
|
source.capabilityBlocker?.zh,
|
|
source.error?.code,
|
|
source.error?.message
|
|
].filter(Boolean).join(" ");
|
|
if (/direct[_-]?(?:hardware|gateway|box|patch)/iu.test(blockerText)) return true;
|
|
if (controlPath.frontendBypass === true || hwlabApi.cloudApiOnly === false) return true;
|
|
for (const field of directPathFields) {
|
|
if (source[field] === true || safety[field] === true || hwlabApi[field] === true) return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function normalizeTarget(source, response, command, m3Io = null) {
|
|
const target = objectOrNull(source?.target) ?? objectOrNull(response?.target) ?? {};
|
|
const commandObject = objectOrNull(command) ?? objectOrNull(response?.command) ?? {};
|
|
return {
|
|
resourceId: firstNonEmpty(m3Io?.do1?.resourceId, target.resourceId, commandObject.resourceId, source?.resourceId, response?.resourceId, CODE_AGENT_M3_TRUSTED_ROUTE.fromResourceId),
|
|
port: firstNonEmpty(m3Io?.do1?.port, target.port, commandObject.port, source?.port, response?.port, CODE_AGENT_M3_TRUSTED_ROUTE.fromPort),
|
|
value: firstDefined(m3Io?.do1?.targetValue, target.value, commandObject.value, source?.value, response?.value)
|
|
};
|
|
}
|
|
|
|
function normalizeReadback(source, response, result, m3Io = null) {
|
|
const targetReadback = objectOrNull(result?.targetReadback) ??
|
|
objectOrNull(response?.result?.targetReadback) ??
|
|
objectOrNull(source?.result?.targetReadback) ??
|
|
objectOrNull(source?.targetReadback) ??
|
|
{};
|
|
return {
|
|
resourceId: firstNonEmpty(m3Io?.di1?.resourceId, targetReadback.resourceId, CODE_AGENT_M3_TRUSTED_ROUTE.toResourceId),
|
|
port: firstNonEmpty(m3Io?.di1?.port, targetReadback.port, CODE_AGENT_M3_TRUSTED_ROUTE.toPort),
|
|
value: firstDefined(m3Io?.di1?.observedValue, targetReadback.value, result?.value)
|
|
};
|
|
}
|
|
|
|
function normalizePath(m3Io, route) {
|
|
return {
|
|
summary: firstNonEmpty(m3Io?.path?.summary, `${CODE_AGENT_M3_PATH_LABEL} ${route ?? CODE_AGENT_M3_IO_ROUTE}`),
|
|
segments: Array.isArray(m3Io?.path?.segments) && m3Io.path.segments.length > 0
|
|
? m3Io.path.segments
|
|
: ["Code Agent", "Skill CLI", "HWLAB API"]
|
|
};
|
|
}
|
|
|
|
function normalizeWiring(m3Io) {
|
|
return {
|
|
from: firstNonEmpty(m3Io?.wiring?.from, `${CODE_AGENT_M3_TRUSTED_ROUTE.fromResourceId}:${CODE_AGENT_M3_TRUSTED_ROUTE.fromPort}`),
|
|
via: firstNonEmpty(m3Io?.wiring?.via, CODE_AGENT_M3_TRUSTED_ROUTE.patchPanelServiceId),
|
|
to: firstNonEmpty(m3Io?.wiring?.to, `${CODE_AGENT_M3_TRUSTED_ROUTE.toResourceId}:${CODE_AGENT_M3_TRUSTED_ROUTE.toPort}`),
|
|
label: firstNonEmpty(
|
|
m3Io?.wiring?.label,
|
|
`${CODE_AGENT_M3_TRUSTED_ROUTE.fromResourceId}:${CODE_AGENT_M3_TRUSTED_ROUTE.fromPort} -> ${CODE_AGENT_M3_TRUSTED_ROUTE.patchPanelServiceId} -> ${CODE_AGENT_M3_TRUSTED_ROUTE.toResourceId}:${CODE_AGENT_M3_TRUSTED_ROUTE.toPort}`
|
|
)
|
|
};
|
|
}
|
|
|
|
function normalizeTrust(m3Io, source, response, trustBlocker) {
|
|
const trust = objectOrNull(m3Io?.trust) ?? {};
|
|
const durableValue = firstDefined(
|
|
trust.durable,
|
|
source?.durable?.durable,
|
|
response?.durable?.durable
|
|
);
|
|
const trustedValue = firstDefined(trust.trusted, source?.trusted, response?.trusted);
|
|
const durable = normalizeBoolean(durableValue);
|
|
const trusted = normalizeBoolean(trustedValue);
|
|
return {
|
|
trusted: trusted === null ? (trustBlocker ? false : null) : trusted,
|
|
durable: durable === null ? null : durable,
|
|
blocker: trustBlocker,
|
|
sourceKind: firstNonEmpty(trust.evidenceSourceKind, source?.evidence?.sourceKind, response?.evidence?.sourceKind),
|
|
durableStatus: firstNonEmpty(trust.durableStatus, source?.durable?.status, response?.durable?.status)
|
|
};
|
|
}
|
|
|
|
function resourcePortValueLabel(item) {
|
|
const value = item.value === undefined || item.value === null ? "value=未产生/不可证明" : `value=${String(item.value)}`;
|
|
return `${item.resourceId ?? missingProofLabel}:${item.port ?? missingProofLabel} ${value}`;
|
|
}
|
|
|
|
function firstBlocker(...sources) {
|
|
for (const source of sources) {
|
|
if (!source || typeof source !== "object") continue;
|
|
const candidate = objectOrNull(source.blocker) ?? objectOrNull(source.capabilityBlocker) ?? objectOrNull(source.error?.blocker);
|
|
if (candidate) return candidate;
|
|
if (Array.isArray(source.blockers)) {
|
|
const blocker = source.blockers.find((item) => (item?.code || item?.message || item?.zh) && !isTrustOnlyBlocker(item));
|
|
if (blocker) return blocker;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function firstTrustBlocker(...sources) {
|
|
for (const source of sources) {
|
|
if (!source || typeof source !== "object") continue;
|
|
const candidate = objectOrNull(source.trustBlocker) ?? objectOrNull(source.trust?.trustBlocker);
|
|
if (candidate) return candidate;
|
|
if (Array.isArray(source.blockers)) {
|
|
const blocker = source.blockers.find((item) => (item?.code || item?.message || item?.zh) && isTrustOnlyBlocker(item));
|
|
if (blocker) return blocker;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function isTrustOnlyBlocker(blocker) {
|
|
const text = `${blocker?.code ?? ""} ${blocker?.layer ?? ""} ${blocker?.category ?? ""}`;
|
|
return /durable|runtime|trust|evidence/i.test(text);
|
|
}
|
|
|
|
function blockerLabel(blocker) {
|
|
if (!blocker) return "none";
|
|
return [blocker.code, blocker.zh ?? blocker.message ?? blocker.reason].filter(Boolean).join(": ");
|
|
}
|
|
|
|
function normalizeEvidenceLinks(...sources) {
|
|
const links = {};
|
|
for (const source of sources) {
|
|
if (!source || typeof source !== "object") continue;
|
|
const candidates = [
|
|
source.links,
|
|
source.urls,
|
|
source.detailUrls,
|
|
source.internalRoutes,
|
|
source.evidence?.links,
|
|
source.audit?.links
|
|
].filter((item) => item && typeof item === "object");
|
|
for (const candidate of candidates) {
|
|
for (const key of ["operationId", "traceId", "auditId", "evidenceId", "route"]) {
|
|
links[key] ??= safeInternalHref(candidate[key] ?? candidate[key.replace(/Id$/u, "")]);
|
|
}
|
|
}
|
|
}
|
|
return links;
|
|
}
|
|
|
|
function safeInternalHref(value) {
|
|
const text = stringOrNull(value);
|
|
if (!text || !safeInternalLinkPattern.test(text)) return null;
|
|
return text;
|
|
}
|
|
|
|
function parseToolStdout(stdout) {
|
|
const text = stringOrNull(stdout);
|
|
if (!text) return null;
|
|
try {
|
|
const payload = JSON.parse(text);
|
|
return payload && typeof payload === "object" ? payload : null;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function isM3EvidenceShape({ route, provider, runnerKind, source, m3Io }) {
|
|
if (route !== CODE_AGENT_M3_IO_ROUTE) return false;
|
|
if (m3Io?.type === "m3_io_result" || m3Io?.type === "m3_io_blocker") return true;
|
|
if (provider === CODE_AGENT_M3_SKILL_PROVIDER) return true;
|
|
if (runnerKind === CODE_AGENT_M3_SKILL_RUNNER_KIND) return true;
|
|
if (source?.name === CODE_AGENT_M3_SKILL_NAME) return true;
|
|
return false;
|
|
}
|
|
|
|
function findM3ToolCall(message) {
|
|
const calls = Array.isArray(message?.toolCalls) ? message.toolCalls : [];
|
|
return calls.find((tool) =>
|
|
tool?.name === CODE_AGENT_M3_SKILL_NAME ||
|
|
tool?.route === CODE_AGENT_M3_IO_ROUTE ||
|
|
tool?.runnerKind === CODE_AGENT_M3_SKILL_RUNNER_KIND ||
|
|
tool?.type === "skill-cli" && tool?.hwlabApi?.route === CODE_AGENT_M3_IO_ROUTE
|
|
) ?? null;
|
|
}
|
|
|
|
function isM3ProviderMessage(message) {
|
|
return message?.provider === CODE_AGENT_M3_SKILL_PROVIDER ||
|
|
message?.runner?.kind === CODE_AGENT_M3_SKILL_RUNNER_KIND ||
|
|
message?.providerTrace?.route === CODE_AGENT_M3_IO_ROUTE ||
|
|
message?.runnerTrace?.route === CODE_AGENT_M3_IO_ROUTE;
|
|
}
|
|
|
|
function firstNonEmpty(...values) {
|
|
for (const value of values) {
|
|
const text = stringOrNull(value);
|
|
if (text) return text;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function firstDefined(...values) {
|
|
return values.find((value) => value !== undefined && value !== null);
|
|
}
|
|
|
|
function normalizeBoolean(value) {
|
|
if (typeof value === "boolean") return value;
|
|
if (typeof value === "number") return value === 1 ? true : value === 0 ? false : null;
|
|
const text = stringOrNull(value)?.toLowerCase();
|
|
if (text === "true") return true;
|
|
if (text === "false") return false;
|
|
return null;
|
|
}
|
|
|
|
function objectOrNull(value) {
|
|
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
}
|
|
|
|
function stringOrNull(value) {
|
|
const text = String(value ?? "").trim();
|
|
return text ? text : null;
|
|
}
|
|
|
|
function formatValue(value) {
|
|
return value === undefined || value === null ? missingProofLabel : String(value);
|
|
}
|
|
|
|
function booleanStateLabel(value) {
|
|
if (value === true) return "true";
|
|
if (value === false) return "false";
|
|
return "unknown";
|
|
}
|