refactor(domain): one owner for the non-array record predicate (#10588)

* refactor(domain): one owner for the non-array record predicate

Thirty-four source files each declared isRecord as a non-null object that is
not an array, and six more spelled the same predicate as isObject,
isSchemaRecord or isStorageRecord. They now import isRecord from
src/domain/isRecord.ts, whose overloads keep JsonValue narrowing for the
desktop and environment readers. DshCordisPatch's predicate also excludes
Cordis expressions, so it is renamed isPatchRecord.

architecture-guards refuses a new local copy under any name. The seven files
that still hold one are listed and must leave the list when their copy goes.

* fix(guards): count record predicate copies precisely

* test(hopper): retry after confirmed application exit

* fix: reject stale record predicate exceptions
This commit is contained in:
morluto authored and GitHub committed 2026-11-15 17:05:54 +08:00
1 parent d5e0747025
commit 5bf66f7500
49 files changed
+168 -205

No files matched your search

+38
View File
@@ -3,6 +3,7 @@ import { existsSync } from "node:fs";
import { readFile, readdir, writeFile } from "node:fs/promises";
import { join, relative, sep } from "node:path";
import { fileURLToPath } from "node:url";
import { recordPredicateCounts } from "./record-predicate-guard.mjs";
import {
compareCountedBaseline,
recordLoweredBaseline,
@@ -167,6 +168,43 @@ check(
],
);
// "Non-null object other than an array" has one owner: src/domain/isRecord.ts.
// A local predicate with that body, under any name, is a copy. The listed files
// still hold one and may only be removed from the list; a listed file that no
// longer matches fails so the list shrinks with the code.
const RECORD_PREDICATE_COPIES = [
// Exported under a module-specific name; importers still use that name.
"src/browser/history/CaptureRedaction.ts",
"src/contracts/compactJsonSchema.ts",
"src/domain/analysisView/environmentStateRules.ts",
// Owned by the advertised-schema workstream.
"src/contracts/inputSchemaPresentation.ts",
// Narrows a protocol-specific parameter type, not unknown.
"src/browser/execution/CdpRuntimeInitiator.ts",
// Named `object`, which also spells the typeof result in these files.
"src/pcap/PcapDnsWire.ts",
"src/pcap/PcapOutput.ts",
];
const mismatchedRecordCopies = files.filter(({ path, content }) => {
if (path === "src/domain/isRecord.ts") return false;
const { named, bodies } = recordPredicateCounts(content);
return (
named !== 0 || bodies !== (RECORD_PREDICATE_COPIES.includes(path) ? 1 : 0)
);
});
const pathsWithRecordCopies = new Set(files.map(({ path }) => path));
const missingRecordCopies = RECORD_PREDICATE_COPIES.filter(
(path) => !pathsWithRecordCopies.has(path),
);
if (mismatchedRecordCopies.length > 0)
failures.push(
`local non-array record predicate count changed (import isRecord and update the baseline):\n${mismatchedRecordCopies.map(({ path }) => ` ${path}`).join("\n")}`,
);
if (missingRecordCopies.length > 0)
failures.push(
`record predicate baseline references missing files; remove those entries:\n${missingRecordCopies.map((path) => ` ${path}`).join("\n")}`,
);
// Node clamps a timer delay above the signed 32-bit bound to 1 ms. Durations
// use MAX_NODE_TIMER_DELAY_MS or timerDurationMsSchema from their one owner.
// The allowed files bound signed 32-bit integers that are not timer delays.
@@ -0,0 +1,4 @@
export function recordPredicateCounts(content: string): {
named: number;
bodies: number;
};
+13
View File
@@ -0,0 +1,13 @@
/** Count declarations named isRecord and declarations with the shared predicate. */
export const recordPredicateCounts = (content) => {
const arrow =
/\bconst\s+[\w$]+\s*=\s*\(\s*([\w$]+)[^)]*\)\s*(?::[^=]+)?=>\s*typeof\s+\1\s*===\s*["']object["']\s*&&\s*\1\s*!==\s*null\s*&&\s*!Array\.isArray\(\1\)\s*(?=;|\})/gu;
const declaration =
/\bfunction\s+[\w$]+\s*\(\s*([\w$]+)[^)]*\)\s*(?::[^{]+)?\{\s*return\s+typeof\s+\1\s*===\s*["']object["']\s*&&\s*\1\s*!==\s*null\s*&&\s*!Array\.isArray\(\1\)\s*(?=;|\})/gu;
return {
named: [...content.matchAll(/\b(?:const|function)\s+isRecord\b/gu)].length,
bodies:
[...content.matchAll(arrow)].length +
[...content.matchAll(declaration)].length,
};
};
+6 -7
View File
@@ -14,6 +14,7 @@ import type { ArtifactResourceScope } from "../artifacts/ArtifactResourceScope.j
import type { ConfigurationError } from "../domain/configurationErrors.js";
import { mergeEnvironmentOverrides } from "../process/snapshotEnvironment.js";
import { PRODUCT_IDENTITY } from "../identity.js";
import { isRecord } from "../domain/isRecord.js";
/** A parsed layer retains its path so rereading the write target replaces only that layer. */
export interface ClientConfigurationSource extends ClientConfigurationDocument {
@@ -167,7 +168,7 @@ export const readClientConfigurationFiles = async (
/** OpenCode merges server objects in file order, replacing arrays and scalar values. */
const mergeConfigurationValue = (previous: unknown, next: unknown): unknown => {
if (!isObject(previous) || !isObject(next)) return next;
if (!isRecord(previous) || !isRecord(next)) return next;
return Object.fromEntries(
[...new Set([...Object.keys(previous), ...Object.keys(next)])].map(
(key) => [
@@ -179,8 +180,6 @@ const mergeConfigurationValue = (previous: unknown, next: unknown): unknown => {
),
);
};
const isObject = (value: unknown): value is Record<string, unknown> =>
typeof value === "object" && value !== null && !Array.isArray(value);
/** Native OpenCode dialect applies across all selected configuration layers. */
export const clientWriteDialect = (
@@ -260,12 +259,12 @@ export const mergeClientConfigurationDocuments = (
const key = document.environmentKey;
if (
name === PRODUCT_IDENTITY.mcpServerKey &&
isObject(merged) &&
isObject(merged[key])
isRecord(merged) &&
isRecord(merged[key])
) {
const before =
isObject(previous) && isObject(previous[key]) ? previous[key] : {};
const after = isObject(next) && isObject(next[key]) ? next[key] : {};
isRecord(previous) && isRecord(previous[key]) ? previous[key] : {};
const after = isRecord(next) && isRecord(next[key]) ? next[key] : {};
servers[name] = {
...merged,
[key]: mergeEnvironmentOverrides(before, after, platform),
+7 -6
View File
@@ -11,7 +11,7 @@ export const DSH_ENTRY_ID = "rea-mcp";
export const DSH_CLIENT_PACKAGE = "@deepseek-ai/dsh-mcp-client";
const LAUNCHER_KEYS = new Set(["serverName", "transport", "command", "args"]);
const SELECTOR_KEYS = new Set(["id", "name"]);
const isRecord = (value: unknown): value is Record<string, unknown> =>
const isPatchRecord = (value: unknown): value is Record<string, unknown> =>
typeof value === "object" &&
value !== null &&
!Array.isArray(value) &&
@@ -133,7 +133,8 @@ export class DshCordisDocument {
: undefined;
const config = this.#field("config");
// Ownership survives malformed config so uninstall can remove the source row.
this.config = insertions === 0 ? undefined : isRecord(config) ? config : {};
this.config =
insertions === 0 ? undefined : isPatchRecord(config) ? config : {};
}
#map(node: unknown): YAMLMap<unknown, unknown> | undefined {
@@ -209,7 +210,7 @@ export class DshCordisDocument {
},
),
);
if (config !== undefined && !isRecord(config))
if (config !== undefined && !isPatchRecord(config))
return err(
new ConfigurationError("Cordis REA config must be a mapping", {
settings: [
@@ -221,7 +222,7 @@ export class DshCordisDocument {
}),
);
const raw = this.config?.env;
if (raw !== undefined && !isRecord(raw))
if (raw !== undefined && !isPatchRecord(raw))
return err(
new ConfigurationError("Cordis server environment must be a mapping"),
);
@@ -268,7 +269,7 @@ export class DshCordisDocument {
const environment = this.environment(overrides);
if (!environment.ok) return environment;
const env = mergeEnvironmentOverrides<unknown>(
isRecord(this.config?.env) ? this.config.env : {},
isPatchRecord(this.config?.env) ? this.config.env : {},
overrides,
);
const desired = { ...this.config, ...dshClientRegistrationEntry(command) };
@@ -378,7 +379,7 @@ export class DshCordisDocument {
this.#yaml.setValue(config, key, desired[key]);
const env = this.#map(this.#yaml.field(sourceConfig, "env"));
const desiredEnv = desired.env;
if (isRecord(desiredEnv)) {
if (isPatchRecord(desiredEnv)) {
const nextEnv =
env === undefined
? this.#yaml.document.createNode({})
@@ -78,6 +78,7 @@ import {
type TaskSummary,
} from "./DesktopTaskExport.js";
import { taskReads } from "./DesktopExportReaders.js";
import { isRecord } from "../../domain/isRecord.js";
const OPERATION = "export_desktop_application";
/** Largest seed file the export copies; the state view reads only 64 MiB. */
@@ -136,11 +137,6 @@ export interface DesktopExportResult {
readonly consented_files: readonly string[];
}
const isRecord = (
value: JsonValue | null | undefined,
): value is Record<string, JsonValue> =>
typeof value === "object" && value !== null && !Array.isArray(value);
/** The application's dconf groups at the seed, as a dconf key file with the identity scrubbed from its strings. */
const seedDconf = (
state: DesktopApplicationState,
@@ -23,6 +23,7 @@ import {
translateValue,
type IdMap,
} from "./environmentCompareIds.js";
import { isRecord } from "../../domain/isRecord.js";
export const DIFFERENCE_CATEGORIES = [
"status",
@@ -67,9 +68,6 @@ const TIME_FIELD_CAMEL =
/(?:At|Time|Date|Timestamp|Created|Updated|Modified)$/u;
const ID_LIST_FIELD = /^(ids|uuids|guids)$/u;
const isObject = (value: JsonValue | undefined): value is JsonObject =>
typeof value === "object" && value !== null && !Array.isArray(value);
const shown = (value: JsonValue | undefined): string => {
const text = value === undefined ? "<absent>" : JSON.stringify(value);
return text.length > VALUE_CHARS ? `${text.slice(0, VALUE_CHARS)}…` : text;
@@ -517,7 +515,7 @@ class Differ {
): void {
if (Array.isArray(live) && Array.isArray(env))
this.array(live, env, pointer, at.clockKey ?? at.key, generation);
else if (isObject(live) && isObject(env))
else if (isRecord(live) && isRecord(env))
this.object(
live,
env,
@@ -528,8 +526,8 @@ class Differ {
else if (
Array.isArray(live) ||
Array.isArray(env) ||
isObject(live) ||
isObject(env)
isRecord(live) ||
isRecord(env)
)
this.add({
category: "value",
@@ -4,13 +4,11 @@ import type { BodyAddress } from "../../domain/analysisView/environmentStateWrit
import { isUuid } from "../../domain/analysisView/environmentStateWriteValues.js";
import type { JsonValue } from "../../domain/jsonValue.js";
import type { ProbeToolCall } from "./environmentCompareMatch.js";
import { isRecord } from "../../domain/isRecord.js";
/** Field names that hold an identifier: `id`, `uuid`, `guid`, or names ending in `id` or `_id`. */
export const ID_FIELD = /^(id|uuid|guid)$|(_id|Id|ID|_uuid|Uuid)$/u;
const isObject = (value: JsonValue | undefined): value is JsonObject =>
typeof value === "object" && value !== null && !Array.isArray(value);
const isScalarId = (value: JsonValue | undefined): value is string | number =>
(typeof value === "string" && value !== "") ||
(typeof value === "number" && Number.isFinite(value));
@@ -179,7 +177,7 @@ const pairing = (
),
by,
});
if (!live.every(isObject) || !env.every(isObject))
if (!live.every(isRecord) || !env.every(isRecord))
return byPosition("position");
const liveItems = live as readonly JsonObject[];
const envItems = env as readonly JsonObject[];
@@ -218,7 +216,7 @@ const keyedByIds = (
([key, value]) =>
/^\d+$/u.test(key) ||
isUuid(key) ||
(isObject(value) &&
(isRecord(value) &&
Object.entries(value).some(
([field, held]) =>
IDENTITY_FIELD.test(field) &&
@@ -237,7 +235,7 @@ export const keyedByLearnedIds = (
if (!entries.some(([key]) => map.hasLive(key))) return false;
return entries.some(([key]) => Object.hasOwn(env, key))
? keyedByIds(entries)
: entries.every(([, value]) => isObject(value)) || keyedByIds(entries);
: entries.every(([, value]) => isRecord(value)) || keyedByIds(entries);
};
/**
@@ -258,8 +256,8 @@ const learnKeyedEntries = (
liveEntries.length === 0 ||
envEntries.length === 0 ||
shared === liveEntries.length ||
!liveEntries.every(([, value]) => isObject(value)) ||
!envEntries.every(([, value]) => isObject(value))
!liveEntries.every(([, value]) => isRecord(value)) ||
!envEntries.every(([, value]) => isRecord(value))
)
return false;
const paired = pairing(
@@ -340,7 +338,7 @@ const learnAt = (
});
return;
}
if (!isObject(live) || !isObject(env)) {
if (!isRecord(live) || !isRecord(env)) {
if (
context.identity &&
place.at === context.entityAt &&
@@ -423,7 +421,7 @@ const inEnvTerms = (
export const translateValue = (value: JsonValue, map: IdMap): JsonValue => {
if (Array.isArray(value))
return value.map((item) => translateValue(item, map));
if (isObject(value))
if (isRecord(value))
return Object.fromEntries(
Object.entries(value).map(([key, item]) => [
key,
@@ -455,7 +453,7 @@ const translateBulkBody = (
? value.map((entry) =>
address.by === "ids"
? id(entry)
: isObject(entry) && Object.hasOwn(entry, idField)
: isRecord(entry) && Object.hasOwn(entry, idField)
? {
...(translateValue(entry, map) as JsonObject),
[idField]: id(entry[idField]!),
@@ -464,7 +462,7 @@ const translateBulkBody = (
)
: translateValue(value, map);
if (address.member === null) return entries(body);
return isObject(body)
return isRecord(body)
? Object.fromEntries(
Object.entries(body).map(([key, value]) => [
key,
+1 -3
View File
@@ -4,12 +4,10 @@ import type { BrowserObservationOperation } from "../domain/browserObservationEr
import { sanitizeBrowserUrl } from "../domain/browserObservation.js";
import { createWebTextArtifact } from "../domain/webContentArtifact.js";
import { err, ok, type Result } from "../domain/result.js";
import { isRecord } from "../domain/isRecord.js";
export type UnknownRecord = Readonly<Record<string, unknown>>;
const isRecord = (value: unknown): value is UnknownRecord =>
typeof value === "object" && value !== null && !Array.isArray(value);
export const recordValue = (value: unknown): UnknownRecord | undefined =>
isRecord(value) ? value : undefined;
+1 -3
View File
@@ -23,6 +23,7 @@ import {
} from "../domain/browserDeadline.js";
import { safeParseJson } from "../domain/safeJson.js";
import { WEB_RUNTIME_LIMITS } from "../domain/webRuntime.js";
import { isRecord } from "../domain/isRecord.js";
// ws currently stores maxPayload as a signed 32-bit integer. Larger values can
// wrap to a negative number, which disables its limit check.
@@ -625,9 +626,6 @@ const rawBytes = (data: RawData): Buffer =>
? data
: Buffer.from(data);
const isRecord = (value: unknown): value is Record<string, unknown> =>
typeof value === "object" && value !== null && !Array.isArray(value);
const waitForOpen = async (
socket: WebSocket,
operation: BrowserObservationOperation,
+1 -3
View File
@@ -31,6 +31,7 @@ import {
DEFAULT_ASAR_HEADER_ADMISSION,
type AsarHeaderAdmission,
} from "../config/asarHeaderAdmission.js";
import { isRecord } from "../domain/isRecord.js";
type ElectronFileOperation =
| "inspect_electron_page"
@@ -374,9 +375,6 @@ const isAsarRegularFile = (
);
};
const isRecord = (value: unknown): value is Readonly<Record<string, unknown>> =>
typeof value === "object" && value !== null && !Array.isArray(value);
const throwIfAborted = (signal?: AbortSignal): void => {
assertArtifactReadActive(signal);
};
+1 -3
View File
@@ -1,4 +1,5 @@
import { z } from "zod";
import { isRecord } from "../../domain/isRecord.js";
import {
observeCdpFrameUrl,
@@ -59,9 +60,6 @@ interface MutableNode extends CdpFrameTreeNode {
readonly children: CdpFrameTreeNode[];
}
const isRecord = (value: unknown): value is UnknownRecord =>
recordValue(value) !== undefined;
const treeRecord = z.custom<UnknownRecord>(isRecord, {
message: "Expected a frame tree object.",
});
+1 -3
View File
@@ -4,6 +4,7 @@ import { err, ok, type Result } from "../domain/result.js";
import { AnalysisInputError } from "../domain/analysisErrorCore.js";
import { jsonParts } from "../domain/jsonSerialization.js";
import { isRecord } from "../domain/isRecord.js";
/** Reserved control-record marker, not an advertised external JSON Schema. */
export const JSON_LINES_PROTOCOL = "jsonl/v1";
@@ -56,9 +57,6 @@ export interface JsonLinesPlan extends Collection {
readonly filter: string | undefined;
}
const isRecord = (value: unknown): value is Record<string, unknown> =>
typeof value === "object" && value !== null && !Array.isArray(value);
const atPath = (value: unknown, path: readonly string[]): unknown => {
for (const key of path) {
if (!isRecord(value) || !Object.hasOwn(value, key)) return undefined;
+1 -3
View File
@@ -8,9 +8,7 @@ import {
type AnalysisErrorProjection,
} from "./domain/analysisErrorProjection.js";
import type { Logger } from "pino";
const isRecord = (value: unknown): value is Readonly<Record<string, unknown>> =>
typeof value === "object" && value !== null && !Array.isArray(value);
import { isRecord } from "./domain/isRecord.js";
type CliFailureIdentity = Pick<
AnalysisErrorProjection,
+1 -3
View File
@@ -3,6 +3,7 @@ import {
schemaDepth,
serializedBytes,
} from "./compactJsonSchema.js";
import { isRecord } from "../domain/isRecord.js";
import {
schemaArrays,
schemaChildren,
@@ -335,9 +336,6 @@ export const portablePatternViolations = (
type SchemaKind = "input" | "output";
const isRecord = (value: unknown): value is Readonly<Record<string, unknown>> =>
typeof value === "object" && value !== null && !Array.isArray(value);
/** Visit every schema position; instance literals stay opaque. */
const visitSchemas = (
schema: unknown,
+6 -11
View File
@@ -15,6 +15,7 @@ import {
} from "./jsonSchemaKeywords.js";
import { digestCanonicalValue } from "../domain/canonicalDigest.js";
import { getEvidenceInputContract } from "../domain/evidenceInputContract.js";
import { isRecord } from "../domain/isRecord.js";
const PROPERTY_DESCRIPTIONS: Readonly<Record<string, string>> = {
addresses: "Ordered provider-normalized procedure addresses to analyze.",
@@ -171,9 +172,9 @@ export const transformAdvertisedInputJsonSchema = <Schema>(
transform: (projected: Record<string, unknown>) => Record<string, unknown>,
): Schema => {
const standard = Reflect.get(schema as object, "~standard");
if (!isSchemaRecord(standard)) return schema;
if (!isRecord(standard)) return schema;
const jsonSchema = Reflect.get(standard, "jsonSchema");
if (!isSchemaRecord(jsonSchema)) return schema;
if (!isRecord(jsonSchema)) return schema;
const project = Reflect.get(jsonSchema, "input");
if (typeof project !== "function") return schema;
const byTarget = new Map<string, Record<string, unknown>>();
@@ -187,11 +188,8 @@ export const transformAdvertisedInputJsonSchema = <Schema>(
if (cached !== undefined) return cached;
}
const projected = (project as (options: unknown) => unknown)(options);
const transformed = isSchemaRecord(projected)
? transform(projected)
: projected;
if (cacheable && isSchemaRecord(transformed))
byTarget.set(target, transformed);
const transformed = isRecord(projected) ? transform(projected) : projected;
if (cacheable && isRecord(transformed)) byTarget.set(target, transformed);
return transformed;
};
return {
@@ -226,7 +224,7 @@ const hasRelativeSchemaReference = (
if (current === null || typeof current !== "object") continue;
const { position } = entry;
const scope =
position !== "map" && isSchemaRecord(current)
position !== "map" && isRecord(current)
? references.enter(current, entry.scope)
: entry.scope;
if (scope === undefined) return true;
@@ -322,9 +320,6 @@ const memoizeByTarget = (
};
};
const isSchemaRecord = (value: unknown): value is Record<string, unknown> =>
typeof value === "object" && value !== null && !Array.isArray(value);
const fallbackPropertyDescription = (property: string): string | undefined => {
const explicit = Object.hasOwn(PROPERTY_DESCRIPTIONS, property)
? PROPERTY_DESCRIPTIONS[property]
@@ -1,11 +1,9 @@
import { jsonPointerTokens } from "../jsonPointer.js";
import type { JsonValue } from "../jsonValue.js";
import { isRecord } from "../isRecord.js";
type JsonObject = { [key: string]: JsonValue };
const isObject = (value: JsonValue | undefined): value is JsonObject =>
typeof value === "object" && value !== null && !Array.isArray(value);
/** A list counter's member names: a top-level member's name not starting with `/`, else a pointer (`/pageInfo/totalRows`, `/~1total`). */
export const counterPath = (counter: string): string[] =>
counter.startsWith("/") ? jsonPointerTokens(counter) : [counter];
@@ -17,7 +15,7 @@ export const counterAt = (
): JsonValue | undefined =>
counterPath(counter).reduce<JsonValue | undefined>(
(value, name) =>
isObject(value) && Object.hasOwn(value, name) ? value[name] : undefined,
isRecord(value) && Object.hasOwn(value, name) ? value[name] : undefined,
body,
);
@@ -30,7 +28,7 @@ export const withCounter = (
const set = (node: JsonValue, path: readonly string[]): JsonValue => {
const [name, ...rest] = path;
if (name === undefined) return value;
if (!isObject(node) || !Object.hasOwn(node, name)) return node;
if (!isRecord(node) || !Object.hasOwn(node, name)) return node;
return { ...node, [name]: set(node[name]!, rest) };
};
return set(body, counterPath(counter));
+2 -4
View File
@@ -8,6 +8,7 @@ import {
type BrowserScenarioNormalization,
type BrowserScenarioNormalizationRule,
} from "./browserScenarioDiffValues.js";
import { isRecord } from "./isRecord.js";
/** Canonically order and commit a parsed browser scenario normalization policy. */
export const commitBrowserScenarioNormalization = (
@@ -78,7 +79,7 @@ export const normalizeScenarioValue = (
/** Compare parsed storage records by their fixed fields without serializing them. */
const compareStorageEntries = (left: unknown, right: unknown): number => {
if (!isStorageRecord(left) || !isStorageRecord(right))
if (!isRecord(left) || !isRecord(right))
return compareUnicodeCodePoints(
digestCanonicalValue(left, "Browser scenario"),
digestCanonicalValue(right, "Browser scenario"),
@@ -103,9 +104,6 @@ const compareStorageEntries = (left: unknown, right: unknown): number => {
return 0;
};
const isStorageRecord = (value: unknown): value is Record<string, unknown> =>
typeof value === "object" && value !== null && !Array.isArray(value);
const compareStorageField = (left: unknown, right: unknown): number => {
if (left === right) return 0;
if (left === null) return -1;
+1 -5
View File
@@ -22,6 +22,7 @@ import {
zipEntryAt,
zipLeavesOutAt,
} from "./zipView.js";
import { isRecord } from "../isRecord.js";
/** One file as the app's data/get reports it: its view, or why it has none. An absent file has no entry. */
export interface TaskFileView {
@@ -83,11 +84,6 @@ const undecided = (file: string): Verdict =>
const notFound = (description: string, cut: boolean): Verdict =>
cut ? unread(description) : fail(description);
const isRecord = (
value: JsonValue | null | undefined,
): value is Record<string, JsonValue> =>
typeof value === "object" && value !== null && !Array.isArray(value);
/** JSON equality: numbers by value, objects by their keys, arrays in order. */
export const sameJson = (
actual: JsonValue | undefined,
+1 -5
View File
@@ -35,6 +35,7 @@ import {
import { diffNamedMembers, diffStateViews } from "./stateChanges.js";
import { DRAFT_VIEW_CHARS } from "./valueCuts.js";
import { resolvePointer } from "./viewPointer.js";
import { isRecord } from "../isRecord.js";
type Version = DesktopStatePath["versions"][number];
type Suggestion = DesktopTaskCheckInput;
@@ -52,11 +53,6 @@ const versions = (baseline: Version, final: Version) => ({
final: final.view,
});
const isRecord = (
value: JsonValue | null | undefined,
): value is Record<string, JsonValue> =>
typeof value === "object" && value !== null && !Array.isArray(value);
/** A JSON value cut to a size an agent can read, and whether anything was cut. */
const bounded = (
value: JsonValue | null,
+1 -5
View File
@@ -22,6 +22,7 @@ import {
import { pairMembers } from "./memberPairing.js";
import { elementText, isXmlElement, resolvePointer } from "./viewPointer.js";
import { selectorOf } from "./viewSelector.js";
import { isRecord } from "../isRecord.js";
type Suggestion = DesktopTaskCheckInput;
@@ -68,11 +69,6 @@ type Element = Extract<JsonValue, Record<string, JsonValue>> & {
readonly children: JsonValue[];
};
const isRecord = (
value: JsonValue | undefined,
): value is Record<string, JsonValue> =>
typeof value === "object" && value !== null && !Array.isArray(value);
const attributesOf = (element: Element) =>
isRecord(element.attributes) ? element.attributes : {};
+1 -5
View File
@@ -31,6 +31,7 @@ const locatePointer = (view: JsonValue | null, pointer: string) =>
const resolvePointer = (view: JsonValue | null, pointer: string) =>
resolveView(view, pointer, "json");
import { selectorOf, spellCondition } from "./viewSelector.js";
import { isRecord } from "../isRecord.js";
type Suggestion = DesktopTaskCheckInput;
@@ -182,11 +183,6 @@ export const ownNamesUnreadOtherwise = (
return kept;
};
const isRecord = (
value: JsonValue | undefined,
): value is Record<string, JsonValue> =>
typeof value === "object" && value !== null && !Array.isArray(value);
/** The array a member pointer sits in, when its parent is one in a view. The empty pointer is the view itself, as a file whose top level is an array. */
const arrayOf = (view: JsonValue | null, pointer: string) => {
const list = pointer.replace(/\/[^/]+$/u, "");
+1 -5
View File
@@ -49,14 +49,10 @@ import {
valueFormat,
type CheckTree,
} from "./appTaskDraftTree.js";
import { isRecord } from "../isRecord.js";
type Suggestion = DesktopTaskCheckInput;
const isRecord = (
value: JsonValue | null | undefined,
): value is Record<string, JsonValue> =>
typeof value === "object" && value !== null && !Array.isArray(value);
/** Pointers without a listed descendant: a container listed with its members is suggested through them. */
export const innermost = (pointers: readonly string[]): readonly string[] =>
pointers.filter(
+1 -5
View File
@@ -13,6 +13,7 @@ import { idReferencesOf, mirrorsOf } from "./appTaskDraftMirrors.js";
import { isPositionalKey, positionalKeys } from "./positionalKeys.js";
import type { DesktopStateDifference } from "./stateChanges.js";
import { elementText, isXmlElement, resolvePointer } from "./viewPointer.js";
import { isRecord } from "../isRecord.js";
/** Why a change is what an app writes as it saves or shows a document, rather than what its user made. */
export const NOISE_REASONS = [
@@ -138,11 +139,6 @@ type Element = Record<string, JsonValue> & {
children: JsonValue[];
};
const isRecord = (
value: JsonValue | undefined,
): value is Record<string, JsonValue> =>
typeof value === "object" && value !== null && !Array.isArray(value);
/** The elements a pointer steps into through `children`, read from the view, outermost first. */
const elementsAlong = (
view: JsonValue | null,
+1 -5
View File
@@ -4,15 +4,11 @@ import { countTaskRows } from "./appTaskChecks.js";
import type { DesktopStatePath } from "./desktopState.js";
import { sqliteRowIdentity } from "./stateChanges.js";
import { appendAll } from "../appendAll.js";
import { isRecord } from "../isRecord.js";
type Version = DesktopStatePath["versions"][number];
type Suggestion = DesktopTaskCheckInput;
const isRecord = (
value: JsonValue | null | undefined,
): value is Record<string, JsonValue> =>
typeof value === "object" && value !== null && !Array.isArray(value);
interface TableView {
readonly columns: readonly string[];
readonly key: readonly string[];
+1 -5
View File
@@ -12,6 +12,7 @@ import { valueEqualsCheck } from "./appTaskDraftScalars.js";
import { rankAttributes } from "./attributeRank.js";
import type { DesktopStateDifference } from "./stateChanges.js";
import { elementText, isXmlElement, resolvePointer } from "./viewPointer.js";
import { isRecord } from "../isRecord.js";
/** The pointers a diff lists, of every kind. */
type Listed = Pick<DesktopStateDifference, "added" | "changed" | "removed">;
@@ -79,11 +80,6 @@ const parentBelowRoot = (pointer: string): string | undefined => {
const namedByAttribute = (pointer: string) =>
(pointer.split("/").at(-1) ?? "").includes("[@");
const isRecord = (
value: JsonValue | undefined,
): value is Record<string, JsonValue> =>
typeof value === "object" && value !== null && !Array.isArray(value);
/** An element's attributes; none for any other value. */
const attributesOf = (value: JsonValue | undefined) =>
isXmlElement(value) && isRecord(value.attributes) ? value.attributes : {};
+1 -5
View File
@@ -10,11 +10,7 @@ import {
zipLeavesOutAt,
zipView,
} from "./zipView.js";
const isRecord = (
value: JsonValue | null | undefined,
): value is Record<string, JsonValue> =>
typeof value === "object" && value !== null && !Array.isArray(value);
import { isRecord } from "../isRecord.js";
/**
* The entry a pointer into a zip view goes into, when it has a view and both
+1 -5
View File
@@ -16,6 +16,7 @@ import { holdsFields, isRecordAt } from "./appTaskDraftText.js";
import { attributesOf, compactValue, previewWriter } from "./compactValue.js";
import { pairMembers, pairedAtSeed } from "./memberPairing.js";
import { isXmlElement, locatePointer, resolvePointer } from "./viewPointer.js";
import { isRecord } from "../isRecord.js";
/** Why a value's pointer cannot go into a check as it is, or why the value is noise. */
export const VALUE_NOTES = ["by_position", ...NOISE_REASONS] as const;
@@ -91,11 +92,6 @@ export const VALUE_LIMITS = {
references: 5,
} as const;
const isRecord = (
value: JsonValue | undefined,
): value is Record<string, JsonValue> =>
typeof value === "object" && value !== null && !Array.isArray(value);
/** The longest value a reference is resolved from. */
const MAX_REFERENCE_CHARS = 64;
+1 -5
View File
@@ -1,10 +1,6 @@
import type { JsonValue } from "../jsonValue.js";
import { isXmlElement } from "./viewPointer.js";
const isRecord = (
value: JsonValue | undefined,
): value is Record<string, JsonValue> =>
typeof value === "object" && value !== null && !Array.isArray(value);
import { isRecord } from "../isRecord.js";
export const cut = (text: string, max: number) =>
text.length > max ? `${text.slice(0, max)}…` : text;
+1 -5
View File
@@ -6,6 +6,7 @@ import {
isPlaceholder,
} from "./attributeRank.js";
import { MAX_INSIDE_STEPS, spellCondition } from "./viewSelector.js";
import { isRecord } from "../isRecord.js";
/** A field naming a JSON record, as TreeLine's `uid`: `id`, `uid`, `uuid`, `guid`, `key`, or a name ending in `_id`, `-id` or `Id`. */
const ID_FIELD = /^(?:_?id|uid|uuid|guid|key)$|[_-]id$|[a-z]Id$/u;
@@ -23,11 +24,6 @@ const MAX_NAME = 64;
/** The most kind fields and paths a version's records are counted by, each a pass over them, so odd records cost no more. */
const MAX_TALLIES = 64;
const isRecord = (
value: JsonValue | undefined,
): value is Record<string, JsonValue> =>
typeof value === "object" && value !== null && !Array.isArray(value);
/** Whether every member holds a string at a key a selector can carry, each its own. */
const namesEach = (members: readonly JsonValue[], key: string) => {
const values = members.map((member) =>
+1 -5
View File
@@ -28,11 +28,7 @@ import {
import { pairRecordsById } from "./jsonRecordPairing.js";
import { resolvePointer } from "./viewPointer.js";
import { selectorOf } from "./viewSelector.js";
const isRecord = (
value: JsonValue | null | undefined,
): value is Record<string, JsonValue> =>
typeof value === "object" && value !== null && !Array.isArray(value);
import { isRecord } from "../isRecord.js";
const own = (record: Record<string, JsonValue>, key: string) =>
Object.hasOwn(record, key) ? record[key] : undefined;
+1 -5
View File
@@ -1,10 +1,6 @@
import type { JsonValue } from "../jsonValue.js";
import { isIdentifyingAttribute } from "./attributeRank.js";
const isRecord = (
value: JsonValue | null | undefined,
): value is Record<string, JsonValue> =>
typeof value === "object" && value !== null && !Array.isArray(value);
import { isRecord } from "../isRecord.js";
/** An identifier of the members with one tag, as `tag` NUL `key`. */
const tagKey = (tag: string, key: string) => `${tag}\u0000${key}`;
+1 -5
View File
@@ -16,6 +16,7 @@ import {
isTextualFormat,
textSyntaxFormat,
} from "./stateFormats.js";
import { isRecord } from "../isRecord.js";
/** What the export ships for one seed. */
export interface SeedScrub {
@@ -291,11 +292,6 @@ const scrubGVariant = (
return { text: scrubbed, decoded: [scrubbed, ...strings].join("\n") };
};
const isRecord = (
value: JsonValue | undefined,
): value is Record<string, JsonValue> =>
typeof value === "object" && value !== null && !Array.isArray(value);
/**
* A dconf view's groups as the key file the computer compiles, each value's
* strings scrubbed, and whether it still holds the identity, strings decoded.
+4 -6
View File
@@ -10,11 +10,7 @@ import {
rankAttributes,
} from "./attributeRank.js";
import { selectorOf, spellCondition, type Condition } from "./viewSelector.js";
const isRecord = (
value: JsonValue | null | undefined,
): value is Record<string, JsonValue> =>
typeof value === "object" && value !== null && !Array.isArray(value);
import { isRecord } from "../isRecord.js";
const own = (record: Record<string, JsonValue>, key: string) =>
Object.hasOwn(record, key) ? record[key] : undefined;
@@ -94,7 +90,9 @@ const stepTag = (tag: JsonValue | undefined): tag is string =>
/** The element children of a member, none for any other value. */
const elementsOf = (member: JsonValue): Record<string, JsonValue>[] => {
const children = isRecord(member) ? own(member, "children") : undefined;
return (Array.isArray(children) ? children : []).filter(isRecord);
return (Array.isArray(children) ? children : []).filter(
(child): child is Record<string, JsonValue> => isRecord(child),
);
};
/** A child's text a condition could name an element by, and whether the child joins it from elements of its own. */
+1 -5
View File
@@ -4,6 +4,7 @@ import type { DesktopStateFormat } from "./stateFormats.js";
import { holdsTagOnly, pairMembers } from "./memberPairing.js";
import { memberLookup } from "./viewPointer.js";
import { zipEntryAt, zipLeavesOut } from "./zipView.js";
import { isRecord } from "../isRecord.js";
const MAX_LISTED = 100;
@@ -20,11 +21,6 @@ export interface DesktopStateDifference {
readonly complete: boolean;
}
const isRecord = (
value: JsonValue | undefined,
): value is Record<string, JsonValue> =>
typeof value === "object" && value !== null && !Array.isArray(value);
/** Longest JSON pointer listed; a longer one is only counted, so deep paths never fill the lists. */
const MAX_LISTED_POINTER = 4096;
+1 -5
View File
@@ -7,6 +7,7 @@ import {
selectorOf,
type Condition,
} from "./viewSelector.js";
import { isRecord } from "../isRecord.js";
/** The arrays a pointer stepped into, each as the pointer to it. */
interface ArraySteps {
@@ -33,11 +34,6 @@ type Stepped =
const INDEX = /^(?:0|[1-9][0-9]*)$/u;
const isRecord = (
value: JsonValue | null | undefined,
): value is Record<string, JsonValue> =>
typeof value === "object" && value !== null && !Array.isArray(value);
const own = (record: Record<string, JsonValue>, key: string) =>
Object.hasOwn(record, key) ? record[key] : undefined;
+1 -5
View File
@@ -14,6 +14,7 @@ import {
desktopViewByteDisplay,
type DesktopViewPolicy,
} from "./desktopViewPolicy.js";
import { isRecord } from "../isRecord.js";
/** Why an archive has no view; the exported reader words it the same. */
export const UNREADABLE_ZIP = "not a readable zip archive";
@@ -111,11 +112,6 @@ export const zipView = (
truncated: count > entries.length,
});
const isRecord = (
value: JsonValue | null | undefined,
): value is Record<string, JsonValue> =>
typeof value === "object" && value !== null && !Array.isArray(value);
/** Whether an entry's view is missing or holds only part of it. */
const entryLeavesOut = (entry: JsonValue): boolean =>
isRecord(entry) &&
+13
View File
@@ -0,0 +1,13 @@
import type { JsonValue } from "./jsonValue.js";
/**
* Whether a value is a non-null object other than an array. Its keys,
* prototype and property values are not inspected.
*/
export function isRecord(
value: JsonValue | null | undefined,
): value is { [key: string]: JsonValue };
export function isRecord(value: unknown): value is Record<string, unknown>;
export function isRecord(value: unknown): boolean {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
+1 -3
View File
@@ -5,6 +5,7 @@ import { safeParseJson } from "./safeJson.js";
import { jsonStringByteLength } from "./jsonStringByteLength.js";
import { jsonValueByteLength } from "./jsonValueByteLength.js";
import { err, ok, type Result } from "./result.js";
import { isRecord } from "./isRecord.js";
const jsonValueTypeSchema = z.enum([
"object",
@@ -278,9 +279,6 @@ export const jsonValueType = (value: unknown): JsonValueType => {
}
};
const isRecord = (value: unknown): value is Readonly<Record<string, unknown>> =>
typeof value === "object" && value !== null && !Array.isArray(value);
/** Reverse sibling order for the traversal stack; array elements sort first. */
const orderedChildren = (node: ShapeNode): ShapeNode[] =>
[...(node.children ?? [])]
+1 -2
View File
@@ -1,6 +1,7 @@
import { types } from "node:util";
import { z } from "zod";
import { isImmutableJsonSnapshot } from "./immutableJson.js";
import { isRecord } from "./isRecord.js";
/** Maximum JSON nesting accepted before recursive result cloning. */
export const MAX_JSON_DEPTH = 1000;
@@ -14,8 +15,6 @@ export type JsonValue =
| JsonValue[]
| { [key: string]: JsonValue };
const isRecord = (value: unknown): value is Record<string, unknown> =>
typeof value === "object" && value !== null && !Array.isArray(value);
const immutableValidation = new WeakMap<object, string | undefined>();
/** Validate JSON meaning and nesting without cloning a trusted immutable value. */
+1 -3
View File
@@ -1,4 +1,5 @@
import { z } from "zod";
import { isRecord } from "./isRecord.js";
/** Artifact source declarations do not require generated-position decoding. */
type SourceMapRecord = Readonly<Record<string, unknown>>;
@@ -53,9 +54,6 @@ export const sourceMapJsonText = (text: string): string => {
return newline === -1 ? "" : withoutBom.slice(newline);
};
const isRecord = (value: unknown): value is SourceMapRecord =>
typeof value === "object" && value !== null && !Array.isArray(value);
/** Classify section ownership once; an external URL takes precedence over map. */
export const classifySourceMapSection = (
section: unknown,
+1 -3
View File
@@ -39,6 +39,7 @@ import {
} from "./mcpStartupPolicy.js";
import { MCP_DOCTOR_ALIAS, MCP_DOCTOR_OPTIONS } from "./mcpDoctorOptions.js";
import { OwnedMcpStdioTransport } from "./process/OwnedMcpStdioTransport.js";
import { isRecord } from "./domain/isRecord.js";
const OUTPUT_FORMATS = CLI_DOCUMENT_OUTPUT_FORMATS;
type OutputFormat = (typeof OUTPUT_FORMATS)[number];
@@ -672,9 +673,6 @@ const captureStderr = () => {
};
};
const isRecord = (value: unknown): value is Readonly<Record<string, unknown>> =>
typeof value === "object" && value !== null && !Array.isArray(value);
const errorMessage = (cause: unknown): string =>
cause instanceof Error ? cause.message : String(cause);
+4 -6
View File
@@ -1,4 +1,5 @@
import { setImmediate as yieldToEventLoop } from "node:timers/promises";
import { isRecord } from "../domain/isRecord.js";
import { err, ok, type Result } from "../domain/result.js";
/**
@@ -31,9 +32,6 @@ export type TsharkJsonFailure =
| { readonly kind: "json-bytes"; readonly maxBytes: number }
| { readonly kind: "json-expansion"; readonly maxDecodedBytes: number };
const object = (value: unknown): value is Record<string, unknown> =>
typeof value === "object" && value !== null && !Array.isArray(value);
const YIELD_INTERVAL = 65_536;
/**
@@ -96,9 +94,9 @@ export const parseTsharkJsonPackets = (
const packets: TsharkPacketLayers[] = [];
for (const [index, packet] of decoded.entries()) {
if (
!object(packet) ||
!object(packet._source) ||
!object(packet._source.layers)
!isRecord(packet) ||
!isRecord(packet._source) ||
!isRecord(packet._source.layers)
)
return err({
kind: "malformed-packet",
+1 -3
View File
@@ -122,9 +122,6 @@ const hasAdvertisedInputJsonSchema = (value: unknown): boolean => {
);
};
const isRecord = (value: unknown): value is Record<string, unknown> =>
typeof value === "object" && value !== null && !Array.isArray(value);
/** Recognize the SDK's argument diagnostic after its wire size is checked. */
const sdkInputValidationDiagnostic = (
text: string | undefined,
@@ -289,6 +286,7 @@ export const forgetPendingMcpRequest = (
pendingCompletions.delete(id);
};
import type { PromptCompletionSource } from "./promptCompletion.js";
import { isRecord } from "../domain/isRecord.js";
/** Standard-schema registration boundary shared with deferred SDK bindings. */
export interface StandardToolRegistration {
@@ -11,6 +11,7 @@ import {
type RpcError,
type StateIntake,
} from "./stateIntake.js";
import { isRecord } from "../../domain/isRecord.js";
/** One data-plane method's outcome; undefined from a method table means the method is not served. */
export type RpcOutcome =
@@ -24,9 +25,6 @@ export type RpcMethods = (
signal?: AbortSignal,
) => Promise<RpcOutcome | undefined>;
const isRecord = (value: unknown): value is Readonly<Record<string, unknown>> =>
typeof value === "object" && value !== null && !Array.isArray(value);
/** The first document problem as `-32602`, located by a JSON pointer into the document. */
const documentError = (error: AnalysisInputError): RpcError => {
const issue = error.issues[0];
+1 -3
View File
@@ -62,6 +62,7 @@ import {
bufferedJsonParts,
jsonParts,
} from "../../domain/jsonSerialization.js";
import { isRecord } from "../../domain/isRecord.js";
/** Where the environment card is served. */
export const ENVIRONMENT_CARD_PATH = "/.well-known/agent-env.json";
@@ -81,9 +82,6 @@ class EnvironmentMcpServer extends RequestTrackingMcpServer {
}
}
const isRecord = (value: unknown): value is Readonly<Record<string, unknown>> =>
typeof value === "object" && value !== null && !Array.isArray(value);
/** At most this many messages in one data-plane batch; a larger batch is refused whole. */
const MAX_RPC_BATCH_MESSAGES = 100;
+1 -3
View File
@@ -19,6 +19,7 @@ import {
RegularFileCleanupFailure,
withOwnedRegularFile,
} from "../../filesystem/OwnedRegularFile.js";
import { isRecord } from "../../domain/isRecord.js";
/** Why a named file or download could not be read, or a call was cancelled: a category naming neither the uri nor the content. */
export type IntakeFailure =
@@ -125,9 +126,6 @@ const fileFailure = (cause: unknown): IntakeFailure =>
? FILE_FAILURES.get(String(cause.code))
: undefined) ?? "read-failed";
const isRecord = (value: unknown): value is Readonly<Record<string, unknown>> =>
typeof value === "object" && value !== null && !Array.isArray(value);
const MAX_INLINE_BASE64_CHARACTERS =
Math.ceil(STATE_INTAKE_LIMIT_BYTES / 3) * 4;
+1 -3
View File
@@ -8,6 +8,7 @@ import {
import type { ClientFeatureAvailability } from "../contracts/toolOutputSchemaPrimitives.js";
import type { ConnectedClientIdentity } from "../serverIdentity.js";
import { isRecord } from "../domain/isRecord.js";
/** Initialize-scoped client metadata the SDK keeps for a 2025-era connection. */
export type McpConnectionClientMetadata = Pick<
@@ -69,6 +70,3 @@ const capabilityFeatures = (value: unknown): ClientFeatureAvailability => {
sampling: capabilities.sampling !== undefined,
};
};
const isRecord = (value: unknown): value is Record<string, unknown> =>
typeof value === "object" && value !== null && !Array.isArray(value);
@@ -0,0 +1,19 @@
import { expect, it } from "vitest";
import { recordPredicateCounts } from "../../scripts/verify/record-predicate-guard.mjs";
it("counts both function forms and does not depend on the parameter name", () => {
const predicate =
'typeof input === "object" && input !== null && !Array.isArray(input)';
expect(
recordPredicateCounts(`function isObject(input) { return ${predicate}; }`)
.bodies,
).toBe(1);
expect(
recordPredicateCounts(`const isObject = (input) => ${predicate};`).bodies,
).toBe(1);
expect(
recordPredicateCounts(
`const a = (input) => ${predicate}; const b = (input) => ${predicate};`,
).bodies,
).toBe(2);
});