refactor: consolidate coercion contracts (#122458)

* refactor: consolidate coercion contracts

Centralize exact string, record, numeric, date, Boolean, argument, and structured-error coercions while preserving call-site semantics.

Migrate canonical-name collisions and deprecated internal SDK bypasses, deleting 55 net production/tooling lines. Expand declaration ownership enforcement to 101 allowed helpers and add a narrow export-completeness audit.

* fix: preserve standalone script coercions

Keep copied Control UI tooling self-contained and retain the trusted release harness module-relative source seam when the harness runs against an old target cwd.
This commit is contained in:
Peter Steinberger
2026-08-11 23:26:37 -07:00
committed by GitHub
parent 66fe424590
commit b080dd1e76
276 changed files with 1685 additions and 1663 deletions
+2 -1
View File
@@ -1,4 +1,5 @@
// Control UI module owns transient operator question state.
import { asSafeIntegerInRange } from "@openclaw/normalization-core/number-coercion";
import { isRecord } from "@openclaw/normalization-core/record-coerce";
import { normalizeNullableString as readNonEmptyString } from "@openclaw/normalization-core/string-coerce";
import type {
@@ -61,7 +62,7 @@ type QuestionAnswerValues = Record<string, string[]>;
const REFRESH_RETRY_DELAYS_MS = [1_000, 2_000, 4_000] as const;
function readTimestamp(value: unknown): number | null {
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : null;
return asSafeIntegerInRange(value, { min: 0 }) ?? null;
}
const MAX_HEADER_GRAPHEMES = 12;
+7 -10
View File
@@ -4,6 +4,7 @@
// that is dispatched against the browser plugin's control routes, either
// locally or via a browser-capable node. This module narrows the handful of
// routes the browser panel needs and keeps route-path knowledge in one place.
import { asFiniteNumber } from "@openclaw/normalization-core/number-coercion";
import { asNullableRecord as asRecord } from "@openclaw/normalization-core/record-coerce";
import { readStringValue } from "@openclaw/normalization-core/string-coerce";
import type { GatewayBrowserClient } from "../../api/gateway.ts";
@@ -71,10 +72,6 @@ function stringOrEmpty(value: unknown): string {
return readStringValue(value) ?? "";
}
function asNullableFiniteNumber(value: unknown): number | null {
return typeof value === "number" && Number.isFinite(value) ? value : null;
}
function normalizeTab(value: unknown): BrowserPanelTab | null {
const record = asRecord(value);
const targetId = stringOrEmpty(record?.targetId);
@@ -236,8 +233,8 @@ export async function readBrowserPageMetrics(
fn: "() => ({ cssWidth: window.innerWidth, cssHeight: window.innerHeight, title: document.title, url: location.href })",
}),
);
const cssWidth = asNullableFiniteNumber(result?.cssWidth);
const cssHeight = asNullableFiniteNumber(result?.cssHeight);
const cssWidth = asFiniteNumber(result?.cssWidth);
const cssHeight = asFiniteNumber(result?.cssHeight);
if (!cssWidth || !cssHeight || cssWidth <= 0 || cssHeight <= 0) {
return null;
}
@@ -293,10 +290,10 @@ export async function inspectBrowserElementAt(
role: stringOrEmpty(result.role),
name: stringOrEmpty(result.name),
rect: {
x: asNullableFiniteNumber(rect?.x) ?? 0,
y: asNullableFiniteNumber(rect?.y) ?? 0,
width: asNullableFiniteNumber(rect?.width) ?? 0,
height: asNullableFiniteNumber(rect?.height) ?? 0,
x: asFiniteNumber(rect?.x) ?? 0,
y: asFiniteNumber(rect?.y) ?? 0,
width: asFiniteNumber(rect?.width) ?? 0,
height: asFiniteNumber(rect?.height) ?? 0,
},
focusable: result.focusable === true,
};
+3 -3
View File
@@ -73,10 +73,10 @@ function isAnySchema(schema: JsonSchema): boolean {
function normalizeEnum(values: unknown[]): { enumValues: unknown[]; nullable: boolean } {
const filtered = values.filter((value) => value != null);
const nullable = filtered.length !== values.length;
return { enumValues: uniqueValues(filtered), nullable };
return { enumValues: uniqueSchemaValues(filtered), nullable };
}
function uniqueValues(values: unknown[]): unknown[] {
function uniqueSchemaValues(values: unknown[]): unknown[] {
const unique: unknown[] = [];
for (const value of values) {
if (!unique.some((existing) => Object.is(existing, value))) {
@@ -591,7 +591,7 @@ function normalizeUnion(
return {
schema: {
...schema,
enum: uniqueValues(literals),
enum: uniqueSchemaValues(literals),
nullable,
enumIncludesNull: nullable,
anyOf: undefined,
+5 -9
View File
@@ -1,4 +1,5 @@
import { initialState, Task } from "@lit/task";
import { asFiniteNumber } from "@openclaw/normalization-core/number-coercion";
import { isRecord } from "@openclaw/normalization-core/record-coerce";
import { ReactiveElement } from "lit";
import type { ControlUiGitHubPreview } from "../../../src/gateway/control-ui-contract.js";
@@ -49,11 +50,6 @@ function readOptionalGitHubString(
return typeof value === "string" && value.trim() ? value : undefined;
}
function optionalNumber(record: Record<string, unknown>, key: string): number | undefined {
const value = record[key];
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
}
function parseGitHubIssueOrPullRequestLink(href: string): GitHubLinkTarget | null {
let url: URL;
try {
@@ -97,13 +93,13 @@ function parsePreviewResponse(target: GitHubLinkTarget, value: unknown): GitHubP
}
return {
...target,
additions: optionalNumber(value, "additions"),
additions: asFiniteNumber(value.additions),
avatarDataUrl: safeAvatarDataUrl(value.avatarDataUrl),
changedFiles: optionalNumber(value, "changedFiles"),
changedFiles: asFiniteNumber(value.changedFiles),
closedAt: readOptionalGitHubString(value, "closedAt"),
comments: optionalNumber(value, "comments"),
comments: asFiniteNumber(value.comments),
createdAt: requiredString(value, "createdAt"),
deletions: optionalNumber(value, "deletions"),
deletions: asFiniteNumber(value.deletions),
draft: typeof value.draft === "boolean" ? value.draft : undefined,
kind: target.kind,
login: readOptionalGitHubString(value, "login") ?? "ghost",
+2 -3
View File
@@ -1,3 +1,4 @@
import { asFiniteNumber } from "@openclaw/normalization-core/number-coercion";
import { isRecord } from "@openclaw/normalization-core/record-coerce";
import { GatewayRequestError, type GatewayBrowserClient } from "../../api/gateway.ts";
import type { GatewaySessionRow } from "../../api/types.ts";
@@ -50,9 +51,7 @@ export function taskLifecycleSourceUpdatedAt(task: WorkboardTaskSummary): number
}
export function sessionUpdatedAtValue(session: GatewaySessionRow): number | undefined {
return typeof session.updatedAt === "number" && Number.isFinite(session.updatedAt)
? session.updatedAt
: undefined;
return asFiniteNumber(session.updatedAt);
}
export function taskSessionKeyMatchesCardSession(
+2 -2
View File
@@ -1,4 +1,5 @@
import { readSessionMessageIdentity } from "@openclaw/gateway-client/browser";
import { asFiniteNumber } from "@openclaw/normalization-core/number-coercion";
import { asNullableRecord as asRecord } from "@openclaw/normalization-core/record-coerce";
import { resolveToolUseId } from "../../../../src/chat/tool-content.js";
import { escapeRegExp } from "../../../../src/shared/regexp.js";
@@ -574,8 +575,7 @@ export function queuedSendThreadMessage(item: ChatQueueItem): Record<string, unk
}
export function rawMessageTimestamp(message: unknown): number | null {
const timestamp = asRecord(message)?.timestamp;
return typeof timestamp === "number" && Number.isFinite(timestamp) ? timestamp : null;
return asFiniteNumber(asRecord(message)?.timestamp) ?? null;
}
function chatItemTimestamp(item: ChatItem): number | null {
@@ -1,3 +1,4 @@
import { asNonNegativeFiniteNumber } from "@openclaw/normalization-core/number-coercion";
import { asNullableObjectRecord as readCostRecord } from "@openclaw/normalization-core/record-coerce";
import { html, nothing } from "lit";
import { isTranscriptOnlyOpenClawAssistantMessage } from "../../../../../src/shared/transcript-only-openclaw-assistant.js";
@@ -39,8 +40,7 @@ function readCostValue(
cost: Record<string, unknown> | null,
key: "input" | "output" | "cacheRead" | "cacheWrite",
) {
const value = cost?.[key];
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : undefined;
return asNonNegativeFiniteNumber(cost?.[key]);
}
function latestProviderCostStats(messages: unknown[] | undefined): ProviderCostStats | null {
@@ -1,3 +1,4 @@
import { asFiniteNumber } from "@openclaw/normalization-core/number-coercion";
import type { ImageLightboxItem } from "../../../components/image-lightbox.ts";
import { t } from "../../../i18n/index.ts";
import type { MessageContentItem } from "../../../lib/chat/chat-types.ts";
@@ -486,8 +487,7 @@ export function extractImages(message: unknown): ImageBlock[] {
}
function readPairingQrExpiresAtMs(block: Record<string, unknown>): number | undefined {
const expiresAtMs = block.expiresAtMs;
return typeof expiresAtMs === "number" && Number.isFinite(expiresAtMs) ? expiresAtMs : undefined;
return asFiniteNumber(block.expiresAtMs);
}
function isExpiredPairingQrBlock(block: Record<string, unknown>, nowMs = Date.now()): boolean {
+3 -6
View File
@@ -1,4 +1,5 @@
// Control UI chat module implements stream reconciliation behavior.
import { asFiniteNumber } from "@openclaw/normalization-core/number-coercion";
import {
streamSegmentHasItemId,
streamSegmentUsesAccumulatedText,
@@ -505,12 +506,8 @@ function messageTimestampMs(message: unknown): number | null {
if (!message || typeof message !== "object") {
return null;
}
const timestamp = (message as { timestamp?: unknown; ts?: unknown }).timestamp;
if (typeof timestamp === "number" && Number.isFinite(timestamp)) {
return timestamp;
}
const ts = (message as { timestamp?: unknown; ts?: unknown }).ts;
return typeof ts === "number" && Number.isFinite(ts) ? ts : null;
const record = message as { timestamp?: unknown; ts?: unknown };
return asFiniteNumber(record.timestamp) ?? asFiniteNumber(record.ts) ?? null;
}
function timestampForInsertedVisibleStream(