Browse Source

refactor(codemode): split runtime into focused interpreter modules (#36540)

Aiden Cline 1 month ago
parent
commit
49cb73b348

+ 2 - 2
packages/codemode/interpreter-support.md

@@ -176,9 +176,9 @@ ultimate source of truth.
 ## Strings
 ## Strings
 
 
 - [x] Case/normalization: `toLowerCase`, `toUpperCase`, `normalize`.
 - [x] Case/normalization: `toLowerCase`, `toUpperCase`, `normalize`.
-- [x] Trimming: `trim`, `trimStart`, `trimEnd`, `trimLeft`, and `trimRight`.
+- [x] Trimming: `trim`, `trimStart`, and `trimEnd`.
 - [x] Searching/tests: `includes`, `startsWith`, `endsWith`, `indexOf`, `lastIndexOf`, and `search`.
 - [x] Searching/tests: `includes`, `startsWith`, `endsWith`, `indexOf`, `lastIndexOf`, and `search`.
-- [x] Slicing/access: `slice`, `substring`, `substr`, `at`, `charAt`, `charCodeAt`, and `codePointAt`.
+- [x] Slicing/access: `slice`, `substring`, `at`, `charAt`, `charCodeAt`, and `codePointAt`.
 - [x] Construction/transformation: `split`, `concat`, `repeat`, `padStart`, `padEnd`, `replace`, and `replaceAll`.
 - [x] Construction/transformation: `split`, `concat`, `repeat`, `padStart`, `padEnd`, `replace`, and `replaceAll`.
 - [x] Regular-expression integration: `match`, materialized `matchAll`, `replace`, `replaceAll`, `split`, and `search`.
 - [x] Regular-expression integration: `match`, materialized `matchAll`, `replace`, `replaceAll`, `split`, and `search`.
 - [x] `localeCompare`; locale and options arguments are currently ignored.
 - [x] `localeCompare`; locale and options arguments are currently ignored.

+ 5 - 6
packages/codemode/src/codemode.ts

@@ -1,5 +1,5 @@
 import { Effect, Schema } from "effect"
 import { Effect, Schema } from "effect"
-import { executeWithLimits } from "./interpreter/runtime.js"
+import { executeWithLimits } from "./interpreter/execute.js"
 import { type HostTools, type Services, type ToolDescription, ToolRuntime } from "./tool-runtime.js"
 import { type HostTools, type Services, type ToolDescription, ToolRuntime } from "./tool-runtime.js"
 import type { Definition } from "./tool.js"
 import type { Definition } from "./tool.js"
 
 
@@ -9,15 +9,15 @@ export type { ToolCall, ToolCallEnded, ToolCallHooks, ToolCallStarted, ToolDescr
 /** Resource budgets enforced independently during each CodeMode program execution. */
 /** Resource budgets enforced independently during each CodeMode program execution. */
 export type ExecutionLimits = {
 export type ExecutionLimits = {
   /**
   /**
-   * Wall-clock milliseconds before execution is interrupted; result delivery additionally
-   * waits for tool interruption cleanup. No default: absent means no timeout.
+   * Wall-clock milliseconds before interruption. Result delivery waits for tool cleanup.
+   * No default: absent means no timeout.
    */
    */
   readonly timeoutMs?: number
   readonly timeoutMs?: number
   /** Maximum number of tool calls admitted by the runtime. No default: absent means unlimited. */
   /** Maximum number of tool calls admitted by the runtime. No default: absent means unlimited. */
   readonly maxToolCalls?: number
   readonly maxToolCalls?: number
   /**
   /**
-   * Maximum UTF-8 bytes retained from the result value and logs; warnings have a separate
-   * budget of the same size. Fixed truncation notices and host formatting are additional.
+   * Maximum UTF-8 bytes retained from the result and logs. Warnings have a separate equal budget;
+   * truncation notices and host formatting are additional.
    */
    */
   readonly maxOutputBytes?: number
   readonly maxOutputBytes?: number
 }
 }
@@ -94,7 +94,6 @@ const ToolCallSchema = Schema.Struct({ name: Schema.String })
 export const Success = Schema.Struct({
 export const Success = Schema.Struct({
   ok: Schema.Literal(true),
   ok: Schema.Literal(true),
   value: Schema.Json,
   value: Schema.Json,
-  // Runtime-authored non-fatal diagnostics; program console output stays in `logs`.
   warnings: Schema.optionalKey(Schema.Array(Diagnostic)),
   warnings: Schema.optionalKey(Schema.Array(Diagnostic)),
   logs: Schema.optionalKey(Schema.Array(Schema.String)),
   logs: Schema.optionalKey(Schema.Array(Schema.String)),
   truncated: Schema.optionalKey(Schema.Boolean),
   truncated: Schema.optionalKey(Schema.Boolean),

+ 93 - 0
packages/codemode/src/interpreter/errors.ts

@@ -0,0 +1,93 @@
+import type { Diagnostic } from "../codemode.js"
+import { ToolError } from "../tool-error.js"
+import { copyOut, ToolRuntimeError, type SafeObject } from "../tool-runtime.js"
+import { type AstNode, formatLocation, InterpreterRuntimeError, ProgramThrow, sourceLocation } from "./model.js"
+import { containsRuntimeReference } from "./references.js"
+import { spreadItems } from "../stdlib/collections.js"
+import { coerceToString, createAggregateErrorValue, createErrorValue, errorConstructors } from "../stdlib/value.js"
+
+export const normalizeError = (error: unknown): Diagnostic => {
+  if (error instanceof InterpreterRuntimeError) {
+    return {
+      kind: error.kind,
+      message: `${error.message}${formatLocation(error.node)}`,
+      ...(error.node?.loc ? { location: sourceLocation(error.node) } : {}),
+      ...(error.suggestions ? { suggestions: error.suggestions } : {}),
+    }
+  }
+
+  if (error instanceof ToolRuntimeError) {
+    return {
+      kind: error.kind,
+      message: error.message,
+      ...(error.suggestions.length > 0 ? { suggestions: error.suggestions } : {}),
+    }
+  }
+
+  if (error instanceof ToolError) {
+    return { kind: "ToolFailure", message: error.message }
+  }
+
+  if (error instanceof ProgramThrow) {
+    const value = error.value
+    let message: string
+    if (containsRuntimeReference(value)) {
+      // Never expose runtime reference internals through thrown values.
+      message = "a non-data value"
+    } else if (typeof value === "string") {
+      message = value
+    } else if (
+      value !== null &&
+      typeof value === "object" &&
+      typeof (value as { message?: unknown }).message === "string"
+    ) {
+      message = (value as { message: string }).message
+    } else {
+      try {
+        message = JSON.stringify(copyOut(value)) ?? String(value)
+      } catch {
+        message = String(value)
+      }
+    }
+    return { kind: "ExecutionFailure", message: `Uncaught: ${message}` }
+  }
+
+  if (error instanceof RangeError && /call stack|recursion/i.test(error.message)) {
+    return {
+      kind: "ExecutionFailure",
+      message: "Execution exceeded the maximum nesting depth.",
+    }
+  }
+
+  if (error instanceof Error) {
+    return {
+      kind: error.name === "SyntaxError" ? "ParseError" : "ExecutionFailure",
+      message: error.message,
+    }
+  }
+
+  return {
+    kind: "ExecutionFailure",
+    message: String(error),
+  }
+}
+
+export const caughtErrorValue = (thrown: unknown): unknown => {
+  if (thrown instanceof ProgramThrow) return thrown.value
+  if (thrown instanceof InterpreterRuntimeError) return createErrorValue(thrown.errorName, thrown.message)
+  const name = thrown instanceof Error && errorConstructors.has(thrown.name) ? thrown.name : "Error"
+  return createErrorValue(name, normalizeError(thrown).message)
+}
+
+export const constructErrorValue = (name: string, args: Array<unknown>, node: AstNode): SafeObject => {
+  if (name !== "AggregateError") return createErrorValue(name, args[0] === undefined ? "" : coerceToString(args[0]))
+  const errors = spreadItems(args[0])
+  if (errors === undefined) {
+    throw new InterpreterRuntimeError(
+      "new AggregateError(...) expects an array of errors (e.g. new AggregateError(errors, message?)).",
+      node,
+    ).as("TypeError")
+  }
+  // Error values must not alias caller-owned arrays.
+  return createAggregateErrorValue([...errors], args[1] === undefined ? "" : coerceToString(args[1]))
+}

+ 224 - 0
packages/codemode/src/interpreter/execute.ts

@@ -0,0 +1,224 @@
+import { parse } from "acorn"
+import { Cause, Effect, Scope } from "effect"
+import { DiagnosticCategory, ModuleKind, ScriptTarget, flattenDiagnosticMessageText, transpileModule } from "typescript"
+import type { DataValue, Diagnostic, ExecuteOptions, ResolvedExecutionLimits, Result } from "../codemode.js"
+import { copyIn, copyOut, ToolRuntime, type HostTools, type Services } from "../tool-runtime.js"
+import { normalizeError } from "./errors.js"
+import { InterpreterRuntimeError, isRecord, type ProgramNode } from "./model.js"
+import { PromiseRuntime } from "./promises.js"
+import { Interpreter } from "./runtime.js"
+
+export const executeWithLimits = <const Tools extends Record<string, unknown>>(
+  options: ExecuteOptions<Tools>,
+  limits: ResolvedExecutionLimits,
+  searchIndex: ToolRuntime.DiscoveryPlan["searchIndex"],
+): Effect.Effect<Result, never, Services<Tools>> => {
+  if (options.code.trim().length === 0) {
+    return Effect.succeed({
+      ok: false,
+      error: { kind: "ParseError", message: "Code cannot be empty." },
+      toolCalls: [],
+    })
+  }
+
+  // Allocate execution state inside suspension so reused Effects never share it.
+  return Effect.suspend(() => {
+    const tools = ToolRuntime.make(
+      (options.tools ?? {}) as HostTools<Services<Tools>>,
+      limits.maxToolCalls,
+      searchIndex,
+      {
+        onToolCallStart: options.onToolCallStart,
+        onToolCallEnd: options.onToolCallEnd,
+      },
+    )
+    const logs: Array<string> = []
+    const logged = () => (logs.length > 0 ? { logs: [...logs] } : {})
+    // Set only after copy-out so timeouts cannot report invalid values as completed.
+    let returned: { value: DataValue; promises: PromiseRuntime<Services<Tools>> } | undefined
+
+    const base = Effect.acquireUseRelease(
+      Scope.make("parallel"),
+      (scope) =>
+        Effect.gen(function* () {
+          const program = parseProgram(options.code)
+          const promises = new PromiseRuntime<Services<Tools>>(scope)
+          const interpreter = new Interpreter<Services<Tools>>(tools.invoke, tools.search, tools.keys, promises, logs)
+          const value = yield* interpreter.run(program)
+          const result = copyOut(copyIn(value, "Execution result"), true) as DataValue
+          returned = { value: result, promises }
+          const warnings = yield* promises.interrupt()
+          return {
+            ok: true,
+            value: result,
+            ...(warnings.length > 0 ? { warnings } : {}),
+            ...logged(),
+            toolCalls: tools.calls,
+          } satisfies Result
+        }),
+      (scope, exit) => Scope.close(scope, exit),
+    )
+    const timeoutMs = limits.timeoutMs
+    const operation =
+      timeoutMs === undefined
+        ? base
+        : base.pipe(
+            Effect.timeoutOrElse({
+              duration: timeoutMs,
+              orElse: () =>
+                Effect.sync(() => {
+                  if (returned === undefined) {
+                    return {
+                      ok: false,
+                      error: { kind: "TimeoutExceeded", message: `Execution timed out after ${timeoutMs}ms.` },
+                      ...logged(),
+                      toolCalls: tools.calls,
+                    } satisfies Result
+                  }
+                  // Keep the timeout warning first so truncation preserves it.
+                  return {
+                    ok: true,
+                    value: returned.value,
+                    warnings: [
+                      {
+                        kind: "TimeoutExceeded",
+                        message: `The program returned, but background work was still running at the ${timeoutMs}ms timeout and was interrupted. Await all started promises.`,
+                      },
+                      ...returned.promises.diagnostics(),
+                    ],
+                    ...logged(),
+                    toolCalls: tools.calls,
+                  } satisfies Result
+                }),
+            }),
+          )
+
+    return operation.pipe(
+      Effect.catchCause((cause) =>
+        Cause.hasInterruptsOnly(cause)
+          ? Effect.interrupt
+          : Effect.succeed({
+              ok: false,
+              error: normalizeError(Cause.squash(cause)),
+              ...logged(),
+              toolCalls: tools.calls,
+            } satisfies Result),
+      ),
+      Effect.map((result) =>
+        limits.maxOutputBytes === undefined ? result : boundOutput(result, limits.maxOutputBytes),
+      ),
+    )
+  })
+}
+
+const parseProgram = (code: string): ProgramNode => {
+  const transpiled = transpileModule(`async function __codemode__() {\n${code}\n}`, {
+    reportDiagnostics: true,
+    compilerOptions: {
+      target: ScriptTarget.ESNext,
+      module: ModuleKind.ESNext,
+    },
+  })
+  const diagnostic = transpiled.diagnostics?.find((item) => item.category === DiagnosticCategory.Error)
+
+  if (diagnostic) {
+    throw new InterpreterRuntimeError(
+      `Failed to parse TypeScript: ${flattenDiagnosticMessageText(diagnostic.messageText, "\n")}`,
+      undefined,
+      "ParseError",
+    )
+  }
+
+  const bodyStart = transpiled.outputText.indexOf("{") + 1
+  const bodyEnd = transpiled.outputText.lastIndexOf("}")
+  const executableCode = transpiled.outputText.slice(bodyStart, bodyEnd)
+  const parsed = parse(executableCode, {
+    ecmaVersion: "latest",
+    sourceType: "script",
+    allowReturnOutsideFunction: true,
+    allowAwaitOutsideFunction: true,
+    locations: true,
+  }) as unknown
+
+  if (!isRecord(parsed) || parsed.type !== "Program" || !Array.isArray(parsed.body)) {
+    throw new InterpreterRuntimeError("Failed to parse script as a Program node.")
+  }
+
+  return parsed as ProgramNode
+}
+
+const utf8ByteLength = (value: string): number => new TextEncoder().encode(value).byteLength
+
+// Drop a replacement character produced by truncating inside a UTF-8 sequence.
+const utf8Truncate = (value: string, maxBytes: number): string => {
+  const bytes = new TextEncoder().encode(value)
+  if (bytes.byteLength <= maxBytes) return value
+  const text = new TextDecoder("utf-8").decode(bytes.slice(0, Math.max(0, maxBytes)))
+  return text.endsWith("\uFFFD") ? text.slice(0, -1) : text
+}
+
+// Warnings have a separate budget so result data cannot starve diagnostics.
+const boundOutput = (result: Result, maxOutputBytes: number): Result => {
+  let truncated = false
+
+  let value: DataValue = null
+  let valueBytes = 0
+  if (result.ok) {
+    const serialized = JSON.stringify(result.value) ?? "null"
+    const bytes = utf8ByteLength(serialized)
+    if (bytes > maxOutputBytes) {
+      truncated = true
+      value = `${utf8Truncate(serialized, maxOutputBytes)} [result truncated: ${bytes} bytes exceeds the ${maxOutputBytes}-byte output limit; return a smaller value]`
+      valueBytes = maxOutputBytes
+    } else {
+      value = result.value
+      valueBytes = bytes
+    }
+  }
+
+  const warnings = result.ok ? (result.warnings ?? []) : []
+  const keptWarnings: Array<Diagnostic> = []
+  let warningBytes = 0
+  for (const warning of warnings) {
+    const bytes = utf8ByteLength(JSON.stringify(warning)) + 1
+    if (warningBytes + bytes > maxOutputBytes) break
+    warningBytes += bytes
+    keptWarnings.push(warning)
+  }
+  if (keptWarnings.length < warnings.length) {
+    truncated = true
+    keptWarnings.push({
+      kind: "Truncated",
+      message: `${warnings.length - keptWarnings.length} additional warnings omitted by the output limit.`,
+    })
+  }
+
+  const logs = result.logs ?? []
+  const kept: Array<string> = []
+  const logBudget = Math.max(0, maxOutputBytes - valueBytes)
+  let logBytes = 0
+  for (const line of logs) {
+    const lineBytes = utf8ByteLength(line) + 1
+    if (logBytes + lineBytes > logBudget) break
+    logBytes += lineBytes
+    kept.push(line)
+  }
+  if (kept.length < logs.length) {
+    truncated = true
+    kept.push(`[logs truncated: showing ${kept.length} of ${logs.length} lines]`)
+  }
+
+  if (!truncated) return result
+  const warningsPart = keptWarnings.length > 0 ? { warnings: keptWarnings } : {}
+  const logsPart = kept.length > 0 ? { logs: kept } : {}
+  return result.ok
+    ? {
+        ok: true,
+        value,
+        ...warningsPart,
+        ...logsPart,
+        truncated: true,
+        toolCalls: result.toolCalls,
+      }
+    : { ok: false, error: result.error, ...logsPart, truncated: true, toolCalls: result.toolCalls }
+}

+ 819 - 0
packages/codemode/src/interpreter/methods.ts

@@ -0,0 +1,819 @@
+import { Effect } from "effect"
+import {
+  type AstNode,
+  CodeModeFunction,
+  CoercionFunction,
+  GlobalMethodReference,
+  IntrinsicReference,
+  InterpreterRuntimeError,
+  PromiseCapabilityFunction,
+  supportedSyntaxMessage,
+  UriFunction,
+} from "./model.js"
+import { rejectCircularInsertion } from "./references.js"
+import { isBlockedMember, type SafeObject } from "../tool-runtime.js"
+import {
+  SandboxDate,
+  SandboxMap,
+  SandboxPromise,
+  SandboxRegExp,
+  SandboxSet,
+  SandboxURL,
+  SandboxURLSearchParams,
+} from "../values.js"
+import { invokeDateMethod, invokeDateStatic } from "../stdlib/date.js"
+import { invokeJsonMethod } from "../stdlib/json.js"
+import { invokeMathMethod } from "../stdlib/math.js"
+import { invokeNumberMethod, invokeNumberStatic } from "../stdlib/number.js"
+import { invokeObjectMethod } from "../stdlib/object.js"
+import { invokeRegExpMethod, matchToValue, toHostRegex } from "../stdlib/regexp.js"
+import { invokeStringStatic } from "../stdlib/string.js"
+import { invokeUriFunction, invokeURLMethod, invokeURLStatic, uriArgument } from "../stdlib/url.js"
+import { boundedData, coerceToNumber, coerceToString, invokeCoercion } from "../stdlib/value.js"
+
+export type CallbackRunner<R> = {
+  readonly invokeFunction: (fn: CodeModeFunction, args: Array<unknown>) => Effect.Effect<unknown, unknown, R>
+  readonly settlePromise: (promise: SandboxPromise) => Effect.Effect<unknown, unknown, never>
+}
+
+export const invokeIntrinsic = <R>(
+  runner: CallbackRunner<R>,
+  ref: IntrinsicReference,
+  args: Array<unknown>,
+  node: AstNode,
+): Effect.Effect<unknown, unknown, R> => {
+  if (typeof ref.receiver === "string") {
+    if (
+      (ref.name === "replace" || ref.name === "replaceAll") &&
+      (args[1] instanceof CodeModeFunction || args[1] instanceof CoercionFunction || args[1] instanceof UriFunction)
+    ) {
+      return invokeStringReplacer(runner, ref.receiver, ref.name, args, node)
+    }
+    return Effect.succeed(invokeStringMethod(ref.receiver, ref.name, args, node))
+  }
+  if (typeof ref.receiver === "number") {
+    return Effect.succeed(invokeNumberMethod(ref.receiver, ref.name, args, node))
+  }
+  if (Array.isArray(ref.receiver)) {
+    return invokeArrayMethod(runner, ref.receiver, ref.name, args, node)
+  }
+  if (ref.receiver instanceof SandboxDate) {
+    return Effect.succeed(invokeDateMethod(ref.receiver, ref.name, node))
+  }
+  if (ref.receiver instanceof SandboxRegExp) {
+    return Effect.succeed(invokeRegExpMethod(ref.receiver, ref.name, args, node))
+  }
+  if (ref.receiver instanceof SandboxMap) {
+    return invokeMapMethod(runner, ref.receiver, ref.name, args, node)
+  }
+  if (ref.receiver instanceof SandboxSet) {
+    return invokeSetMethod(runner, ref.receiver, ref.name, args, node)
+  }
+  if (ref.receiver instanceof SandboxURL) {
+    return Effect.succeed(invokeURLMethod(ref.receiver, ref.name, node))
+  }
+  if (ref.receiver instanceof SandboxURLSearchParams) {
+    return invokeURLSearchParamsMethod(runner, ref.receiver, ref.name, args, node)
+  }
+  throw new InterpreterRuntimeError(`Method '${ref.name}' is not available in CodeMode.`, node)
+}
+
+export const invokeGlobalMethod = (ref: GlobalMethodReference, args: Array<unknown>, node: AstNode): unknown => {
+  if (ref.namespace === "console")
+    throw new InterpreterRuntimeError(`console.${ref.name} is not available in CodeMode.`, node)
+  if (ref.namespace === "Object") return invokeObjectMethod(ref.name, args, node)
+  if (ref.namespace === "Math") return invokeMathMethod(ref.name, args, node)
+  if (ref.namespace === "Array") return invokeArrayStatic(ref.name, args, node)
+  if (ref.namespace === "Number") return invokeNumberStatic(ref.name, args, node)
+  if (ref.namespace === "String") return invokeStringStatic(ref.name, args, node)
+  if (ref.namespace === "URL") return invokeURLStatic(ref.name, args, node)
+  if (ref.namespace === "Date") return invokeDateStatic(ref.name, args, node)
+  if (
+    ref.namespace === "RegExp" ||
+    ref.namespace === "Map" ||
+    ref.namespace === "Set" ||
+    ref.namespace === "URLSearchParams"
+  ) {
+    throw new InterpreterRuntimeError(`${ref.namespace}.${ref.name} is not available in CodeMode.`, node)
+  }
+  return invokeJsonMethod(ref.name, args, node)
+}
+
+const invokeStringMethod = (value: string, name: string, args: Array<unknown>, node: AstNode): unknown => {
+  const str = (index: number): string => {
+    const arg = args[index]
+    if (typeof arg !== "string")
+      throw new InterpreterRuntimeError(`String.${name} expects argument ${index + 1} to be a string.`, node)
+    return arg
+  }
+  const num = (index: number): number => {
+    const arg = args[index]
+    if (typeof arg !== "number")
+      throw new InterpreterRuntimeError(`String.${name} expects argument ${index + 1} to be a number.`, node)
+    return arg
+  }
+  const optNum = (index: number): number | undefined => (args[index] === undefined ? undefined : num(index))
+  const optStr = (index: number): string | undefined => (args[index] === undefined ? undefined : str(index))
+
+  let result: unknown
+  switch (name) {
+    case "toLowerCase":
+      result = value.toLowerCase()
+      break
+    case "toUpperCase":
+      result = value.toUpperCase()
+      break
+    case "trim":
+      result = value.trim()
+      break
+    case "trimStart":
+      result = value.trimStart()
+      break
+    case "trimEnd":
+      result = value.trimEnd()
+      break
+    // Locale/options are deliberately unsupported; comparison uses the host default locale.
+    case "localeCompare":
+      result = value.localeCompare(str(0))
+      break
+    case "normalize": {
+      const form = optStr(0)
+      try {
+        result = value.normalize(form)
+      } catch {
+        throw new InterpreterRuntimeError(
+          `String.normalize expects the form "NFC", "NFD", "NFKC", or "NFKD" (got ${JSON.stringify(form)}).`,
+          node,
+        ).as("RangeError")
+      }
+      break
+    }
+    case "split": {
+      if (args.length === 0) {
+        result = [value]
+        break
+      }
+      if (args[0] instanceof SandboxRegExp) {
+        result = value.split(args[0].regex, optNum(1))
+        break
+      }
+      const requestedLimit = optNum(1)
+      result = value.split(str(0), requestedLimit === undefined ? undefined : requestedLimit >>> 0)
+      break
+    }
+    case "slice":
+      result = value.slice(optNum(0), optNum(1))
+      break
+    case "includes":
+      result = value.includes(str(0), optNum(1))
+      break
+    case "startsWith":
+      result = value.startsWith(str(0), optNum(1))
+      break
+    case "endsWith":
+      result = value.endsWith(str(0), optNum(1))
+      break
+    case "indexOf":
+      result = value.indexOf(str(0), optNum(1))
+      break
+    case "lastIndexOf":
+      result = value.lastIndexOf(str(0), optNum(1))
+      break
+    case "replace":
+    case "replaceAll": {
+      if (args[0] instanceof SandboxRegExp) {
+        const pattern = args[0].regex
+        const replacement = str(1)
+        if (name === "replaceAll" && !pattern.global) {
+          throw new InterpreterRuntimeError(
+            `String.replaceAll requires a regular expression with the global (g) flag: write /${pattern.source}/${pattern.flags}g, or use String.replace to replace only the first match.`,
+            node,
+          )
+        }
+        result = name === "replace" ? value.replace(pattern, replacement) : value.replaceAll(pattern, replacement)
+        break
+      }
+      if (name === "replace") {
+        result = value.replace(str(0), str(1))
+        break
+      }
+      result = value.replaceAll(str(0), str(1))
+      break
+    }
+    case "match": {
+      const pattern = toHostRegex(args[0], name, node)
+      const matched = value.match(pattern)
+      if (matched === null) return null
+      // Preserve the own `index` and `groups` properties on non-global matches.
+      if (pattern.global) return boundedData(matched, "String.match result")
+      return matchToValue(matched)
+    }
+    case "matchAll": {
+      const pattern = toHostRegex(args[0], name, node, "g")
+      if (!pattern.global) {
+        throw new InterpreterRuntimeError(
+          `String.matchAll requires a regular expression with the global (g) flag: write /${pattern.source}/${pattern.flags}g, or use String.match for a single match.`,
+          node,
+        )
+      }
+      return Array.from(value.matchAll(pattern), matchToValue)
+    }
+    case "search": {
+      result = value.search(toHostRegex(args[0], name, node))
+      break
+    }
+    case "repeat": {
+      const count = num(0)
+      if (!Number.isFinite(count) || count < 0)
+        throw new InterpreterRuntimeError("String.repeat expects a finite non-negative count.", node)
+      result = value.repeat(count)
+      break
+    }
+    case "padStart":
+      result = value.padStart(num(0), optStr(1))
+      break
+    case "padEnd":
+      result = value.padEnd(num(0), optStr(1))
+      break
+    case "charAt":
+      result = value.charAt(optNum(0) ?? 0)
+      break
+    case "at":
+      result = value.at(optNum(0) ?? 0)
+      break
+    case "substring":
+      result = value.substring(optNum(0) ?? 0, optNum(1))
+      break
+    case "charCodeAt":
+      result = value.charCodeAt(optNum(0) ?? 0)
+      break
+    case "codePointAt":
+      result = value.codePointAt(optNum(0) ?? 0)
+      break
+    case "toString":
+      result = value
+      break
+    case "concat": {
+      result = value.concat(...args.map((_, index) => str(index)))
+      break
+    }
+    default:
+      throw new InterpreterRuntimeError(`String method '${name}' is not available in CodeMode.`, node)
+  }
+  return boundedData(result, `String.${name} result`)
+}
+
+const invokeArrayStatic = (name: string, args: Array<unknown>, node: AstNode): unknown => {
+  switch (name) {
+    case "isArray":
+      return Array.isArray(args[0])
+    case "of":
+      return [...args]
+    case "from": {
+      if (args.length > 1) {
+        throw new InterpreterRuntimeError(
+          "Array.from(...) does not support a map function in CodeMode; call .map() on the result instead.",
+          node,
+          "UnsupportedSyntax",
+          [supportedSyntaxMessage],
+        )
+      }
+      if (args[0] instanceof SandboxMap) return Array.from(args[0].map.entries(), ([key, item]) => [key, item])
+      if (args[0] instanceof SandboxSet) return Array.from(args[0].set.values())
+      if (args[0] instanceof SandboxURLSearchParams) {
+        return Array.from(args[0].params.entries(), ([key, value]) => [key, value])
+      }
+      const source = args[0]
+      if (source instanceof SandboxPromise) {
+        throw new InterpreterRuntimeError(
+          "Array.from received an un-awaited Promise; await it before creating the array.",
+          node,
+          "InvalidDataValue",
+        )
+      }
+      if (typeof source === "string") return Array.from(source)
+      if (Array.isArray(source)) return [...source]
+      if (
+        source !== null &&
+        typeof source === "object" &&
+        (Object.getPrototypeOf(source) === Object.prototype || Object.getPrototypeOf(source) === null) &&
+        typeof (source as { length?: unknown }).length === "number"
+      ) {
+        return Array.from(source as ArrayLike<unknown>)
+      }
+      throw new InterpreterRuntimeError(
+        "Array.from expects an array, string, Map, Set, or array-like value.",
+        node,
+        "InvalidDataValue",
+      )
+    }
+    default:
+      throw new InterpreterRuntimeError(`Array.${name} is not available in CodeMode.`, node)
+  }
+}
+
+const invokeStringReplacer = <R>(
+  runner: CallbackRunner<R>,
+  value: string,
+  name: "replace" | "replaceAll",
+  args: Array<unknown>,
+  node: AstNode,
+): Effect.Effect<unknown, unknown, R> => {
+  const apply = applyCollectionCallback(runner, args[1], `String.${name}`, node)
+  const matches: Array<{ readonly match: string; readonly offset: number; readonly args: Array<unknown> }> = []
+  const collect = (...callbackArgs: Array<unknown>): string => {
+    const match = callbackArgs[0]
+    const groups = callbackArgs[callbackArgs.length - 1]
+    const hasGroups = groups !== null && typeof groups === "object"
+    const offset = callbackArgs[callbackArgs.length - (hasGroups ? 3 : 2)]
+    if (typeof match !== "string" || typeof offset !== "number") {
+      throw new InterpreterRuntimeError(`String.${name} produced an invalid replacement match.`, node)
+    }
+    if (hasGroups) {
+      const safeGroups: SafeObject = Object.create(null) as SafeObject
+      for (const [key, group] of Object.entries(groups)) {
+        if (!isBlockedMember(key)) safeGroups[key] = group
+      }
+      callbackArgs[callbackArgs.length - 1] = safeGroups
+    }
+    matches.push({ match, offset, args: callbackArgs })
+    return match
+  }
+
+  const pattern = args[0]
+  if (pattern instanceof SandboxRegExp) {
+    if (name === "replaceAll" && !pattern.regex.global) {
+      throw new InterpreterRuntimeError(
+        `String.replaceAll requires a regular expression with the global (g) flag: write /${pattern.regex.source}/${pattern.regex.flags}g, or use String.replace to replace only the first match.`,
+        node,
+      )
+    }
+    if (name === "replace") value.replace(pattern.regex, collect)
+    else value.replaceAll(pattern.regex, collect)
+  } else {
+    if (typeof pattern !== "string") {
+      throw new InterpreterRuntimeError(`String.${name} expects argument 1 to be a string.`, node)
+    }
+    if (name === "replace") value.replace(pattern, collect)
+    else value.replaceAll(pattern, collect)
+  }
+
+  return Effect.gen(function* () {
+    const output: Array<string> = []
+    let end = 0
+    for (const match of matches) {
+      const replacement = yield* apply(match.args)
+      const resolved =
+        args[1] instanceof CodeModeFunction && args[1].async && replacement instanceof SandboxPromise
+          ? yield* runner.settlePromise(replacement)
+          : replacement
+      output.push(
+        value.slice(end, match.offset),
+        coerceToString(boundedData(resolved, `String.${name} replacer result`)),
+      )
+      end = match.offset + match.match.length
+    }
+    output.push(value.slice(end))
+    return boundedData(output.join(""), `String.${name} result`)
+  })
+}
+
+export const applyCollectionCallback = <R>(
+  runner: CallbackRunner<R>,
+  callback: unknown,
+  name: string,
+  node: AstNode,
+): ((args: Array<unknown>) => Effect.Effect<unknown, unknown, R>) => {
+  if (
+    !(callback instanceof CodeModeFunction) &&
+    !(callback instanceof CoercionFunction) &&
+    !(callback instanceof UriFunction) &&
+    !(callback instanceof PromiseCapabilityFunction)
+  ) {
+    throw new InterpreterRuntimeError(`${name} expects a function callback.`, node)
+  }
+  return (callbackArgs) =>
+    callback instanceof CoercionFunction
+      ? Effect.succeed(invokeCoercion(callback, callbackArgs, node))
+      : callback instanceof UriFunction
+        ? Effect.succeed(invokeUriFunction(callback, callbackArgs, node))
+        : callback instanceof PromiseCapabilityFunction
+          ? Effect.sync(() => callback.settle(callbackArgs[0]))
+          : runner.invokeFunction(callback, callbackArgs)
+}
+
+const invokeMapMethod = <R>(
+  runner: CallbackRunner<R>,
+  target: SandboxMap,
+  name: string,
+  args: Array<unknown>,
+  node: AstNode,
+): Effect.Effect<unknown, unknown, R> => {
+  switch (name) {
+    case "get":
+      return Effect.succeed(target.map.get(args[0]))
+    case "has":
+      return Effect.succeed(target.map.has(args[0]))
+    case "set":
+      return Effect.sync(() => {
+        target.map.set(args[0], args[1])
+        return target
+      })
+    case "delete":
+      return Effect.sync(() => target.map.delete(args[0]))
+    case "clear":
+      return Effect.sync(() => {
+        target.map.clear()
+        return undefined
+      })
+    case "keys":
+      return Effect.sync(() => Array.from(target.map.keys()))
+    case "values":
+      return Effect.sync(() => Array.from(target.map.values()))
+    case "entries":
+      return Effect.sync(() => Array.from(target.map.entries(), ([key, item]): Array<unknown> => [key, item]))
+    case "forEach": {
+      const apply = applyCollectionCallback(runner, args[0], "Map.forEach", node)
+      return Effect.gen(function* () {
+        for (const [key, item] of Array.from(target.map.entries())) yield* apply([item, key, target])
+        return undefined
+      })
+    }
+    default:
+      throw new InterpreterRuntimeError(`Map method '${name}' is not available in CodeMode.`, node)
+  }
+}
+
+const invokeSetMethod = <R>(
+  runner: CallbackRunner<R>,
+  target: SandboxSet,
+  name: string,
+  args: Array<unknown>,
+  node: AstNode,
+): Effect.Effect<unknown, unknown, R> => {
+  switch (name) {
+    case "has":
+      return Effect.succeed(target.set.has(args[0]))
+    case "add":
+      return Effect.sync(() => {
+        target.set.add(args[0])
+        return target
+      })
+    case "delete":
+      return Effect.sync(() => target.set.delete(args[0]))
+    case "clear":
+      return Effect.sync(() => {
+        target.set.clear()
+        return undefined
+      })
+    case "keys":
+    case "values":
+      return Effect.sync(() => Array.from(target.set.values()))
+    case "entries":
+      return Effect.sync(() => Array.from(target.set.values(), (item): Array<unknown> => [item, item]))
+    case "forEach": {
+      const apply = applyCollectionCallback(runner, args[0], "Set.forEach", node)
+      return Effect.gen(function* () {
+        for (const item of Array.from(target.set.values())) yield* apply([item, item, target])
+        return undefined
+      })
+    }
+    default:
+      throw new InterpreterRuntimeError(`Set method '${name}' is not available in CodeMode.`, node)
+  }
+}
+
+const invokeURLSearchParamsMethod = <R>(
+  runner: CallbackRunner<R>,
+  target: SandboxURLSearchParams,
+  name: string,
+  args: Array<unknown>,
+  node: AstNode,
+): Effect.Effect<unknown, unknown, R> => {
+  const arg = (index: number): string => uriArgument(args[index], `URLSearchParams.${name} argument ${index + 1}`)
+  const requireArgs = (count: number): void => {
+    if (args.length < count) {
+      throw new InterpreterRuntimeError(
+        `URLSearchParams.${name} requires ${count} argument${count === 1 ? "" : "s"}.`,
+        node,
+      ).as("TypeError")
+    }
+  }
+  switch (name) {
+    case "append": {
+      requireArgs(2)
+      return Effect.sync(() => {
+        target.params.append(arg(0), arg(1))
+        return undefined
+      })
+    }
+    case "delete": {
+      requireArgs(1)
+      return Effect.sync(() => {
+        if (args[1] !== undefined) target.params.delete(arg(0), arg(1))
+        else target.params.delete(arg(0))
+        return undefined
+      })
+    }
+    case "get":
+      requireArgs(1)
+      return Effect.sync(() => target.params.get(arg(0)))
+    case "getAll":
+      requireArgs(1)
+      return Effect.sync(() => target.params.getAll(arg(0)))
+    case "has":
+      requireArgs(1)
+      return Effect.sync(() => (args[1] !== undefined ? target.params.has(arg(0), arg(1)) : target.params.has(arg(0))))
+    case "set": {
+      requireArgs(2)
+      return Effect.sync(() => {
+        target.params.set(arg(0), arg(1))
+        return undefined
+      })
+    }
+    case "sort":
+      return Effect.sync(() => {
+        target.params.sort()
+        return undefined
+      })
+    case "keys":
+      return Effect.sync(() => Array.from(target.params.keys()))
+    case "values":
+      return Effect.sync(() => Array.from(target.params.values()))
+    case "entries":
+      return Effect.sync(() => Array.from(target.params.entries(), ([key, value]): Array<unknown> => [key, value]))
+    case "toString":
+      return Effect.sync(() => target.params.toString())
+    case "forEach": {
+      requireArgs(1)
+      const apply = applyCollectionCallback(runner, args[0], "URLSearchParams.forEach", node)
+      return Effect.gen(function* () {
+        for (const [key, value] of Array.from(target.params.entries())) yield* apply([value, key, target])
+        return undefined
+      })
+    }
+    default:
+      throw new InterpreterRuntimeError(`URLSearchParams method '${name}' is not available in CodeMode.`, node)
+  }
+}
+
+const invokeArrayMethod = <R>(
+  runner: CallbackRunner<R>,
+  target: Array<unknown>,
+  name: string,
+  args: Array<unknown>,
+  node: AstNode,
+): Effect.Effect<unknown, unknown, R> => {
+  const optNumber = (value: unknown, label: string): number | undefined => {
+    if (value === undefined) return undefined
+    if (typeof value !== "number")
+      throw new InterpreterRuntimeError(`Array.${name} expects ${label} to be a number.`, node)
+    return value
+  }
+  switch (name) {
+    case "join": {
+      if (args.length > 1 || (args.length === 1 && typeof args[0] !== "string")) {
+        throw new InterpreterRuntimeError("Array.join expects zero arguments or one string separator.", node)
+      }
+      const input = boundedData(target, "Array.join input") as Array<unknown>
+      return Effect.succeed(
+        input.map((item) => coerceToString(item ?? "")).join(args.length === 0 ? "," : (args[0] as string)),
+      )
+    }
+    case "includes":
+      if (args.length === 0 || args.length > 2)
+        throw new InterpreterRuntimeError("Array.includes expects a value and optional start index.", node)
+      return Effect.succeed(target.includes(args[0], optNumber(args[1], "start index")))
+    case "indexOf":
+      return Effect.succeed(target.indexOf(args[0], optNumber(args[1], "start index")))
+    case "lastIndexOf":
+      return Effect.succeed(
+        args[1] === undefined
+          ? target.lastIndexOf(args[0])
+          : target.lastIndexOf(args[0], optNumber(args[1], "start index")),
+      )
+    case "at":
+      return Effect.succeed(target.at(optNumber(args[0], "index") ?? 0))
+    case "slice":
+      return Effect.succeed(target.slice(optNumber(args[0], "start"), optNumber(args[1], "end")))
+    case "concat":
+      return Effect.succeed(target.concat(...args))
+    case "flat":
+      return Effect.succeed(target.flat(optNumber(args[0], "depth") ?? 1))
+    case "reverse":
+      return Effect.succeed(target.reverse())
+    case "sort":
+      return Effect.map(sortArray(runner, target, args[0], node), (sorted) => {
+        target.splice(0, target.length, ...sorted)
+        return target
+      })
+    case "toSorted":
+      return sortArray(runner, target, args[0], node)
+    case "toReversed":
+      return Effect.succeed([...target].reverse())
+    case "with": {
+      const index = optNumber(args[0], "index") ?? 0
+      const resolved = index < 0 ? target.length + index : index
+      if (resolved < 0 || resolved >= target.length) {
+        throw new InterpreterRuntimeError("Array.with index is out of range.", node)
+      }
+      const copied = [...target]
+      copied[resolved] = args[1]
+      return Effect.succeed(copied)
+    }
+    case "push": {
+      // Validate all insertions before mutating to avoid partial cyclic updates.
+      for (const item of args) rejectCircularInsertion(target, item, "Array.push result", node)
+      target.push(...args)
+      return Effect.succeed(target.length)
+    }
+    case "unshift": {
+      for (const item of args) rejectCircularInsertion(target, item, "Array.unshift result", node)
+      target.unshift(...args)
+      return Effect.succeed(target.length)
+    }
+    case "pop":
+      return Effect.succeed(target.pop())
+    case "shift":
+      return Effect.succeed(target.shift())
+    case "splice": {
+      if (args.length === 0) return Effect.succeed(target.splice(0, 0))
+      const start = optNumber(args[0], "start") ?? 0
+      if (args.length === 1) return Effect.succeed(target.splice(start))
+      const deleteCount = optNumber(args[1], "delete count") ?? 0
+      const inserted = args.slice(2)
+      for (const item of inserted) rejectCircularInsertion(target, item, "Array.splice result", node)
+      return Effect.succeed(target.splice(start, deleteCount, ...inserted))
+    }
+    case "fill": {
+      rejectCircularInsertion(target, args[0], "Array.fill result", node)
+      return Effect.succeed(target.fill(args[0], optNumber(args[1], "start"), optNumber(args[2], "end")))
+    }
+    case "copyWithin":
+      return Effect.succeed(
+        target.copyWithin(
+          optNumber(args[0], "target index") ?? 0,
+          optNumber(args[1], "start") ?? 0,
+          optNumber(args[2], "end"),
+        ),
+      )
+    case "keys":
+      return Effect.succeed(Array.from(target.keys()))
+    case "values":
+      return Effect.succeed([...target])
+    case "entries":
+      return Effect.succeed(Array.from(target.entries(), ([index, item]): Array<unknown> => [index, item]))
+  }
+
+  const apply = applyCollectionCallback(runner, args[0], `Array.${name}`, node)
+  return Effect.gen(function* () {
+    // Fix iteration length while reading existing elements live.
+    const length = target.length
+    switch (name) {
+      case "map": {
+        const values: Array<unknown> = []
+        values.length = length
+        for (let index = 0; index < length; index += 1) {
+          if (!(index in target)) continue
+          values[index] = yield* apply([target[index], index, target])
+        }
+        return values
+      }
+      case "flatMap": {
+        const values: Array<unknown> = []
+        for (let index = 0; index < length; index += 1) {
+          if (!(index in target)) continue
+          const mapped = yield* apply([target[index], index, target])
+          if (Array.isArray(mapped)) values.push(...mapped)
+          else values.push(mapped)
+        }
+        return values
+      }
+      case "filter": {
+        const values: Array<unknown> = []
+        for (let index = 0; index < length; index += 1) {
+          if (!(index in target)) continue
+          const item = target[index]
+          if (yield* apply([item, index, target])) values.push(item)
+        }
+        return values
+      }
+      case "find":
+        for (let index = 0; index < length; index += 1) {
+          const item = target[index]
+          if (yield* apply([item, index, target])) return item
+        }
+        return undefined
+      case "findIndex":
+        for (let index = 0; index < length; index += 1) {
+          if (yield* apply([target[index], index, target])) return index
+        }
+        return -1
+      case "some":
+        for (let index = 0; index < length; index += 1) {
+          if (!(index in target)) continue
+          if (yield* apply([target[index], index, target])) return true
+        }
+        return false
+      case "every":
+        for (let index = 0; index < length; index += 1) {
+          if (!(index in target)) continue
+          if (!(yield* apply([target[index], index, target]))) return false
+        }
+        return true
+      case "forEach":
+        for (let index = 0; index < length; index += 1) {
+          if (index in target) yield* apply([target[index], index, target])
+        }
+        return undefined
+      case "reduce": {
+        let accumulator: unknown
+        let start: number
+        if (args.length >= 2) {
+          accumulator = args[1]
+          start = 0
+        } else {
+          if (length === 0)
+            throw new InterpreterRuntimeError("Array.reduce of an empty array with no initial value.", node)
+          accumulator = target[0]
+          start = 1
+        }
+        for (let index = start; index < length; index += 1) {
+          if (!(index in target)) continue
+          accumulator = yield* apply([accumulator, target[index], index, target])
+        }
+        return accumulator
+      }
+      case "reduceRight": {
+        let accumulator: unknown
+        let start: number
+        if (args.length >= 2) {
+          accumulator = args[1]
+          start = length - 1
+        } else {
+          if (length === 0)
+            throw new InterpreterRuntimeError("Array.reduceRight of an empty array with no initial value.", node)
+          accumulator = target[length - 1]
+          start = length - 2
+        }
+        for (let index = start; index >= 0; index -= 1) {
+          if (!(index in target)) continue
+          accumulator = yield* apply([accumulator, target[index], index, target])
+        }
+        return accumulator
+      }
+      case "findLast":
+        for (let index = length - 1; index >= 0; index -= 1) {
+          if (yield* apply([target[index], index, target])) return target[index]
+        }
+        return undefined
+      case "findLastIndex":
+        for (let index = length - 1; index >= 0; index -= 1) {
+          if (yield* apply([target[index], index, target])) return index
+        }
+        return -1
+    }
+    throw new InterpreterRuntimeError(`Array method '${name}' is not available in CodeMode.`, node)
+  })
+}
+
+const sortArray = <R>(
+  runner: CallbackRunner<R>,
+  target: Array<unknown>,
+  comparator: unknown,
+  node: AstNode,
+): Effect.Effect<Array<unknown>, unknown, R> => {
+  if (comparator !== undefined && !(comparator instanceof CodeModeFunction)) {
+    throw new InterpreterRuntimeError("Array.sort expects an arrow function comparator.", node)
+  }
+  if (!(comparator instanceof CodeModeFunction)) {
+    return Effect.sync(() =>
+      [...target].sort((a, b) => {
+        const left = coerceToString(a)
+        const right = coerceToString(b)
+        return left < right ? -1 : left > right ? 1 : 0
+      }),
+    )
+  }
+  const mergeSort = (items: Array<unknown>): Effect.Effect<Array<unknown>, unknown, R> => {
+    if (items.length <= 1) return Effect.succeed(items)
+    const midpoint = Math.floor(items.length / 2)
+    return Effect.gen(function* () {
+      const left = yield* mergeSort(items.slice(0, midpoint))
+      const right = yield* mergeSort(items.slice(midpoint))
+      const merged: Array<unknown> = []
+      let leftIndex = 0
+      let rightIndex = 0
+      while (leftIndex < left.length && rightIndex < right.length) {
+        // Treat a NaN comparator result as equal to preserve stable ordering.
+        const order = coerceToNumber(yield* runner.invokeFunction(comparator, [left[leftIndex], right[rightIndex]]))
+        if (Number.isNaN(order) || order <= 0) merged.push(left[leftIndex++])
+        else merged.push(right[rightIndex++])
+      }
+      return [...merged, ...left.slice(leftIndex), ...right.slice(rightIndex)]
+    })
+  }
+  const defined = target.filter((item) => item !== undefined)
+  const undefinedCount = target.length - defined.length
+  return Effect.map(mergeSort(defined), (items) => [...items, ...Array(undefinedCount).fill(undefined)])
+}

+ 0 - 4
packages/codemode/src/interpreter/model.ts

@@ -76,8 +76,6 @@ export class PromiseInstanceMethodReference {
   ) {}
   ) {}
 }
 }
 
 
-// The resolve/reject callables handed to a `new Promise(executor)` executor. `settle` closes
-// over the promise's deferred and is first-settlement-wins; later calls are no-ops, as in JS.
 export class PromiseCapabilityFunction {
 export class PromiseCapabilityFunction {
   constructor(readonly settle: (value: unknown) => void) {}
   constructor(readonly settle: (value: unknown) => void) {}
 }
 }
@@ -114,8 +112,6 @@ export class UriFunction {
   constructor(readonly name: "encodeURI" | "encodeURIComponent" | "decodeURI" | "decodeURIComponent") {}
   constructor(readonly name: "encodeURI" | "encodeURIComponent" | "decodeURI" | "decodeURIComponent") {}
 }
 }
 
 
-// The global `search` built-in: synchronous tool discovery that shares the tool admission
-// pipeline (budget, audit, hooks) without living in the `tools` tree.
 export class SearchFunction {}
 export class SearchFunction {}
 
 
 export class ProgramThrow {
 export class ProgramThrow {

+ 336 - 0
packages/codemode/src/interpreter/promises.ts

@@ -0,0 +1,336 @@
+import { Cause, Deferred, Effect, Exit, Fiber, Scope } from "effect"
+import type { Diagnostic } from "../codemode.js"
+import type { SafeObject } from "../tool-runtime.js"
+import {
+  type AstNode,
+  CodeModeFunction,
+  CoercionFunction,
+  InterpreterRuntimeError,
+  ProgramThrow,
+  PromiseCapabilityFunction,
+  PromiseInstanceMethodReference,
+  PromiseMethodReference,
+  UriFunction,
+} from "./model.js"
+import { caughtErrorValue, normalizeError } from "./errors.js"
+import { applyCollectionCallback, type CallbackRunner } from "./methods.js"
+import { typeofValue } from "./references.js"
+import { spreadItems } from "../stdlib/collections.js"
+import { createAggregateErrorValue } from "../stdlib/value.js"
+import { SandboxPromise } from "../values.js"
+
+// Observation only controls rejection reporting; program completion interrupts all promise work.
+export class PromiseRuntime<R> {
+  private readonly active = new Set<SandboxPromise>()
+  private readonly ids = new WeakMap<SandboxPromise, number>()
+  private readonly observed = new WeakSet<SandboxPromise>()
+  private readonly failures = new Map<number, Diagnostic>()
+  private nextID = 0
+
+  constructor(private readonly scope: Scope.Scope) {}
+
+  create(effect: Effect.Effect<unknown, unknown, R>): Effect.Effect<SandboxPromise, never, R> {
+    return Effect.suspend(() => {
+      // Allocate before forking so reruns get distinct IDs and diagnostics retain creation order.
+      const id = this.nextID++
+      return Effect.map(Effect.forkIn(effect, this.scope, { startImmediately: true }), (fiber) => {
+        const promise = new SandboxPromise(fiber)
+        this.active.add(promise)
+        this.ids.set(promise, id)
+        fiber.addObserver((exit) => {
+          this.active.delete(promise)
+          if (Exit.isSuccess(exit) || Cause.hasInterruptsOnly(exit.cause) || this.observed.has(promise)) {
+            this.ids.delete(promise)
+            return
+          }
+          const failure = normalizeError(Cause.squash(exit.cause))
+          this.failures.set(id, {
+            ...failure,
+            message: `Unhandled rejection from an un-awaited promise: ${failure.message}`,
+          })
+        })
+        return promise
+      })
+    })
+  }
+
+  // Observation must be recorded when responsibility transfers, before the consumer fiber runs.
+  markObserved(promise: SandboxPromise): void {
+    this.observed.add(promise)
+    const id = this.ids.get(promise)
+    this.ids.delete(promise)
+    if (id !== undefined) this.failures.delete(id)
+  }
+
+  await(promise: SandboxPromise): Effect.Effect<Exit.Exit<unknown, unknown>> {
+    return Fiber.await(promise.fiber)
+  }
+
+  diagnostics(): Array<Diagnostic> {
+    return [...this.failures].sort(([left], [right]) => left - right).map(([, failure]) => failure)
+  }
+
+  // Re-check because a straggler can create promises before its interruption lands.
+  interrupt(): Effect.Effect<Array<Diagnostic>> {
+    const self = this
+    return Effect.gen(function* () {
+      while (self.active.size > 0) {
+        yield* Fiber.interruptAll([...self.active].map((promise) => promise.fiber))
+      }
+      return self.diagnostics()
+    })
+  }
+}
+
+export const selfResolutionError = (node?: AstNode): InterpreterRuntimeError =>
+  new InterpreterRuntimeError("Chaining cycle detected: a promise cannot resolve with itself.", node).as("TypeError")
+
+export const invokePromiseMethod = <R>(
+  runner: CallbackRunner<R>,
+  promises: PromiseRuntime<R>,
+  ref: PromiseMethodReference,
+  args: Array<unknown>,
+  node: AstNode,
+): Effect.Effect<unknown, unknown, R> => {
+  if (ref.name === "resolve") {
+    const value = args[0]
+    return value instanceof SandboxPromise ? Effect.succeed(value) : promises.create(Effect.succeed(value))
+  }
+  if (ref.name === "reject") {
+    return promises.create(Effect.fail(new ProgramThrow(args[0])))
+  }
+
+  const spread = spreadItems(args[0])
+  if (spread === undefined) {
+    return promises.create(
+      Effect.fail(
+        new InterpreterRuntimeError(
+          `Promise.${ref.name} expects an array of promises or plain values (e.g. Promise.${ref.name}(items.map((item) => tools.ns.tool(item)))).`,
+          node,
+        ).as("TypeError"),
+      ),
+    )
+  }
+  const items = Array.from(spread)
+
+  for (const item of items) {
+    if (item instanceof SandboxPromise) promises.markObserved(item)
+  }
+
+  switch (ref.name) {
+    case "all": {
+      const observations = items.map((item) =>
+        item instanceof SandboxPromise ? Effect.flatten(promises.await(item)) : Effect.succeed(item),
+      )
+      return promises.create(settleAfterTurn(Effect.all(observations, { concurrency: "unbounded" })))
+    }
+    case "allSettled": {
+      const observations = items.map((item) =>
+        item instanceof SandboxPromise ? promises.await(item) : Effect.succeed(Exit.succeed(item)),
+      )
+      return promises.create(
+        settleAfterTurn(
+          Effect.gen(function* () {
+            const outcomes: Array<unknown> = []
+            for (const observation of observations) {
+              const exit = yield* observation
+              if (Exit.isSuccess(exit)) {
+                outcomes.push(
+                  Object.assign(Object.create(null) as SafeObject, { status: "fulfilled", value: exit.value }),
+                )
+                continue
+              }
+              if (Cause.hasInterruptsOnly(exit.cause)) {
+                // Teardown interruption is not a program-level rejection.
+                return yield* Effect.failCause(exit.cause)
+              }
+              outcomes.push(
+                Object.assign(Object.create(null) as SafeObject, {
+                  status: "rejected",
+                  reason: caughtErrorValue(Cause.squash(exit.cause)),
+                }),
+              )
+            }
+            return outcomes
+          }),
+        ),
+      )
+    }
+    case "race": {
+      if (items.length === 0) {
+        return promises.create(
+          Effect.fail(
+            new InterpreterRuntimeError(
+              "Promise.race([]) would never settle; provide at least one promise or value.",
+              node,
+            ),
+          ),
+        )
+      }
+      const observations = items.map((item) =>
+        item instanceof SandboxPromise ? promises.await(item) : Effect.succeed(Exit.succeed(item)),
+      )
+      return promises.create(settleAfterTurn(Effect.flatten(Effect.raceAll(observations))))
+    }
+    case "any": {
+      const flipped = items.map((item) =>
+        item instanceof SandboxPromise
+          ? Effect.flatMap(promises.await(item), (exit) => {
+              if (Exit.isSuccess(exit)) return Effect.fail(new PromiseAnyFulfilled(exit.value))
+              if (Cause.hasInterruptsOnly(exit.cause)) return Effect.failCause(exit.cause)
+              return Effect.succeed(caughtErrorValue(Cause.squash(exit.cause)))
+            })
+          : Effect.fail(new PromiseAnyFulfilled(item)),
+      )
+      const body = Effect.all(flipped, { concurrency: "unbounded" }).pipe(
+        Effect.flatMap((reasons) =>
+          Effect.fail(new ProgramThrow(createAggregateErrorValue(reasons, "All promises were rejected"))),
+        ),
+        Effect.catch((error) =>
+          error instanceof PromiseAnyFulfilled ? Effect.succeed(error.value) : Effect.fail(error),
+        ),
+      )
+      return promises.create(settleAfterTurn(body))
+    }
+  }
+}
+
+export const invokePromiseInstanceMethod = <R>(
+  runner: CallbackRunner<R>,
+  promises: PromiseRuntime<R>,
+  ref: PromiseInstanceMethodReference,
+  args: Array<unknown>,
+  node: AstNode,
+): Effect.Effect<SandboxPromise, never, R> => {
+  const method = `Promise.prototype.${ref.name}`
+  promises.markObserved(ref.promise)
+  if (ref.name === "finally") {
+    return chainFinally(runner, promises, ref.promise, reactionHandler(args[0], method, node), method, node)
+  }
+  const onFulfilled = ref.name === "then" ? reactionHandler(args[0], method, node) : undefined
+  const onRejected = reactionHandler(ref.name === "then" ? args[1] : args[0], method, node)
+  return chainReaction(runner, promises, ref.promise, onFulfilled, onRejected, method, node)
+}
+
+export const constructPromise = <R>(
+  runner: CallbackRunner<R>,
+  promises: PromiseRuntime<R>,
+  executor: unknown,
+  node: AstNode,
+): Effect.Effect<SandboxPromise, unknown, R> => {
+  if (!(executor instanceof CodeModeFunction)) {
+    throw new InterpreterRuntimeError(
+      "new Promise(...) expects an executor function (e.g. new Promise((resolve, reject) => { ... })).",
+      node,
+    ).as("TypeError")
+  }
+  return Effect.gen(function* () {
+    const deferred = Deferred.makeUnsafe<unknown, unknown>()
+    const box: { own?: SandboxPromise } = {}
+    const promise = yield* promises.create(
+      Effect.flatMap(Deferred.await(deferred), (value) => {
+        if (!(value instanceof SandboxPromise)) return Effect.succeed(value)
+        if (value === box.own) return Effect.fail(selfResolutionError(node))
+        return runner.settlePromise(value)
+      }),
+    )
+    box.own = promise
+    const resolve = new PromiseCapabilityFunction((value) => {
+      Deferred.doneUnsafe(deferred, Exit.succeed(value))
+    })
+    const reject = new PromiseCapabilityFunction((value) => {
+      Deferred.doneUnsafe(deferred, Exit.fail(new ProgramThrow(value)))
+    })
+    const executed = yield* Effect.exit(runner.invokeFunction(executor, [resolve, reject]))
+    if (!Exit.isSuccess(executed)) {
+      if (Cause.hasInterruptsOnly(executed.cause)) return yield* Effect.failCause(executed.cause)
+      Deferred.doneUnsafe(deferred, Exit.fail(Cause.squash(executed.cause)))
+    }
+    return promise
+  })
+}
+
+// Settle one reaction turn after the deciding member, after its existing reactions.
+const settleAfterTurn = <A, E, R>(body: Effect.Effect<A, E, R>): Effect.Effect<A, E, R> =>
+  Effect.flatMap(Effect.exit(body), (exit) => Effect.andThen(Effect.yieldNow, exit))
+
+class PromiseAnyFulfilled {
+  constructor(readonly value: unknown) {}
+}
+
+type ReactionHandler = CodeModeFunction | CoercionFunction | UriFunction | PromiseCapabilityFunction
+
+const reactionHandler = (value: unknown, method: string, node: AstNode): ReactionHandler | undefined => {
+  if (
+    value instanceof CodeModeFunction ||
+    value instanceof CoercionFunction ||
+    value instanceof UriFunction ||
+    value instanceof PromiseCapabilityFunction
+  ) {
+    return value
+  }
+  if (typeofValue(value) === "function") {
+    throw new InterpreterRuntimeError(
+      `${method} handlers must be plain functions; wrap other callables in an arrow function, e.g. (value) => tools.ns.tool(value).`,
+      node,
+    )
+  }
+  return undefined
+}
+
+// Teardown bypasses handlers; settled reactions yield once so handlers never run inline.
+const reactionExit = <R>(
+  promises: PromiseRuntime<R>,
+  source: SandboxPromise,
+): Effect.Effect<Exit.Exit<unknown, unknown>, unknown, R> =>
+  Effect.gen(function* () {
+    const exit = yield* promises.await(source)
+    if (!Exit.isSuccess(exit) && Cause.hasInterruptsOnly(exit.cause)) return yield* Effect.failCause(exit.cause)
+    yield* Effect.yieldNow
+    return exit
+  })
+
+const chainReaction = <R>(
+  runner: CallbackRunner<R>,
+  promises: PromiseRuntime<R>,
+  source: SandboxPromise,
+  onFulfilled: ReactionHandler | undefined,
+  onRejected: ReactionHandler | undefined,
+  method: string,
+  node: AstNode,
+): Effect.Effect<SandboxPromise, never, R> => {
+  const box: { derived?: SandboxPromise } = {}
+  const body = Effect.gen(function* () {
+    const exit = yield* reactionExit(promises, source)
+    const handler = Exit.isSuccess(exit) ? onFulfilled : onRejected
+    if (handler === undefined) return yield* exit
+    const input = Exit.isSuccess(exit) ? exit.value : caughtErrorValue(Cause.squash(exit.cause))
+    const result = yield* applyCollectionCallback(runner, handler, method, node)([input])
+    if (result === box.derived) return yield* Effect.fail(selfResolutionError(node))
+    if (result instanceof SandboxPromise) return yield* runner.settlePromise(result)
+    return result
+  })
+  return Effect.map(promises.create(body), (derived) => {
+    box.derived = derived
+    return derived
+  })
+}
+
+const chainFinally = <R>(
+  runner: CallbackRunner<R>,
+  promises: PromiseRuntime<R>,
+  source: SandboxPromise,
+  cleanup: ReactionHandler | undefined,
+  method: string,
+  node: AstNode,
+): Effect.Effect<SandboxPromise, never, R> =>
+  promises.create(
+    Effect.gen(function* () {
+      const exit = yield* reactionExit(promises, source)
+      if (cleanup !== undefined) {
+        const result = yield* applyCollectionCallback(runner, cleanup, method, node)([])
+        if (result instanceof SandboxPromise) yield* runner.settlePromise(result)
+      }
+      return yield* exit
+    }),
+  )

+ 99 - 0
packages/codemode/src/interpreter/references.ts

@@ -0,0 +1,99 @@
+import {
+  type AstNode,
+  CodeModeFunction,
+  CoercionFunction,
+  ErrorConstructorReference,
+  GlobalMethodReference,
+  GlobalNamespace,
+  InterpreterRuntimeError,
+  IntrinsicReference,
+  PromiseCapabilityFunction,
+  PromiseInstanceMethodReference,
+  PromiseMethodReference,
+  PromiseNamespace,
+  SearchFunction,
+  UriFunction,
+} from "./model.js"
+import { ToolReference } from "../tool-runtime.js"
+import { isSandboxValue, SandboxPromise } from "../values.js"
+
+export const isRuntimeReference = (value: unknown): boolean =>
+  value instanceof CodeModeFunction ||
+  value instanceof ToolReference ||
+  value instanceof IntrinsicReference ||
+  value instanceof GlobalNamespace ||
+  value instanceof GlobalMethodReference ||
+  value instanceof PromiseNamespace ||
+  value instanceof PromiseMethodReference ||
+  value instanceof PromiseInstanceMethodReference ||
+  value instanceof SandboxPromise ||
+  value instanceof CoercionFunction ||
+  value instanceof UriFunction ||
+  value instanceof SearchFunction ||
+  value instanceof PromiseCapabilityFunction ||
+  value instanceof ErrorConstructorReference ||
+  isSandboxValue(value)
+
+export const containsRuntimeReference = (value: unknown, seen = new Set<object>()): boolean => {
+  if (isRuntimeReference(value)) return true
+  if (value === null || typeof value !== "object") return false
+  if (seen.has(value)) return false
+  seen.add(value)
+  const contains = Array.isArray(value)
+    ? value.some((item) => containsRuntimeReference(item, seen))
+    : Object.values(value).some((item) => containsRuntimeReference(item, seen))
+  seen.delete(value)
+  return contains
+}
+
+// Sandbox values are data here, not opaque interpreter references.
+export const containsOpaqueReference = (value: unknown, seen = new Set<object>()): boolean => {
+  if (isSandboxValue(value)) return false
+  if (isRuntimeReference(value)) return true
+  if (value === null || typeof value !== "object") return false
+  if (seen.has(value)) return false
+  seen.add(value)
+  const contains = Array.isArray(value)
+    ? value.some((item) => containsOpaqueReference(item, seen))
+    : Object.values(value).some((item) => containsOpaqueReference(item, seen))
+  seen.delete(value)
+  return contains
+}
+
+// Reject cycles before mutation so later boundary walks remain safe.
+export const rejectCircularInsertion = (
+  container: object,
+  value: unknown,
+  label: string,
+  node: AstNode,
+  seen = new Set<object>(),
+): void => {
+  if (value === container)
+    throw new InterpreterRuntimeError(`${label} contains a circular value.`, node, "InvalidDataValue")
+  if (value === null || typeof value !== "object" || isRuntimeReference(value) || seen.has(value)) return
+  seen.add(value)
+  const items = Array.isArray(value) ? value : Object.values(value)
+  for (const item of items) rejectCircularInsertion(container, item, label, node, seen)
+  seen.delete(value)
+}
+
+export const typeofValue = (value: unknown): string => {
+  if (
+    value instanceof CodeModeFunction ||
+    value instanceof CoercionFunction ||
+    value instanceof IntrinsicReference ||
+    value instanceof GlobalMethodReference ||
+    value instanceof PromiseMethodReference ||
+    value instanceof PromiseInstanceMethodReference ||
+    value instanceof PromiseNamespace ||
+    value instanceof PromiseCapabilityFunction ||
+    value instanceof ErrorConstructorReference
+  )
+    return "function"
+  if (value instanceof UriFunction || value instanceof SearchFunction) return "function"
+  if (value instanceof ToolReference) return value.path.length > 0 ? "function" : "object"
+  if (value instanceof GlobalNamespace) {
+    return value.name === "Math" || value.name === "JSON" || value.name === "console" ? "object" : "function"
+  }
+  return typeof value
+}

File diff suppressed because it is too large
+ 54 - 678
packages/codemode/src/interpreter/runtime.ts


+ 84 - 0
packages/codemode/src/interpreter/scope.ts

@@ -0,0 +1,84 @@
+import { type AstNode, type Binding, InterpreterRuntimeError } from "./model.js"
+
+export class ScopeStack {
+  private readonly scopes: Array<Map<string, Binding>>
+
+  constructor(scopes: Array<Map<string, Binding>>) {
+    this.scopes = scopes
+  }
+
+  declare(name: string, value: unknown, mutable: boolean, node: AstNode): void {
+    const scope = this.current()
+
+    const existing = scope.get(name)
+    if (existing && existing.initialized !== false) {
+      throw new InterpreterRuntimeError(`Identifier '${name}' has already been declared.`, node)
+    }
+
+    scope.set(name, { mutable, value, initialized: true })
+  }
+
+  get(name: string, node: AstNode): unknown {
+    const binding = this.resolve(name)
+
+    if (!binding) {
+      throw new InterpreterRuntimeError(`Unknown identifier '${name}'.`, node).as("ReferenceError")
+    }
+
+    if (binding.initialized === false) {
+      throw new InterpreterRuntimeError(`Cannot access '${name}' before initialization.`, node).as("ReferenceError")
+    }
+
+    return binding.value
+  }
+
+  set(name: string, value: unknown, node: AstNode): unknown {
+    const binding = this.resolve(name)
+
+    if (!binding) {
+      throw new InterpreterRuntimeError(`Unknown identifier '${name}'.`, node).as("ReferenceError")
+    }
+
+    if (!binding.mutable) {
+      throw new InterpreterRuntimeError(`Cannot assign to constant '${name}'.`, node).as("TypeError")
+    }
+
+    binding.value = value
+    return value
+  }
+
+  resolve(name: string): Binding | undefined {
+    for (let index = this.scopes.length - 1; index >= 0; index -= 1) {
+      const scope = this.scopes[index]
+      const binding = scope?.get(name)
+
+      if (binding) {
+        return binding
+      }
+    }
+
+    return undefined
+  }
+
+  current(): Map<string, Binding> {
+    const scope = this.scopes[this.scopes.length - 1]
+
+    if (!scope) {
+      throw new InterpreterRuntimeError("Interpreter scope stack is empty.")
+    }
+
+    return scope
+  }
+
+  push(scope: Map<string, Binding> = new Map()): void {
+    this.scopes.push(scope)
+  }
+
+  pop(): void {
+    this.scopes.pop()
+  }
+
+  capture(): Array<Map<string, Binding>> {
+    return this.scopes.slice()
+  }
+}

+ 2 - 4
packages/codemode/src/openapi/index.ts

@@ -31,10 +31,8 @@ export type {
 } from "./types.js"
 } from "./types.js"
 
 
 /**
 /**
- * Builds a CodeMode tool subtree from an OpenAPI 3.x document, one tool per
- * operation. Auth is resolved host-side via `auth.resolve` and never
- * model-visible. Tools require `HttpClient.HttpClient`; unrepresentable
- * operations land in `skipped`.
+ * Builds one CodeMode tool per representable OpenAPI 3.x operation. Auth remains host-side,
+ * tools require `HttpClient.HttpClient`, and unrepresentable operations land in `skipped`.
  */
  */
 export const fromSpec = (options: Options): Result => {
 export const fromSpec = (options: Options): Result => {
   const document = options.spec
   const document = options.spec

+ 3 - 6
packages/codemode/src/openapi/runtime.ts

@@ -56,7 +56,7 @@ const buildRequest = (
   input: Readonly<Record<string, unknown>>,
   input: Readonly<Record<string, unknown>>,
 ): Effect.Effect<HttpClientRequest.HttpClientRequest, ToolError> =>
 ): Effect.Effect<HttpClientRequest.HttpClientRequest, ToolError> =>
   Effect.gen(function* () {
   Effect.gen(function* () {
-    // Validate every model-controlled value before auth resolution, which may refresh tokens.
+    // Validate model input before auth resolution can refresh credentials.
     const url = buildUrl(plan, input)
     const url = buildUrl(plan, input)
     if (url instanceof ToolError) return yield* Effect.fail(url)
     if (url instanceof ToolError) return yield* Effect.fail(url)
     const missing = plan.fields.find(
     const missing = plan.fields.find(
@@ -77,7 +77,6 @@ const buildRequest = (
       request = serialized
       request = serialized
     }
     }
 
 
-    // Host headers first, then declared header parameters.
     request = HttpClientRequest.setHeaders(request, plan.headers)
     request = HttpClientRequest.setHeaders(request, plan.headers)
     for (const field of plan.fields) {
     for (const field of plan.fields) {
       if (field.location !== "header") continue
       if (field.location !== "header") continue
@@ -169,7 +168,7 @@ const applyCredentials = (
       continue
       continue
     }
     }
     if (credential.type === "basic") {
     if (credential.type === "basic") {
-      // Buffer instead of btoa: btoa throws on non-Latin-1 credentials.
+      // Basic auth credentials are UTF-8; btoa rejects non-Latin-1 input.
       const duplicate = add(
       const duplicate = add(
         "header",
         "header",
         "authorization",
         "authorization",
@@ -183,7 +182,6 @@ const applyCredentials = (
       if (duplicate !== undefined) return duplicate
       if (duplicate !== undefined) return duplicate
       continue
       continue
     }
     }
-    // apiKey: the carrier comes from the scheme declaration.
     if (definition.type !== "apiKey") {
     if (definition.type !== "apiKey") {
       return toolError(
       return toolError(
         `Security scheme '${name}' is not an apiKey scheme; resolve a bearer, basic, or header credential for it.`,
         `Security scheme '${name}' is not an apiKey scheme; resolve a bearer, basic, or header credential for it.`,
@@ -212,8 +210,7 @@ const buildUrl = (plan: Plan, input: Readonly<Record<string, unknown>>): string
       ),
       ),
     )
     )
     if (fieldValue instanceof ToolError) return fieldValue
     if (fieldValue instanceof ToolError) return fieldValue
-    // '.'/'..' survive encoding and URL normalization collapses them, letting a
-    // model-supplied value retarget the request to a different endpoint.
+    // URL normalization collapses encoded `.` and `..`, which could retarget the request.
     if (fieldValue === "" || fieldValue === "." || fieldValue === "..") {
     if (fieldValue === "" || fieldValue === "." || fieldValue === "..") {
       return toolError(`Invalid path parameter '${field.inputName}'.`)
       return toolError(`Invalid path parameter '${field.inputName}'.`)
     }
     }

+ 2 - 3
packages/codemode/src/openapi/spec.ts

@@ -23,8 +23,7 @@ const asArray = (value: unknown): ReadonlyArray<unknown> => (Array.isArray(value
 export const nonEmptyString = (value: unknown): string | undefined =>
 export const nonEmptyString = (value: unknown): string | undefined =>
   typeof value === "string" && value !== "" ? value : undefined
   typeof value === "string" && value !== "" ? value : undefined
 
 
-// Guards record lookups keyed by spec- or model-controlled names against
-// prototype-inherited values (e.g. a parameter named `toString`).
+// Spec- and model-controlled keys must not resolve inherited properties.
 export const own = <T>(record: Readonly<Record<string, T>>, key: string): T | undefined =>
 export const own = <T>(record: Readonly<Record<string, T>>, key: string): T | undefined =>
   Object.hasOwn(record, key) ? record[key] : undefined
   Object.hasOwn(record, key) ? record[key] : undefined
 
 
@@ -106,7 +105,7 @@ const operationParameters = (
   pathItem: Record<string, unknown>,
   pathItem: Record<string, unknown>,
   operation: Record<string, unknown>,
   operation: Record<string, unknown>,
 ): Parsed<ReadonlyArray<PlannedField>> => {
 ): Parsed<ReadonlyArray<PlannedField>> => {
-  // Operation-level parameters override path-level ones sharing (location, name).
+  // OpenAPI operation parameters override path parameters with the same location and name.
   const declared = new Map<
   const declared = new Map<
     string,
     string,
     { readonly name: string; readonly location: string; readonly parameter: Record<string, unknown> }
     { readonly name: string; readonly location: string; readonly parameter: Record<string, unknown> }

+ 3 - 9
packages/codemode/src/openapi/types.ts

@@ -22,9 +22,8 @@ export type SecurityScheme =
   | { readonly type: "openIdConnect" }
   | { readonly type: "openIdConnect" }
 
 
 /**
 /**
- * Credential material returned by a host auth resolver. The carrier for `apiKey`
- * comes from the scheme definition, not the credential. `header` is the escape
- * hatch for nonstandard schemes.
+ * Credential material returned by a host auth resolver. `apiKey` uses the scheme's carrier;
+ * `header` supports nonstandard schemes.
  */
  */
 export type Credential =
 export type Credential =
   | { readonly type: "bearer"; readonly token: string }
   | { readonly type: "bearer"; readonly token: string }
@@ -33,9 +32,7 @@ export type Credential =
   | { readonly type: "header"; readonly name: string; readonly value: string }
   | { readonly type: "header"; readonly name: string; readonly value: string }
 
 
 /**
 /**
- * Resolves credential material for one named security scheme at call time.
- * `undefined` means unavailable, try the next OR alternative; a failure aborts
- * the call rather than falling through.
+ * Resolves credentials at call time. `undefined` tries the next OR alternative; failure aborts.
  */
  */
 export type AuthResolver = (context: {
 export type AuthResolver = (context: {
   readonly name: string
   readonly name: string
@@ -74,9 +71,7 @@ export type Parsed<T> = { readonly ok: true; readonly value: T } | { readonly ok
 export type InputLocation = "path" | "query" | "header" | "body"
 export type InputLocation = "path" | "query" | "header" | "body"
 
 
 export type InputField = {
 export type InputField = {
-  /** Model-visible field name after cross-location collision handling. */
   readonly inputName: string
   readonly inputName: string
-  /** Original parameter or body-property name used on the wire. */
   readonly name: string
   readonly name: string
   readonly location: InputLocation
   readonly location: InputLocation
   readonly required: boolean
   readonly required: boolean
@@ -92,7 +87,6 @@ export type OperationInput = {
   readonly body: Body | undefined
   readonly body: Body | undefined
 }
 }
 
 
-/** One OR alternative: scheme name -> required scopes. Empty object = unauthenticated is acceptable. */
 export type SecurityRequirement = Readonly<Record<string, ReadonlyArray<string>>>
 export type SecurityRequirement = Readonly<Record<string, ReadonlyArray<string>>>
 
 
 export type Plan = {
 export type Plan = {

+ 120 - 2
packages/codemode/src/stdlib/console.ts

@@ -1,4 +1,122 @@
+import { containsOpaqueReference, containsRuntimeReference, isRuntimeReference } from "../interpreter/references.js"
+import { copyIn, copyOut } from "../tool-runtime.js"
+import {
+  isSandboxValue,
+  SandboxDate,
+  SandboxMap,
+  SandboxPromise,
+  SandboxRegExp,
+  SandboxSet,
+  SandboxURL,
+  SandboxURLSearchParams,
+} from "../values.js"
+import { boundedData, coerceToString } from "./value.js"
+
 export const consoleMethods = new Set(["log", "info", "debug", "warn", "error", "dir", "table"])
 export const consoleMethods = new Set(["log", "info", "debug", "warn", "error", "dir", "table"])
 
 
-/** Console formatting recursion ceiling; deeper values render as "...". */
-export const MAX_CONSOLE_DEPTH = 32
+const MAX_CONSOLE_DEPTH = 32
+
+export const formatConsoleMessage = (name: string, args: Array<unknown>): string => {
+  if (name === "dir") return args.length === 0 ? "undefined" : formatConsoleArgument(args[0])
+  if (name === "table") return formatConsoleTable(args[0], args[1])
+  const prefix = name === "warn" ? "[warn] " : name === "error" ? "[error] " : name === "debug" ? "[debug] " : ""
+  return `${prefix}${args.map((arg) => formatConsoleArgument(arg)).join(" ")}`
+}
+
+const formatConsoleArgument = (value: unknown): string => {
+  if (value === undefined) return "undefined"
+  if (typeof value === "string") return value
+  return formatConsoleValue(value, new Set(), 0)
+}
+
+const formatConsoleValue = (value: unknown, seen: Set<object>, depth: number): string => {
+  if (value === null || value === undefined) return "null"
+  if (typeof value === "string") return JSON.stringify(value)
+  if (typeof value === "number" || typeof value === "boolean") return String(value)
+  if (typeof value !== "object") return String(value)
+  if (value instanceof SandboxPromise) return "[Promise (await it to get its value)]"
+  if (value instanceof SandboxDate) return coerceToString(value)
+  if (value instanceof SandboxRegExp) return coerceToString(value)
+  if (value instanceof SandboxURL) return coerceToString(value)
+  if (value instanceof SandboxURLSearchParams) return coerceToString(value)
+  if (depth > MAX_CONSOLE_DEPTH) return "..."
+  if (seen.has(value)) return "[Circular]"
+  if (value instanceof SandboxMap) {
+    seen.add(value)
+    try {
+      const entries = Array.from(value.map.entries(), ([key, item]): Array<unknown> => [key, item])
+      return `Map(${value.map.size}) ${formatConsoleValue(entries, seen, depth + 1)}`
+    } finally {
+      seen.delete(value)
+    }
+  }
+  if (value instanceof SandboxSet) {
+    seen.add(value)
+    try {
+      return `Set(${value.set.size}) ${formatConsoleValue(Array.from(value.set.values()), seen, depth + 1)}`
+    } finally {
+      seen.delete(value)
+    }
+  }
+  if (isRuntimeReference(value)) return "[CodeMode reference]"
+  seen.add(value)
+  try {
+    if (Array.isArray(value)) {
+      return `[${value.map((item) => formatConsoleValue(item, seen, depth + 1)).join(",")}]`
+    }
+    return `{${Object.entries(value)
+      .map(([key, item]) => `${JSON.stringify(key)}:${formatConsoleValue(item, seen, depth + 1)}`)
+      .join(",")}}`
+  } finally {
+    seen.delete(value)
+  }
+}
+
+const formatConsoleTable = (value: unknown, columnsArgument: unknown): string => {
+  if (value === undefined) return "undefined"
+  if (containsOpaqueReference(value)) return "[CodeMode reference]"
+  const data = boundedData(value, "console.table argument")
+  const columns = consoleTableColumns(columnsArgument)
+  const rows = consoleTableRows(data, columns)
+  const keys = columns ?? Array.from(new Set(rows.flatMap((row) => Object.keys(row.values))))
+  const header = ["(index)", ...keys].join("\t")
+  return [
+    header,
+    ...rows.map((row) => [row.index, ...keys.map((key) => formatConsoleTableCell(row.values[key]))].join("\t")),
+  ].join("\n")
+}
+
+const consoleTableColumns = (value: unknown): ReadonlyArray<string> | undefined => {
+  if (value === undefined) return undefined
+  if (containsRuntimeReference(value)) return undefined
+  const columns = copyOut(copyIn(value, "console.table columns"), true)
+  return Array.isArray(columns) ? columns.map((column) => String(column)) : undefined
+}
+
+const consoleTableRows = (
+  data: unknown,
+  columns: ReadonlyArray<string> | undefined,
+): Array<{ readonly index: string; readonly values: Record<string, unknown> }> => {
+  if (Array.isArray(data)) {
+    return data.map((item, index) => ({ index: String(index), values: consoleTableValues(item, columns) }))
+  }
+  if (data !== null && typeof data === "object" && !isSandboxValue(data)) {
+    return Object.entries(data).map(([index, item]) => ({ index, values: consoleTableValues(item, columns) }))
+  }
+  return [{ index: "0", values: { Value: data } }]
+}
+
+const consoleTableValues = (value: unknown, columns: ReadonlyArray<string> | undefined): Record<string, unknown> => {
+  if (value !== null && typeof value === "object" && !Array.isArray(value) && !isSandboxValue(value)) {
+    const source = value as Record<string, unknown>
+    if (columns !== undefined) return Object.fromEntries(columns.map((column) => [column, source[column]]))
+    return Object.fromEntries(Object.entries(source))
+  }
+  return { Value: value }
+}
+
+const formatConsoleTableCell = (value: unknown): string => {
+  if (value === undefined) return ""
+  if (typeof value === "string") return value
+  return formatConsoleValue(value, new Set(), 0)
+}

+ 0 - 1
packages/codemode/src/stdlib/promise.ts

@@ -2,5 +2,4 @@ import type { PromiseMethodName } from "../interpreter/model.js"
 
 
 export const promiseStatics = new Set<PromiseMethodName>(["all", "allSettled", "race", "any", "resolve", "reject"])
 export const promiseStatics = new Set<PromiseMethodName>(["all", "allSettled", "race", "any", "resolve", "reject"])
 
 
-/** Maximum number of eagerly forked tool calls that may run concurrently. */
 export const TOOL_CALL_CONCURRENCY = 8
 export const TOOL_CALL_CONCURRENCY = 8

+ 0 - 3
packages/codemode/src/stdlib/string.ts

@@ -4,12 +4,9 @@ export const stringMethods = new Set([
   "trim",
   "trim",
   "trimStart",
   "trimStart",
   "trimEnd",
   "trimEnd",
-  "trimLeft",
-  "trimRight",
   "split",
   "split",
   "slice",
   "slice",
   "substring",
   "substring",
-  "substr",
   "includes",
   "includes",
   "startsWith",
   "startsWith",
   "endsWith",
   "endsWith",

+ 4 - 108
packages/codemode/src/tool-runtime.ts

@@ -44,36 +44,30 @@ type ServicesOf<Tools, Depth extends ReadonlyArray<unknown>> = Depth["length"] e
           : ServicesOf<Tools[keyof Tools], [...Depth, unknown]>
           : ServicesOf<Tools[keyof Tools], [...Depth, unknown]>
         : never
         : never
 
 
-/** Minimal audit record retained for each admitted tool call. */
 export type ToolCall = {
 export type ToolCall = {
   readonly name: string
   readonly name: string
 }
 }
 
 
-/** Decoded tool call observed immediately before tool execution. */
 export type ToolCallStarted = {
 export type ToolCallStarted = {
   readonly index: number
   readonly index: number
   readonly name: string
   readonly name: string
   readonly input: unknown
   readonly input: unknown
 }
 }
 
 
-/** Completed tool call observed immediately after tool execution settles. */
 export type ToolCallEnded = {
 export type ToolCallEnded = {
   readonly index: number
   readonly index: number
   readonly name: string
   readonly name: string
   readonly input: unknown
   readonly input: unknown
   readonly durationMs: number
   readonly durationMs: number
   readonly outcome: "success" | "failure"
   readonly outcome: "success" | "failure"
-  /** Model-safe failure message; present only when `outcome` is `"failure"`. */
   readonly message?: string
   readonly message?: string
 }
 }
 
 
-/** Non-throwing observation hooks fired around each admitted tool call. */
 export type ToolCallHooks<R = never> = {
 export type ToolCallHooks<R = never> = {
   readonly onToolCallStart?: ((call: ToolCallStarted) => Effect.Effect<void, never, R>) | undefined
   readonly onToolCallStart?: ((call: ToolCallStarted) => Effect.Effect<void, never, R>) | undefined
   readonly onToolCallEnd?: ((call: ToolCallEnded) => Effect.Effect<void, never, R>) | undefined
   readonly onToolCallEnd?: ((call: ToolCallEnded) => Effect.Effect<void, never, R>) | undefined
 }
 }
 
 
-/** Model-visible description of one schema-backed tool. */
 export type ToolDescription = {
 export type ToolDescription = {
   readonly path: string
   readonly path: string
   readonly description: string
   readonly description: string
@@ -113,11 +107,6 @@ export class ToolReference {
   constructor(readonly path: ReadonlyArray<string>) {}
   constructor(readonly path: ReadonlyArray<string>) {}
 }
 }
 
 
-/**
- * Maximum nesting depth for values crossing a data boundary. Fixed (not a configurable
- * limit) purely because it produces a clearer diagnostic than a native stack-overflow
- * RangeError would.
- */
 const MAX_VALUE_DEPTH = 32
 const MAX_VALUE_DEPTH = 32
 
 
 export class ToolRuntimeError extends Error {
 export class ToolRuntimeError extends Error {
@@ -152,21 +141,7 @@ const blockedMemberNames = new Set(["__proto__", "constructor", "prototype"])
 
 
 export const isBlockedMember = (name: string): boolean => blockedMemberNames.has(name)
 export const isBlockedMember = (name: string): boolean => blockedMemberNames.has(name)
 
 
-/**
- * Validates and copies a value against the plain-data contract (depth, circularity, plain
- * objects only, blocked properties, data-only leaves).
- *
- * Two modes share the walk:
- * - **Boundary** (`preserveSandboxValues` false, the default): the host<->sandbox boundary -
- *   final results, tool-call arguments, `JSON.stringify`. Sandbox value types serialize
- *   exactly as JSON.stringify would: Date/URL -> strings, the remaining value types -> {}.
- * - **Intra-sandbox checkpoint** (`preserveSandboxValues` true; see `boundedData` in
- *   codemode.ts): standard-library value instances pass through untouched (treated as leaves,
- *   contents not walked), so values flowing through `Object.*` helpers, coercion inputs, and
- *   other in-sandbox checkpoints stay fully usable (`.getTime()`, `.has()`, ...).
- *
- * Both modes reject un-awaited promises with an await-hinting diagnostic.
- */
+// Checkpoint mode preserves sandbox values; boundary mode JSON-normalizes them.
 export const copyIn = (value: unknown, label: string, preserveSandboxValues = false): unknown =>
 export const copyIn = (value: unknown, label: string, preserveSandboxValues = false): unknown =>
   copyBounded(value, label, 0, new Set(), preserveSandboxValues)
   copyBounded(value, label, 0, new Set(), preserveSandboxValues)
 
 
@@ -185,10 +160,6 @@ const copyBounded = (
     value === undefined ||
     value === undefined ||
     typeof value === "string" ||
     typeof value === "string" ||
     typeof value === "boolean" ||
     typeof value === "boolean" ||
-    // NaN/Infinity are allowed to exist as in-sandbox intermediates (matching real JS and a real
-    // engine) so defensive guards like `Number.isNaN(x)` / `parseInt(x) || 0` can run. They are
-    // normalized to `null` when the value leaves the sandbox - see copyOut - exactly as
-    // JSON.stringify already does at any tool boundary.
     typeof value === "number"
     typeof value === "number"
   ) {
   ) {
     return value
     return value
@@ -198,8 +169,6 @@ const copyBounded = (
     throw new ToolRuntimeError("InvalidDataValue", `${label} must contain data only.`)
     throw new ToolRuntimeError("InvalidDataValue", `${label} must contain data only.`)
   }
   }
 
 
-  // An un-awaited promise never crosses a data checkpoint as `{}`; the diagnostic tells the
-  // model exactly how to fix the program instead.
   if (value instanceof SandboxPromise) {
   if (value instanceof SandboxPromise) {
     throw new ToolRuntimeError(
     throw new ToolRuntimeError(
       "InvalidDataValue",
       "InvalidDataValue",
@@ -208,9 +177,6 @@ const copyBounded = (
   }
   }
 
 
   if (preserveSandboxValues) {
   if (preserveSandboxValues) {
-    // Intra-sandbox checkpoints keep sandbox value instances alive as leaves; their contents
-    // are never walked here (Map/Set members are validated where mutation happens, and the
-    // real boundary still serializes them below).
     if (
     if (
       value instanceof SandboxDate ||
       value instanceof SandboxDate ||
       value instanceof SandboxRegExp ||
       value instanceof SandboxRegExp ||
@@ -221,8 +187,6 @@ const copyBounded = (
     ) {
     ) {
       return value
       return value
     }
     }
-    // Host instances cannot normally reach an intra-sandbox checkpoint (tool results cross
-    // the boundary first), but wrap them defensively rather than degrading to JSON forms.
     if (value instanceof Date) return new SandboxDate(value.getTime())
     if (value instanceof Date) return new SandboxDate(value.getTime())
     if (value instanceof RegExp) return new SandboxRegExp(value.source, value.flags)
     if (value instanceof RegExp) return new SandboxRegExp(value.source, value.flags)
     if (value instanceof Map) {
     if (value instanceof Map) {
@@ -241,9 +205,6 @@ const copyBounded = (
     if (value instanceof URLSearchParams) return new SandboxURLSearchParams(new URLSearchParams(value))
     if (value instanceof URLSearchParams) return new SandboxURLSearchParams(new URLSearchParams(value))
   }
   }
 
 
-  // Sandbox value types (and their host counterparts, which a host tool may legitimately
-  // return) serialize exactly as JSON.stringify would at the data boundary: Date/URL use
-  // toJSON(), while RegExp/Map/Set/URLSearchParams have no JSON form beyond {}.
   if (value instanceof SandboxDate) {
   if (value instanceof SandboxDate) {
     return Number.isFinite(value.time) ? new Date(value.time).toISOString() : null
     return Number.isFinite(value.time) ? new Date(value.time).toISOString() : null
   }
   }
@@ -274,7 +235,7 @@ const copyBounded = (
   if (Array.isArray(value)) {
   if (Array.isArray(value)) {
     const copied = value.map((item) => copyBounded(item, label, depth + 1, seen, preserveSandboxValues))
     const copied = value.map((item) => copyBounded(item, label, depth + 1, seen, preserveSandboxValues))
     if (preserveSandboxValues) {
     if (preserveSandboxValues) {
-      // Array metadata is not serialized, but intra-sandbox copies must retain it.
+      // Checkpoint copies retain array metadata that boundary copies omit.
       for (const [key, item] of Object.entries(value)) {
       for (const [key, item] of Object.entries(value)) {
         if (Object.hasOwn(copied, key)) continue
         if (Object.hasOwn(copied, key)) continue
         if (isBlockedMember(key)) {
         if (isBlockedMember(key)) {
@@ -305,9 +266,6 @@ const copyBounded = (
 
 
 export const copyOut = (value: unknown, undefinedAsNull = false): unknown => {
 export const copyOut = (value: unknown, undefinedAsNull = false): unknown => {
   if (value === undefined && undefinedAsNull) return null
   if (value === undefined && undefinedAsNull) return null
-  // Normalize non-finite numbers to null as the value crosses out of the sandbox (final return
-  // and tool-call arguments both funnel through here), matching JSON semantics - NaN/Infinity
-  // have no JSON representation, so JSON.stringify would produce null anyway.
   if (typeof value === "number" && !Number.isFinite(value)) {
   if (typeof value === "number" && !Number.isFinite(value)) {
     return null
     return null
   }
   }
@@ -353,18 +311,10 @@ export type DiscoveryPlan = {
 
 
 export type SearchEntry = {
 export type SearchEntry = {
   readonly description: ToolDescription
   readonly description: ToolDescription
-  /** Top-level namespace (first path segment), matched by the search `namespace` option. */
   readonly namespace: string
   readonly namespace: string
-  /** Lowercased path + description + input property names/descriptions, for substring matching. */
   readonly searchText: string
   readonly searchText: string
 }
 }
 
 
-/**
- * Split a query into lowercased search terms. camelCase boundaries are split
- * (`resolveLibrary` -> `resolve library`) and every non-alphanumeric character is a
- * separator, so `resolve-library-id`, `resolveLibraryId`, and `resolve library id` all
- * tokenize alike. Empties and the `*` wildcard are dropped.
- */
 const tokenize = (query: string): Array<string> =>
 const tokenize = (query: string): Array<string> =>
   query
   query
     .replace(/([a-z0-9])([A-Z])/g, "$1 $2")
     .replace(/([a-z0-9])([A-Z])/g, "$1 $2")
@@ -372,13 +322,6 @@ const tokenize = (query: string): Array<string> =>
     .split(/[^a-z0-9]+/)
     .split(/[^a-z0-9]+/)
     .filter((term) => term.length > 0 && term !== "*")
     .filter((term) => term.length > 0 && term !== "*")
 
 
-/**
- * A term plus its naive singular variants (trailing "s"/"es" stripped), so a plural
- * query term ("issues") still matches indexed text that only carries the singular
- * ("issue"). Matching is one-directional substring containment, so the variants are
- * needed only on the query side; scoring weights are unchanged - each field check
- * passes when ANY form matches.
- */
 const termForms = (term: string): Array<string> => {
 const termForms = (term: string): Array<string> => {
   const forms = [term]
   const forms = [term]
   if (term.endsWith("es") && term.length > 3) forms.push(term.slice(0, -2))
   if (term.endsWith("es") && term.length > 3) forms.push(term.slice(0, -2))
@@ -400,8 +343,6 @@ const makeSearchTool = (searchIndex: ReadonlyArray<SearchEntry>): Definition =>
         request.namespace === undefined
         request.namespace === undefined
           ? searchIndex
           ? searchIndex
           : searchIndex.filter((entry) => entry.namespace === request.namespace)
           : searchIndex.filter((entry) => entry.namespace === request.namespace)
-      // A query that names one tool path exactly (canonical path or rendered JavaScript
-      // expression) is a lookup, not a search: return that tool alone.
       const trimmed = query.trim()
       const trimmed = query.trim()
       const pathQuery = trimmed.startsWith("tools.") ? trimmed.slice("tools.".length) : trimmed
       const pathQuery = trimmed.startsWith("tools.") ? trimmed.slice("tools.".length) : trimmed
       const exact =
       const exact =
@@ -411,9 +352,6 @@ const makeSearchTool = (searchIndex: ReadonlyArray<SearchEntry>): Definition =>
               (entry) => entry.description.path === pathQuery || toolExpression(entry.description.path) === trimmed,
               (entry) => entry.description.path === pathQuery || toolExpression(entry.description.path) === trimmed,
             )
             )
       const terms = tokenize(query).map(termForms)
       const terms = tokenize(query).map(termForms)
-      // Additive field-weighted scoring, summed across terms: exact path or path segment
-      // (20) > path substring (8) > description substring (4) > any searchable text,
-      // including input parameter names and descriptions (2).
       const ranked =
       const ranked =
         exact !== undefined
         exact !== undefined
           ? [exact]
           ? [exact]
@@ -451,15 +389,12 @@ const makeSearchTool = (searchIndex: ReadonlyArray<SearchEntry>): Definition =>
     }),
     }),
 })
 })
 
 
-// The built-in `search` is a synchronous global function, not a tool-tree entry, so its
-// advertised signature is rendered by hand instead of through `describeDefinition`.
 const searchSignature = (() => {
 const searchSignature = (() => {
   const definition = makeSearchTool([])
   const definition = makeSearchTool([])
   return `search(input: ${inputTypeScript(definition, true)}): ${outputTypeScript(definition, true)}`
   return `search(input: ${inputTypeScript(definition, true)}): ${outputTypeScript(definition, true)}`
 })()
 })()
 
 
 const catalogLine = (tool: ToolDescription) => {
 const catalogLine = (tool: ToolDescription) => {
-  // Keep the tool description concise; the full schema documentation remains in the signature.
   const line = tool.description.split("\n", 1)[0]!.trim()
   const line = tool.description.split("\n", 1)[0]!.trim()
   const description = line.length > 120 ? line.slice(0, 119) + "..." : line
   const description = line.length > 120 ? line.slice(0, 119) + "..." : line
   return description === "" ? `  - ${tool.signature}` : `  - ${tool.signature} // ${description}`
   return description === "" ? `  - ${tool.signature}` : `  - ${tool.signature} // ${description}`
@@ -479,21 +414,10 @@ const toSearchEntry = <R>(path: string, definition: Definition<R>, description:
     .toLowerCase(),
     .toLowerCase(),
 })
 })
 
 
-/** The runtime search index over every described tool. Search is always registered. */
 export const searchIndex = <R>(tools: HostTools<R>): ReadonlyArray<SearchEntry> =>
 export const searchIndex = <R>(tools: HostTools<R>): ReadonlyArray<SearchEntry> =>
   visibleDefinitions(tools).map(({ path, definition, description }) => toSearchEntry(path, definition, description))
   visibleDefinitions(tools).map(({ path, definition, description }) => toSearchEntry(path, definition, description))
 
 
-/**
- * Budgeted catalog: every namespace is always listed with its tool count; full call
- * signatures are inlined against the `catalogBudget` (estimated tokens,
- * chars/4) round-robin across namespaces - in each round (namespaces alphabetical), every
- * namespace still holding un-inlined tools attempts to place its next-cheapest line, and
- * a namespace whose next line does not fit is done while the others keep going - so every
- * namespace gets some representation before any namespace gets everything. The section
- * states exactly how comprehensive it is - overall (COMPLETE vs PARTIAL) and per
- * namespace. Namespace stub lines are never budgeted: every namespace appears with its
- * tool count even at budget 0.
- */
+// Budget signatures round-robin so every namespace remains visible.
 export const prepare = <R>(tools: HostTools<R>, catalogBudget = defaultCatalogBudget): DiscoveryPlan => {
 export const prepare = <R>(tools: HostTools<R>, catalogBudget = defaultCatalogBudget): DiscoveryPlan => {
   if (!Number.isSafeInteger(catalogBudget) || catalogBudget < 0) {
   if (!Number.isSafeInteger(catalogBudget) || catalogBudget < 0) {
     throw new RangeError("discovery.catalogBudget must be a non-negative safe integer")
     throw new RangeError("discovery.catalogBudget must be a non-negative safe integer")
@@ -510,12 +434,6 @@ export const prepare = <R>(tools: HostTools<R>, catalogBudget = defaultCatalogBu
   }
   }
   const ordered = [...namespaces].sort(([left], [right]) => left.localeCompare(right))
   const ordered = [...namespaces].sort(([left], [right]) => left.localeCompare(right))
 
 
-  // Select which signatures fit the budget before emitting, so the list can state
-  // exactly how comprehensive it is. Round-robin fairness: in each round (namespaces
-  // alphabetical), every namespace still holding un-inlined tools tries to place its
-  // next-cheapest line against the shared budget; a namespace whose next line does not
-  // fit is done - the others keep going - so every namespace gets some representation
-  // before any namespace gets everything.
   const selections = ordered.map(([namespace, group]) => ({
   const selections = ordered.map(([namespace, group]) => ({
     namespace,
     namespace,
     picked: new Set<ToolDescription>(),
     picked: new Set<ToolDescription>(),
@@ -547,10 +465,6 @@ export const prepare = <R>(tools: HostTools<R>, catalogBudget = defaultCatalogBu
 
 
   const empty = described.length === 0
   const empty = described.length === 0
 
 
-  // Section order is deliberate: workflow first (the top is the least likely part of a long
-  // description to be truncated or skimmed away), then rules, then syntax, with the budgeted
-  // catalog at the bottom. Example call forms use placeholders - never a real or fabricated
-  // tool name - and show both dot and bracket notation so non-identifier names are not normalized.
   const intro = [
   const intro = [
     empty
     empty
       ? "This is a restricted JavaScript language for calling tools, not a general-purpose runtime."
       ? "This is a restricted JavaScript language for calling tools, not a general-purpose runtime."
@@ -562,8 +476,6 @@ export const prepare = <R>(tools: HostTools<R>, catalogBudget = defaultCatalogBu
       : ["Do not infer or normalize tool names; use only exact signatures shown below or returned by search."]),
       : ["Do not infer or normalize tool names; use only exact signatures shown below or returned by search."]),
   ]
   ]
 
 
-  // The search step exists only when search is advertised (PARTIAL catalog); a COMPLETE
-  // catalog already shows every signature, so step 1 picks from the list instead.
   const workflow = empty
   const workflow = empty
     ? []
     ? []
     : [
     : [
@@ -627,8 +539,6 @@ export const prepare = <R>(tools: HostTools<R>, catalogBudget = defaultCatalogBu
     for (const [namespace, group] of ordered) {
     for (const [namespace, group] of ordered) {
       const picked = shown.get(namespace)!
       const picked = shown.get(namespace)!
       const count = `${group.length} tool${group.length === 1 ? "" : "s"}`
       const count = `${group.length} tool${group.length === 1 ? "" : "s"}`
-      // Annotate only when a namespace is not fully shown, so a comprehensive
-      // namespace reads cleanly and a truncated one is unambiguous.
       const label =
       const label =
         picked.size === group.length
         picked.size === group.length
           ? count
           ? count
@@ -651,13 +561,6 @@ export const prepare = <R>(tools: HostTools<R>, catalogBudget = defaultCatalogBu
   }
   }
 }
 }
 
 
-/**
- * The enumerable names at one node of the callable tool tree - namespace names at the root,
- * tool/namespace names below - powering `Object.keys(tools)` and `for...in` over tool
- * references. A callable tool is a leaf and enumerates as `[]` (like `Object.keys` of a
- * function in JS). An unknown path is an `UnknownTool` error pointing at the working
- * discovery idioms, mirroring how calling an unknown tool fails.
- */
 const namespaceKeys = <R>(tools: HostTools<R>, path: ReadonlyArray<string>): ReadonlyArray<string> => {
 const namespaceKeys = <R>(tools: HostTools<R>, path: ReadonlyArray<string>): ReadonlyArray<string> => {
   let value: HostTool<R> | Definition<R> | HostTools<R> = tools
   let value: HostTool<R> | Definition<R> | HostTools<R> = tools
   for (const segment of path) {
   for (const segment of path) {
@@ -705,18 +608,12 @@ export type ToolRuntime<R = never> = {
   readonly root: ToolReference
   readonly root: ToolReference
   readonly calls: Array<ToolCall>
   readonly calls: Array<ToolCall>
   readonly invoke: (path: ReadonlyArray<string>, args: Array<unknown>) => Effect.Effect<unknown, unknown, R>
   readonly invoke: (path: ReadonlyArray<string>, args: Array<unknown>) => Effect.Effect<unknown, unknown, R>
-  /**
-   * The built-in `search` global: a synchronous discovery call that shares the tool
-   * admission pipeline (budget, audit, hooks) without living in the `tools` tree.
-   */
   readonly search: (args: Array<unknown>) => Effect.Effect<unknown, unknown, R>
   readonly search: (args: Array<unknown>) => Effect.Effect<unknown, unknown, R>
-  /** Enumerable namespace/tool names at one node of the callable tool tree; see `namespaceKeys`. */
   readonly keys: (path: ReadonlyArray<string>) => ReadonlyArray<string>
   readonly keys: (path: ReadonlyArray<string>) => ReadonlyArray<string>
 }
 }
 
 
 export const make = <R>(
 export const make = <R>(
   tools: HostTools<R>,
   tools: HostTools<R>,
-  /** Undefined means unlimited tool calls. */
   maxToolCalls: number | undefined,
   maxToolCalls: number | undefined,
   searchIndex: ReadonlyArray<SearchEntry>,
   searchIndex: ReadonlyArray<SearchEntry>,
   hooks?: ToolCallHooks<R>,
   hooks?: ToolCallHooks<R>,
@@ -724,8 +621,7 @@ export const make = <R>(
   const calls: Array<ToolCall> = []
   const calls: Array<ToolCall> = []
   const searchTool = makeSearchTool(searchIndex)
   const searchTool = makeSearchTool(searchIndex)
 
 
-  // Wraps the settling portion of a tool call so onToolCallEnd observes success and failure
-  // symmetrically. Interruption (e.g. the execution timeout) fires neither outcome.
+  // End hooks observe settled success or failure; interruption emits neither outcome.
   const observeEnd = <A, E>(effect: Effect.Effect<A, E, R>, call: ToolCallStarted): Effect.Effect<A, E, R> => {
   const observeEnd = <A, E>(effect: Effect.Effect<A, E, R>, call: ToolCallStarted): Effect.Effect<A, E, R> => {
     const onEnd = hooks?.onToolCallEnd
     const onEnd = hooks?.onToolCallEnd
     if (onEnd === undefined) return effect
     if (onEnd === undefined) return effect

+ 2 - 59
packages/codemode/src/tool-schema.ts

@@ -5,13 +5,8 @@ const isEffectSchema = (schema: SchemaType): schema is Schema.Decoder<unknown> &
 
 
 const renderLiteral = (value: unknown): string => JSON.stringify(value) ?? "unknown"
 const renderLiteral = (value: unknown): string => JSON.stringify(value) ?? "unknown"
 
 
-/**
- * Bare TypeScript identifier - usable unquoted as an object key (and, in the tool runtime,
- * with dot access as a tool-path segment). Anything else must be quoted/bracketed.
- */
 export const identifierSegment = /^[A-Za-z_$][A-Za-z0-9_$]*$/
 export const identifierSegment = /^[A-Za-z_$][A-Za-z0-9_$]*$/
 
 
-/** Renders a property name as a valid TS object key: bare when an identifier, quoted otherwise. */
 const renderKey = (name: string): string => (identifierSegment.test(name) ? name : JSON.stringify(name))
 const renderKey = (name: string): string => (identifierSegment.test(name) ? name : JSON.stringify(name))
 
 
 const effectNumberSentinel = (schema: JsonSchema) =>
 const effectNumberSentinel = (schema: JsonSchema) =>
@@ -27,16 +22,10 @@ const intersection = (members: ReadonlyArray<string>): string => {
   return concrete.map((member) => (member.includes(" | ") ? `(${member})` : member)).join(" & ")
   return concrete.map((member) => (member.includes(" | ") ? `(${member})` : member)).join(" & ")
 }
 }
 
 
-/**
- * Recursion ceiling for schema rendering. Object, array, and union recursion all increment
- * depth, so this bounds every recursion path - pathological or structurally cyclic schemas
- * degrade to `unknown` instead of overflowing the stack (rendering must never throw).
- */
 const MAX_RENDER_DEPTH = 8
 const MAX_RENDER_DEPTH = 8
 
 
 type RenderContext = {
 type RenderContext = {
   readonly definitions: Readonly<Record<string, JsonSchema>>
   readonly definitions: Readonly<Record<string, JsonSchema>>
-  /** Indented, JSDoc-annotated multiline rendering (search results); compact single line otherwise. */
   readonly pretty: boolean
   readonly pretty: boolean
 }
 }
 
 
@@ -64,10 +53,6 @@ const hasUnresolvedRef = (
   ].some((item) => hasUnresolvedRef(item, definitions, seen, nextVisited))
   ].some((item) => hasUnresolvedRef(item, definitions, seen, nextVisited))
 }
 }
 
 
-/**
- * Schema constraints a TypeScript type cannot express natively but a model benefits from,
- * surfaced as JSDoc tags (`@deprecated`, `@default`, `@format`, `@minItems`, `@maxItems`).
- */
 const docTags = (schema: JsonSchema): Array<string> => {
 const docTags = (schema: JsonSchema): Array<string> => {
   const tags: Array<string> = []
   const tags: Array<string> = []
   if (schema.deprecated === true) tags.push("@deprecated")
   if (schema.deprecated === true) tags.push("@deprecated")
@@ -75,9 +60,7 @@ const docTags = (schema: JsonSchema): Array<string> => {
     try {
     try {
       const rendered = JSON.stringify(schema.default)
       const rendered = JSON.stringify(schema.default)
       if (rendered !== undefined) tags.push(`@default ${rendered}`)
       if (rendered !== undefined) tags.push(`@default ${rendered}`)
-    } catch {
-      // unserializable default: skip rather than emit a broken tag
-    }
+    } catch {}
   }
   }
   if (typeof schema.format === "string") tags.push(`@format ${schema.format}`)
   if (typeof schema.format === "string") tags.push(`@format ${schema.format}`)
   if (typeof schema.minItems === "number") tags.push(`@minItems ${schema.minItems}`)
   if (typeof schema.minItems === "number") tags.push(`@minItems ${schema.minItems}`)
@@ -85,13 +68,7 @@ const docTags = (schema: JsonSchema): Array<string> => {
   return tags
   return tags
 }
 }
 
 
-/**
- * Format a schema `description` plus `tags` as a JSDoc comment at the given indent,
- * preserving multi-line text (a single line stays `/** ... *\/`; multiple lines become a
- * `*`-prefixed block). `*\/` is neutralized so nothing can close the comment early, and
- * blank leading/trailing lines are trimmed. Returns "" (else a trailing newline) so
- * callers can prepend it directly to the field line.
- */
+// Neutralize `*\/` so model-provided schema text cannot terminate generated documentation.
 const jsdoc = (description: string | undefined, tags: ReadonlyArray<string>, pad: string): string => {
 const jsdoc = (description: string | undefined, tags: ReadonlyArray<string>, pad: string): string => {
   const lines = [...(description === undefined ? [] : description.split("\n")), ...tags].map((line) =>
   const lines = [...(description === undefined ? [] : description.split("\n")), ...tags].map((line) =>
     line.replaceAll("*/", "* /").replace(/\s+$/, ""),
     line.replaceAll("*/", "* /").replace(/\s+$/, ""),
@@ -128,17 +105,11 @@ const renderSchema = (
   if (schema.enum) return schema.enum.map(renderLiteral).join(" | ")
   if (schema.enum) return schema.enum.map(renderLiteral).join(" | ")
   const alternatives = schema.anyOf ?? schema.oneOf
   const alternatives = schema.anyOf ?? schema.oneOf
   if (alternatives) {
   if (alternatives) {
-    // Effect's number schema emits `anyOf: [{ type: "number" }, { const: "NaN" },
-    // { const: "Infinity" }, { const: "-Infinity" }]`. Collapse only that artifact;
-    // real JSON Schema unions such as `string | number` or `number | null` must keep
-    // every branch.
     if (
     if (
       alternatives.some((item) => item.type === "number") &&
       alternatives.some((item) => item.type === "number") &&
       alternatives.every((item) => item.type === "number" || effectNumberSentinel(item))
       alternatives.every((item) => item.type === "number" || effectNumberSentinel(item))
     )
     )
       return "number"
       return "number"
-    // An empty Schema.Struct({}) emits `anyOf: [{ type: "object" }, { type: "array" }]`
-    // (no properties/items); render the bare shape as {} instead of `{} | Array<unknown>`.
     if (
     if (
       alternatives.length === 2 &&
       alternatives.length === 2 &&
       alternatives[0]?.type === "object" &&
       alternatives[0]?.type === "object" &&
@@ -183,7 +154,6 @@ const renderSchema = (
       return fields.length === 0 ? "{}" : `{ ${fields.join("; ")} }`
       return fields.length === 0 ? "{}" : `{ ${fields.join("; ")} }`
     }
     }
 
 
-    // Pretty: an indented block, each described field preceded by its JSDoc comment.
     if (properties.length === 0 && indexType === undefined) return "{}"
     if (properties.length === 0 && indexType === undefined) return "{}"
     const pad = "  ".repeat(depth + 1)
     const pad = "  ".repeat(depth + 1)
     const lines = properties.map(
     const lines = properties.map(
@@ -208,7 +178,6 @@ export const toTypeScript = (schema: Schema.Top, decoded = false, pretty = false
   }
   }
 }
 }
 
 
-/** Renders a raw JSON Schema document as a TypeScript type string. */
 export const jsonSchemaToTypeScript = (schema: JsonSchema, pretty = false): string => {
 export const jsonSchemaToTypeScript = (schema: JsonSchema, pretty = false): string => {
   try {
   try {
     return renderSchema(schema, { definitions: { ...(schema.definitions ?? {}), ...(schema.$defs ?? {}) }, pretty })
     return renderSchema(schema, { definitions: { ...(schema.definitions ?? {}), ...(schema.$defs ?? {}) }, pretty })
@@ -217,20 +186,12 @@ export const jsonSchemaToTypeScript = (schema: JsonSchema, pretty = false): stri
   }
   }
 }
 }
 
 
-/** One input property of a tool, extracted best-effort from its input schema. */
 export type InputProperty = {
 export type InputProperty = {
   readonly name: string
   readonly name: string
   readonly description: string | undefined
   readonly description: string | undefined
   readonly required: boolean
   readonly required: boolean
 }
 }
 
 
-/**
- * The property names, descriptions, and required flags of a tool's input schema - the raw
- * material for search text. Best-effort: Effect Schemas go through their
- * JSON Schema document (the same emission signature rendering uses); JSON Schemas are read
- * directly, resolving a trivial top-level `$ref` into `$defs`/`definitions` when present.
- * Anything unresolvable yields `[]` (search falls back to path + description).
- */
 export const inputProperties = <R>(definition: Definition<R>): Array<InputProperty> => {
 export const inputProperties = <R>(definition: Definition<R>): Array<InputProperty> => {
   try {
   try {
     const document = isEffectSchema(definition.input)
     const document = isEffectSchema(definition.input)
@@ -262,20 +223,11 @@ export const inputProperties = <R>(definition: Definition<R>): Array<InputProper
   }
   }
 }
 }
 
 
-/**
- * The model-visible TypeScript type of a tool's input. `pretty` renders an indented
- * multiline block with schema descriptions and constraints as JSDoc comments on the
- * fields; the default stays the compact single-line form.
- */
 export const inputTypeScript = <R>(definition: Definition<R>, pretty = false): string =>
 export const inputTypeScript = <R>(definition: Definition<R>, pretty = false): string =>
   isEffectSchema(definition.input)
   isEffectSchema(definition.input)
     ? toTypeScript(definition.input, false, pretty)
     ? toTypeScript(definition.input, false, pretty)
     : jsonSchemaToTypeScript(definition.input, pretty)
     : jsonSchemaToTypeScript(definition.input, pretty)
 
 
-/**
- * The model-visible TypeScript type of a tool's result; tools without an output schema
- * return `unknown`. `pretty` renders the JSDoc-annotated multiline form, as for inputs.
- */
 export const outputTypeScript = <R>(definition: Definition<R>, pretty = false): string =>
 export const outputTypeScript = <R>(definition: Definition<R>, pretty = false): string =>
   definition.output === undefined
   definition.output === undefined
     ? "unknown"
     ? "unknown"
@@ -283,18 +235,9 @@ export const outputTypeScript = <R>(definition: Definition<R>, pretty = false):
       ? toTypeScript(definition.output, true, pretty)
       ? toTypeScript(definition.output, true, pretty)
       : jsonSchemaToTypeScript(definition.output, pretty)
       : jsonSchemaToTypeScript(definition.output, pretty)
 
 
-/**
- * Decodes tool input before `run` is invoked. Effect Schemas validate (throwing on failure);
- * JSON-Schema-described inputs pass through unvalidated (render-only).
- */
 export const decodeInput = <R>(definition: Definition<R>, value: unknown): unknown =>
 export const decodeInput = <R>(definition: Definition<R>, value: unknown): unknown =>
   isEffectSchema(definition.input) ? Schema.decodeUnknownSync(definition.input)(value) : value
   isEffectSchema(definition.input) ? Schema.decodeUnknownSync(definition.input)(value) : value
 
 
-/**
- * Decodes a tool result before it is exposed to the program. Effect Schemas validate and
- * transform (throwing on failure); JSON Schema outputs and tools without an output schema pass
- * the host value through unchanged.
- */
 export const decodeOutput = <R>(definition: Definition<R>, value: unknown): unknown =>
 export const decodeOutput = <R>(definition: Definition<R>, value: unknown): unknown =>
   definition.output !== undefined && isEffectSchema(definition.output)
   definition.output !== undefined && isEffectSchema(definition.output)
     ? Schema.decodeUnknownSync(definition.output)(value)
     ? Schema.decodeUnknownSync(definition.output)(value)

+ 5 - 30
packages/codemode/src/tool.ts

@@ -1,11 +1,8 @@
 import { Effect, Schema } from "effect"
 import { Effect, Schema } from "effect"
 
 
 /**
 /**
- * JSON Schema subset accepted for render-only tool schemas.
- *
- * A JSON-Schema-described side of a tool is used to generate the model-visible TypeScript
- * signature only - CodeMode performs no validation against it. This is the natural shape for
- * adapter-provided tools (e.g. MCP definitions) whose schemas arrive as JSON Schema documents.
+ * JSON Schema subset for model-visible signatures. CodeMode does not validate values against
+ * these schemas.
  */
  */
 export type JsonSchema = {
 export type JsonSchema = {
   readonly type?: string | ReadonlyArray<string>
   readonly type?: string | ReadonlyArray<string>
@@ -41,10 +38,8 @@ export type Definition<R = never> = {
   readonly run: (input: unknown) => Effect.Effect<unknown, unknown, R>
   readonly run: (input: unknown) => Effect.Effect<unknown, unknown, R>
 }
 }
 
 
-/** The value `run` receives: the decoded type for Effect Schemas, `unknown` for JSON Schemas. */
 type InputType<S> = S extends Schema.Decoder<unknown> ? S["Type"] : unknown
 type InputType<S> = S extends Schema.Decoder<unknown> ? S["Type"] : unknown
 
 
-/** The value `run` returns: the encoded type for Effect Schemas, `unknown` otherwise. */
 type ResultType<S> = S extends Schema.Decoder<unknown> ? S["Encoded"] : unknown
 type ResultType<S> = S extends Schema.Decoder<unknown> ? S["Encoded"] : unknown
 
 
 /** Options for defining one CodeMode tool. */
 /** Options for defining one CodeMode tool. */
@@ -61,29 +56,9 @@ export const isDefinition = <R = never>(value: unknown): value is Definition<R>
 /**
 /**
  * Defines one schema-described tool available to a CodeMode program through `tools.*`.
  * Defines one schema-described tool available to a CodeMode program through `tools.*`.
  *
  *
- * `input` and `output` each accept a validating Effect Schema or a render-only JSON Schema
- * document. Effect Schema input is decoded before `run` is invoked, and `run` returns the
- * encoded representation of an Effect Schema `output`, which CodeMode decodes before returning
- * it to the program. JSON Schemas only shape the model-visible signature; values pass through
- * unvalidated. `output` is optional - without it the signature advertises `unknown` and the
- * host result is exposed as-is. The host tool remains responsible for authorization and
- * durable side-effect handling.
- *
- * @example
- * ```ts
- * const lookup = Tool.make({
- *   description: "Look up an order",
- *   input: Schema.Struct({ id: Schema.String }),
- *   output: Schema.Struct({ status: Schema.String }),
- *   run: ({ id }) => Effect.succeed({ status: "open" }),
- * })
- *
- * const fromJsonSchema = Tool.make({
- *   description: "Call an adapter-described tool",
- *   input: { type: "object", properties: { id: { type: "string" } }, required: ["id"] },
- *   run: (input) => callHost(input),
- * })
- * ```
+ * Effect Schemas validate values; JSON Schemas only shape the model-visible signature.
+ * Without `output`, results are exposed as `unknown`. Hosts remain responsible for authorization
+ * and durable side effects.
  */
  */
 export const make = <I extends SchemaType, const O extends SchemaType | undefined = undefined, R = never>(
 export const make = <I extends SchemaType, const O extends SchemaType | undefined = undefined, R = never>(
   options: Options<I, O, R>,
   options: Options<I, O, R>,

+ 16 - 0
packages/codemode/test/codemode.test.ts

@@ -26,6 +26,22 @@ describe("CodeMode host failure boundary", () => {
     })
     })
   })
   })
 
 
+  test("does not rewrite explicit safe tool failures", async () => {
+    const result = await run(
+      Tool.make({
+        description: "Fail safely",
+        input: Schema.Struct({}),
+        output: Schema.String,
+        run: () => Effect.fail(toolError("File not found: /tmp/report.json")),
+      }),
+    )
+
+    expect(result.ok ? undefined : result.error).toStrictEqual({
+      kind: "ToolFailure",
+      message: "File not found: /tmp/report.json",
+    })
+  })
+
   test("sanitizes unknown host failures and defects", async () => {
   test("sanitizes unknown host failures and defects", async () => {
     for (const failure of [
     for (const failure of [
       Effect.fail(new UnsafeHostError({ reason: "Authorization: Bearer typed-secret" })),
       Effect.fail(new UnsafeHostError({ reason: "Authorization: Bearer typed-secret" })),

+ 6 - 3
packages/codemode/test/parity.test.ts

@@ -302,9 +302,12 @@ describe("CodeMode-specific string behavior", () => {
     expect(await value(`try { "x".normalize("nope"); return "no" } catch (e) { return e.message }`)).toContain('"NFC"')
     expect(await value(`try { "x".normalize("nope"); return "no" } catch (e) { return e.message }`)).toContain('"NFC"')
   })
   })
 
 
-  test("trimLeft/trimRight alias trimStart/trimEnd", async () => {
-    expect(await value(`return "  x ".trimLeft()`)).toBe("x ")
-    expect(await value(`return "  x ".trimRight()`)).toBe("  x")
+  test("does not expose obsolete string aliases", async () => {
+    expect(await value(`return [typeof "x".trimLeft, typeof "x".trimRight, typeof "x".substr]`)).toEqual([
+      "undefined",
+      "undefined",
+      "undefined",
+    ])
   })
   })
 })
 })
 
 

+ 0 - 67
packages/codemode/test/string-search-test262.test.ts

@@ -49,12 +49,6 @@
  * - test/built-ins/String/prototype/substring/S15.5.4.15_A2_T8.js
  * - test/built-ins/String/prototype/substring/S15.5.4.15_A2_T8.js
  * - test/built-ins/String/prototype/substring/S15.5.4.15_A2_T9.js
  * - test/built-ins/String/prototype/substring/S15.5.4.15_A2_T9.js
  * - test/built-ins/String/prototype/substring/S15.5.4.15_A2_T10.js
  * - test/built-ins/String/prototype/substring/S15.5.4.15_A2_T10.js
- * - test/annexB/built-ins/String/prototype/substr/start-negative.js
- * - test/annexB/built-ins/String/prototype/substr/length-negative.js
- * - test/annexB/built-ins/String/prototype/substr/length-positive.js
- * - test/annexB/built-ins/String/prototype/substr/length-falsey.js
- * - test/annexB/built-ins/String/prototype/substr/length-undef.js
- * - test/annexB/built-ins/String/prototype/substr/surrogate-pairs.js
  * - test/built-ins/String/prototype/includes/String.prototype.includes_FailMissingLetter.js
  * - test/built-ins/String/prototype/includes/String.prototype.includes_FailMissingLetter.js
  * - test/built-ins/String/prototype/includes/String.prototype.includes_SuccessNoLocation.js
  * - test/built-ins/String/prototype/includes/String.prototype.includes_SuccessNoLocation.js
  * - test/built-ins/String/prototype/includes/String.prototype.includes_FailBadLocation.js
  * - test/built-ins/String/prototype/includes/String.prototype.includes_FailBadLocation.js
@@ -460,67 +454,6 @@ const cases = [
     expected: ["this_is_"],
     expected: ["this_is_"],
     labels: ['#1: __string.substring(0,8) === "this_is_"'],
     labels: ['#1: __string.substring(0,8) === "this_is_"'],
   },
   },
-  {
-    path: "test/annexB/built-ins/String/prototype/substr/start-negative.js",
-    code: `return ["abc".substr(-1), "abc".substr(-2), "abc".substr(-3), "abc".substr(-4), "abc".substr(-1.1)]`,
-    expected: ["c", "bc", "abc", "abc", "c"],
-    labels: ["-1", "-2", "-3", "size + intStart < 0", "floating point rounding semantics"],
-  },
-  {
-    path: "test/annexB/built-ins/String/prototype/substr/length-negative.js",
-    code: `return [
-      "abc".substr(0, -1), "abc".substr(0, -2), "abc".substr(0, -3), "abc".substr(0, -4),
-      "abc".substr(1, -1), "abc".substr(1, -2), "abc".substr(1, -3), "abc".substr(1, -4),
-      "abc".substr(2, -1), "abc".substr(2, -2), "abc".substr(2, -3), "abc".substr(2, -4),
-      "abc".substr(3, -1), "abc".substr(3, -2), "abc".substr(3, -3), "abc".substr(3, -4),
-    ]`,
-    expected: ["", "", "", "", "", "", "", "", "", "", "", "", "", "", "", ""],
-    labels: [
-      "0, -1", "0, -2", "0, -3", "0, -4", "1, -1", "1, -2", "1, -3", "1, -4",
-      "2, -1", "2, -2", "2, -3", "2, -4", "3, -1", "3, -2", "3, -3", "3, -4",
-    ],
-  },
-  {
-    path: "test/annexB/built-ins/String/prototype/substr/length-positive.js",
-    code: `return [
-      "abc".substr(0, 1), "abc".substr(0, 2), "abc".substr(0, 3), "abc".substr(0, 4),
-      "abc".substr(1, 1), "abc".substr(1, 2), "abc".substr(1, 3), "abc".substr(1, 4),
-      "abc".substr(2, 1), "abc".substr(2, 2), "abc".substr(2, 3), "abc".substr(2, 4),
-      "abc".substr(3, 1), "abc".substr(3, 2), "abc".substr(3, 3), "abc".substr(3, 4),
-    ]`,
-    expected: ["a", "ab", "abc", "abc", "b", "bc", "bc", "bc", "c", "c", "c", "c", "", "", "", ""],
-    labels: [
-      "0, 1", "0, 1", "0, 1", "0, 1", "1, 1", "1, 1", "1, 1", "1, 1",
-      "2, 1", "2, 1", "2, 1", "2, 1", "3, 1", "3, 1", "3, 1", "3, 1",
-    ],
-  },
-  {
-    path: "test/annexB/built-ins/String/prototype/substr/length-falsey.js",
-    code: `return ["abc".substr(0, NaN), "abc".substr(1, NaN), "abc".substr(2, NaN), "abc".substr(3, NaN)]`,
-    expected: ["", "", "", ""],
-    labels: ["start: 0, length: NaN", "start: 1, length: NaN", "start: 2, length: NaN", "start: 3, length: NaN"],
-  },
-  {
-    path: "test/annexB/built-ins/String/prototype/substr/length-undef.js",
-    code: `return [
-      "abc".substr(0), "abc".substr(1), "abc".substr(2), "abc".substr(3),
-      "abc".substr(0, undefined), "abc".substr(1, undefined), "abc".substr(2, undefined), "abc".substr(3, undefined),
-    ]`,
-    expected: ["abc", "bc", "c", "", "abc", "bc", "c", ""],
-    labels: [
-      "start: 0, length: unspecified", "start: 1, length: unspecified", "start: 2, length: unspecified", "start: 3, length: unspecified",
-      "start: 0, length: undefined", "start: 1, length: undefined", "start: 2, length: undefined", "start: 3, length: undefined",
-    ],
-  },
-  {
-    path: "test/annexB/built-ins/String/prototype/substr/surrogate-pairs.js",
-    code: `return [
-      "\uD834\uDF06".substr(0), "\uD834\uDF06".substr(1), "\uD834\uDF06".substr(2),
-      "\uD834\uDF06".substr(0, 0), "\uD834\uDF06".substr(0, 1), "\uD834\uDF06".substr(0, 2),
-    ]`,
-    expected: ["\uD834\uDF06", "\uDF06", "", "", "\uD834", "\uD834\uDF06"],
-    labels: ["start: 0", "start: 1", "start: 2", "end: 0", "end: 1", "end: 2"],
-  },
   {
   {
     path: "test/built-ins/String/prototype/includes/String.prototype.includes_FailMissingLetter.js",
     path: "test/built-ins/String/prototype/includes/String.prototype.includes_FailMissingLetter.js",
     code: `return ["word".includes("a", 0)]`, expected: [false], labels: ['"word".includes("a", 0)'],
     code: `return ["word".includes("a", 0)]`, expected: [false], labels: ['"word".includes("a", 0)'],

+ 2 - 1
packages/codemode/tsconfig.json

@@ -2,6 +2,7 @@
   "$schema": "https://json.schemastore.org/tsconfig",
   "$schema": "https://json.schemastore.org/tsconfig",
   "extends": "@tsconfig/bun/tsconfig.json",
   "extends": "@tsconfig/bun/tsconfig.json",
   "compilerOptions": {
   "compilerOptions": {
-    "noUncheckedIndexedAccess": false
+    "noUncheckedIndexedAccess": false,
+    "noUnusedLocals": true
   }
   }
 }
 }

Some files were not shown because too many files changed in this diff