Просмотр исходного кода

feat(code-runtime): own portable-identifier exclusions at the seam

Move the reserved-word, reserved-global, reserved-error-member, and
dunder exclusion sets from the worker backend up to the code-runtime
seam package, and narrow the portable identifier subset to drop the
JS-only `$`. Every backend now imports one contract so a binding
namespace list valid on one backend is valid on all.

Delivers only the seam extension and the worker's adoption; the Python
backend, py-types renderer, and Code Mode language dispatch are later
PRs in the stack that depend on these exports.
Chinesezjc 1 месяц назад
Родитель
Сommit
5d4cea9dc1

+ 6 - 0
.agents/notes/implemented/architecture/2026-07-31-code-runtime-portable-identifier-seam.i18n.yaml

@@ -0,0 +1,6 @@
+# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
+# side as of the last confirmed-consistent state. Both languages carry equal authority;
+# after editing either side, bring the other along and re-record with:
+#   pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-31-code-runtime-portable-identifier-seam.md
+2026-07-31-code-runtime-portable-identifier-seam.md: a24a2c03c937a7d569528504b5bd5fba812009a2
+2026-07-31-code-runtime-portable-identifier-seam.zh.md: 1bef607e31a5820ba849f09eb7ae4cc781c45a84

+ 42 - 0
.agents/notes/implemented/architecture/2026-07-31-code-runtime-portable-identifier-seam.md

@@ -0,0 +1,42 @@
+# Agent Note: the code-runtime seam owns portable-identifier exclusions
+
+Status: implemented
+
+English | [中文](2026-07-31-code-runtime-portable-identifier-seam.zh.md)
+
+## Problem
+
+The code-runtime seam promises that a binding-namespace list valid on one backend is valid on every backend, so a Code Mode consumer can hand the same bindings to any registered runtime without knowing its language. The first backend, `dsh-code-runtime-worker`, privately owned the identifier rules that enforce part of that promise: an `IDENTIFIER` regex that allowed the JS-only `$`, a `RESERVED_WORDS` set holding only ECMAScript keywords, and a `RESERVED_ERROR_PROPERTIES` set of three JS `Error` slots. Those rules described the worker's own language, not the seam's portability contract.
+
+A second backend written against a different language (CPython, arriving in a later PR of this stack) would either re-declare its own rules — letting `lambda` pass the worker and fail Python, or `$tools` pass the worker and fail every non-JS backend — or import the worker's, inverting the dependency so the seam's implementation reached into a sibling implementation. Neither keeps the portability promise real: it would hold only for the backend a caller happened to test against.
+
+## Decision
+
+The seam package (`@deepseek-ai/dsh-code-runtime`) exports the portable-identifier exclusion contract as four named constants, and every backend imports them rather than re-declaring:
+
+- `PORTABLE_RESERVED_WORDS` — the union of ECMAScript and Python reserved words. A namespace global or error-class name matching any is refused on all backends, so `lambda` is refused even though it is a legal JS parameter name. Adding a language widens this union, which is a deliberate breaking review of existing binding names.
+- `RESERVED_BINDING_GLOBALS` — globals some backend owns in the program's namespace: `console` (the worker's log capture) and `__dsh_main__`/`__builtins__`/`__name__`/`__debug__` (the Python bootstrap's wrapper and seeded module globals). Refused everywhere so a namespace list cannot pick a name that works on one backend and collides on another.
+- `RESERVED_ERROR_MEMBERS` — error-member names every backend refuses: the JS `Error` slots (`name`, `message`, `stack`) and Python's exception-protocol members (`args`, `with_traceback`, `add_note`).
+- `DUNDER_MEMBER` — the dunder-form regex (`__*__`), refused as an error member wholesale because several are constrained CPython descriptors whose exact set is an interpreter-version detail.
+
+The seam also narrows the portable identifier subset to `[A-Za-z_][A-Za-z0-9_]*` (documented on `CodeBindingNamespace.global` and `CodeBindingErrorClass`), dropping the JS-only `$`. The worker consumes the shared constants: `RESERVED_WORDS = PORTABLE_RESERVED_WORDS`, `RESERVED_ERROR_PROPERTIES = RESERVED_ERROR_MEMBERS`, its `IDENTIFIER` regex loses `$`, and its error-member check adds `DUNDER_MEMBER`.
+
+The constants live at the seam even though only one backend ships in this PR: the whole point is that the contract is language-agnostic and owned above any single language. A backend that violated it would be the bug, and the shared set is where a reviewer looks to see what "portable" means.
+
+## Scope
+
+This PR delivers only the seam extension and the worker's adoption of it. No Python backend, `py-types` renderer, or Code Mode language dispatch ships here — they are later PRs in the stack that depend on these exports. The seam README's worker-only wording is left unchanged for the same reason: linking to a `dsh-code-runtime-python` README that does not yet exist would break the dead-link gate.
+
+## Alternatives considered
+
+**Each backend declares its own exclusions.** Rejected: it makes the portability promise per-backend. A binding list the caller tested on the worker could be refused by Python, which is exactly the split the seam exists to prevent.
+
+**The Python backend imports the worker's constants.** Rejected: it inverts the dependency — the seam's implementations would reach into a sibling implementation for a contract neither owns. The contract belongs above both, at the seam.
+
+**Keep `$` in the portable identifier subset.** Rejected: `$` is JS-only spelling. Allowing it would let `$tools` pass the worker and fail every non-JS backend, breaking portability for a purely cosmetic gain.
+
+## Consequences
+
+Bought: one place — the seam package — defines what a portable binding name is, and every backend enforces the same contract by import. A namespace list valid on one backend is valid on all, verifiably, not by coincidence of which backend the caller tested.
+
+Cost: existing worker callers using a `$`-containing global now fail identifier validation. Under the pre-release stance this is a corrected foundation, not a compatibility break to shim. The worker's seam-misuse tests gain cases for `$tools`, Python exception members (`args`), dunders (`__dict__`), and a Python-owned global (`__dsh_main__`), proving the shared set is enforced from the worker side.

+ 42 - 0
.agents/notes/implemented/architecture/2026-07-31-code-runtime-portable-identifier-seam.zh.md

@@ -0,0 +1,42 @@
+# Agent Note:code-runtime seam 拥有可移植标识符排除集
+
+Status: implemented
+
+[English](2026-07-31-code-runtime-portable-identifier-seam.md) | 中文
+
+## Problem
+
+code-runtime seam 承诺:在一个后端上有效的绑定命名空间列表,在每个后端上都有效,因此 Code Mode 消费方可以把同一组绑定交给任何已注册的运行时,而不必知道它的语言。首个后端 `dsh-code-runtime-worker` 私自拥有了执行这项承诺一部分的标识符规则:一个允许 JS 专有 `$` 的 `IDENTIFIER` 正则、一个只含 ECMAScript 关键字的 `RESERVED_WORDS` 集合,以及一个含三个 JS `Error` 槽位的 `RESERVED_ERROR_PROPERTIES` 集合。这些规则描述的是 worker 自身的语言,而非 seam 的可移植性契约。
+
+一个针对不同语言(CPython,将在本 stack 后续 PR 中到来)编写的第二后端,要么重新声明自己的规则——让 `lambda` 通过 worker 却在 Python 上失败,或让 `$tools` 通过 worker 却在每个非 JS 后端上失败——要么导入 worker 的规则,从而反转依赖,使 seam 的一个实现伸手进入另一个兄弟实现。二者都无法让可移植承诺成真:它只对调用方恰好测试过的那个后端成立。
+
+## Decision
+
+seam 包(`@deepseek-ai/dsh-code-runtime`)以四个具名常量导出可移植标识符排除契约,每个后端导入它们而非重新声明:
+
+- `PORTABLE_RESERVED_WORDS`——ECMAScript 与 Python 保留字的联集。任何命名空间 global 或 error-class 名称匹配其中之一,都在所有后端上被拒绝,因此 `lambda` 即便是合法的 JS 参数名也被拒绝。新增一门语言即扩宽此联集,这是对现有绑定名称的一次有意的破坏性复审。
+- `RESERVED_BINDING_GLOBALS`——某个后端在程序命名空间中拥有的 global:`console`(worker 的日志捕获)与 `__dsh_main__`/`__builtins__`/`__name__`/`__debug__`(Python bootstrap 的包装器与预置模块 global)。在所有后端上被拒绝,使命名空间列表无法选到一个在某后端能用、在另一后端冲突的名称。
+- `RESERVED_ERROR_MEMBERS`——每个后端都拒绝的 error-member 名称:JS `Error` 槽位(`name`、`message`、`stack`)与 Python 异常协议成员(`args`、`with_traceback`、`add_note`)。
+- `DUNDER_MEMBER`——dunder 形式正则(`__*__`),作为 error member 被整体拒绝,因为其中若干是受约束的 CPython 描述符,其确切集合是解释器版本细节。
+
+seam 同时把可移植标识符子集收窄为 `[A-Za-z_][A-Za-z0-9_]*`(记录在 `CodeBindingNamespace.global` 与 `CodeBindingErrorClass` 上),去掉 JS 专有的 `$`。worker 消费这些共享常量:`RESERVED_WORDS = PORTABLE_RESERVED_WORDS`、`RESERVED_ERROR_PROPERTIES = RESERVED_ERROR_MEMBERS`,其 `IDENTIFIER` 正则去掉 `$`,其 error-member 检查加上 `DUNDER_MEMBER`。
+
+尽管本 PR 只交付一个后端,这些常量仍置于 seam:要点正是该契约与语言无关,且拥有权在任何单一语言之上。违反它的后端才是 bug,而共享集合正是复审者查看"可移植"含义的地方。
+
+## Scope
+
+本 PR 只交付 seam 扩展与 worker 对它的采用。这里不交付任何 Python 后端、`py-types` 渲染器或 Code Mode 的语言分发——它们是本 stack 中依赖这些导出的后续 PR。seam README 中仅描述 worker 的措辞保持不变,理由相同:链接到一个尚不存在的 `dsh-code-runtime-python` README 会破坏死链 gate。
+
+## Alternatives considered
+
+**每个后端声明自己的排除集。** 拒绝:这让可移植承诺变成逐后端成立。调用方在 worker 上测过的绑定列表可能被 Python 拒绝,而这正是 seam 存在要防止的分裂。
+
+**Python 后端导入 worker 的常量。** 拒绝:这反转依赖——seam 的实现会为一个二者都不拥有的契约伸手进入兄弟实现。契约属于二者之上,即 seam。
+
+**在可移植标识符子集中保留 `$`。** 拒绝:`$` 是 JS 专有拼写。允许它会让 `$tools` 通过 worker 却在每个非 JS 后端上失败,为纯粹表面的好处破坏可移植性。
+
+## Consequences
+
+获得:一个地方——seam 包——定义什么是可移植绑定名称,每个后端通过导入执行同一契约。在一个后端上有效的命名空间列表在所有后端上都有效,这是可验证的,而非取决于调用方测试了哪个后端的巧合。
+
+代价:现有使用含 `$` global 的 worker 调用方现在会在标识符校验时失败。在预发布立场下这是一次被纠正的地基,而非需要 shim 的兼容性破坏。worker 的 seam-misuse 测试新增了 `$tools`、Python 异常成员(`args`)、dunder(`__dict__`)与一个 Python 拥有的 global(`__dsh_main__`)等用例,从 worker 侧证明共享集合被执行。

+ 28 - 16
packages/code-runtime/code-runtime-worker/src/index.ts

@@ -13,7 +13,7 @@ import { fileURLToPath } from 'node:url'
 import { Context } from 'cordis'
 import z from 'schemastery'
 import { MAX_TIMER_DELAY_MS } from '@deepseek-ai/dsh-timeout'
-import { CodeRuntime } from '@deepseek-ai/dsh-code-runtime'
+import { CodeRuntime, DUNDER_MEMBER, PORTABLE_RESERVED_WORDS, RESERVED_BINDING_GLOBALS, RESERVED_ERROR_MEMBERS } from '@deepseek-ai/dsh-code-runtime'
 import type { CodeBindingNamespace, CodeJsonValue, CodeRunFailure, CodeRunRequest, CodeRunResult } from '@deepseek-ai/dsh-code-runtime'
 import { snapshotJsonValue } from '@deepseek-ai/dsh-session'
 import type { ReplyMessage, WorkerBootData, WorkerToHost } from './protocol.ts'
@@ -65,20 +65,27 @@ const ELU_POLL_INTERVAL_MS = 25
 /** Smallest cap that can represent the counted payloads: an empty logs array plus an empty JSON failure message. */
 const MIN_OUTPUT_BYTES = 4
 
-/** ECMAScript reserved words that cannot be async-function parameter names — rejected as binding globals. */
-const RESERVED_WORDS = new Set([
-  'await', 'break', 'case', 'catch', 'class', 'const', 'continue', 'debugger', 'default', 'delete', 'do',
-  'else', 'enum', 'export', 'extends', 'false', 'finally', 'for', 'function', 'if', 'import', 'in',
-  'instanceof', 'new', 'null', 'return', 'super', 'switch', 'this', 'throw', 'true', 'try', 'typeof',
-  'var', 'void', 'while', 'with', 'yield', 'let', 'static', 'implements', 'interface', 'package',
-  'private', 'protected', 'public', 'arguments', 'eval',
-])
+/**
+ * The seam's cross-language reserved-word union: the portable-identifier
+ * contract promises a namespace list valid here is valid on every backend, so
+ * a Python keyword like `lambda` is refused even though it is a legal JS
+ * parameter name.
+ */
+const RESERVED_WORDS = PORTABLE_RESERVED_WORDS
 
-/** Valid async-function parameter name (the binding global becomes one). */
-const IDENTIFIER = /^[A-Za-z_$][A-Za-z0-9_$]*$/
+/**
+ * The seam's language-portable identifier subset (see
+ * `CodeBindingNamespace.global`): no `$`, which is JS-only spelling — the same
+ * namespace list must be usable against every backend regardless of language.
+ */
+const IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*$/
 
-/** Error properties whose binding-member replacement would destroy the promised Error contract. */
-const RESERVED_ERROR_PROPERTIES = new Set(['name', 'message', 'stack'])
+/**
+ * The seam's shared error-member exclusions (plus the dunder rule below):
+ * enforced identically here and in the Python backend so an errorClass valid
+ * on one backend is valid on all.
+ */
+const RESERVED_ERROR_PROPERTIES = RESERVED_ERROR_MEMBERS
 
 /**
  * The shell a program is wrapped in for the type-strip, matching the
@@ -331,7 +338,11 @@ export class WorkerCodeRuntime extends CodeRuntime {
       if (!IDENTIFIER.test(namespace.global) || RESERVED_WORDS.has(namespace.global)) {
         throw new Error(`dsh-code-runtime-worker: binding global ${JSON.stringify(namespace.global)} is not a usable identifier`)
       }
-      if (namespace.global === 'console' || bindings.has(namespace.global)) {
+      // RESERVED_BINDING_GLOBALS is the seam's shared backend-owned set:
+      // `console` is THIS backend's log-capture slot; the dunder entries are
+      // the Python bootstrap's — refused here too so the namespace list stays
+      // portable across backends.
+      if (RESERVED_BINDING_GLOBALS.has(namespace.global) || bindings.has(namespace.global)) {
         throw new Error(`dsh-code-runtime-worker: duplicate binding global ${JSON.stringify(namespace.global)}`)
       }
       bindings.set(namespace.global, namespace)
@@ -344,10 +355,11 @@ export class WorkerCodeRuntime extends CodeRuntime {
       if (!IDENTIFIER.test(descriptor.name) || RESERVED_WORDS.has(descriptor.name)) {
         throw new Error(`dsh-code-runtime-worker: binding error class ${JSON.stringify(descriptor.name)} is not a usable identifier`)
       }
-      if (descriptor.name === 'console' || bindings.has(descriptor.name) || errorClassNames.has(descriptor.name)) {
+      if (RESERVED_BINDING_GLOBALS.has(descriptor.name) || bindings.has(descriptor.name) || errorClassNames.has(descriptor.name)) {
         throw new Error(`dsh-code-runtime-worker: duplicate injected global ${JSON.stringify(descriptor.name)}`)
       }
-      if (descriptor.memberNameProperty.length === 0 || RESERVED_ERROR_PROPERTIES.has(descriptor.memberNameProperty)) {
+      const member = descriptor.memberNameProperty
+      if (member.length === 0 || RESERVED_ERROR_PROPERTIES.has(member) || DUNDER_MEMBER.test(member)) {
         throw new Error(`dsh-code-runtime-worker: binding error member property ${JSON.stringify(descriptor.memberNameProperty)} is not usable`)
       }
       errorClassNames.add(descriptor.name)

+ 11 - 0
packages/code-runtime/code-runtime-worker/tests/runtime.spec.ts

@@ -787,6 +787,9 @@ describe('WorkerCodeRuntime — seam misuse and lifecycle', () => {
     const cases: [string, RegExp][] = [
       ['not valid!', /not a usable identifier/],
       ['await', /not a usable identifier/],
+      // `$tools` is legal JS but outside the seam's language-portable subset:
+      // the same namespace list must work against every backend's language.
+      ['$tools', /not a usable identifier/],
       ['console', /duplicate binding global/],
     ]
     for (const [global, message] of cases) {
@@ -822,6 +825,14 @@ describe('WorkerCodeRuntime — seam misuse and lifecycle', () => {
     ])).rejects.toThrow(/duplicate injected global/)
     await expect(run([namespace('tools', 'CallError', '')])).rejects.toThrow(/member property.*not usable/)
     await expect(run([namespace('tools', 'CallError', 'message')])).rejects.toThrow(/member property.*not usable/)
+    // The shared exclusion set covers Python's exception-protocol members and
+    // dunders too, so the same errorClass is valid (or not) on every backend.
+    await expect(run([namespace('tools', 'CallError', 'args')])).rejects.toThrow(/member property.*not usable/)
+    await expect(run([namespace('tools', 'CallError', '__dict__')])).rejects.toThrow(/member property.*not usable/)
+    // The Python bootstrap's owned globals are refused here too (shared
+    // RESERVED_BINDING_GLOBALS), keeping namespace lists backend-portable.
+    await expect(runtime.run({ program: 'return 1', bindings: [{ global: '__dsh_main__', functions: {} }] }))
+      .rejects.toThrow(/duplicate binding global/)
   })
 
   it('rejects config values that are not positive numbers', async () => {

+ 62 - 0
packages/code-runtime/code-runtime/src/index.ts

@@ -17,6 +17,68 @@ export type {
   CodeRunResult,
 } from './types.ts'
 
+/**
+ * Binding globals EVERY backend refuses because SOME backend owns the slot in
+ * the program's namespace: `console` (the worker's log capture), and
+ * `__dsh_main__`/`__builtins__`/`__name__` (the Python bootstrap's wrapper
+ * and seeded module globals), and `__debug__`. One shared set — rather than each backend
+ * refusing only its own slots — keeps the portability promise real: a
+ * namespace list valid on one backend is valid on all, so a caller cannot
+ * pick a name that works on the worker and collides on Python (or vice
+ * versa). Dunder-form names are additionally covered by the identifier rule
+ * on `CodeBindingNamespace.global` only when they fail it; `__name__` et al.
+ * ARE valid identifiers, hence this explicit set. `__debug__` is listed for a
+ * different reason than a collision: CPython compiles a bare `__debug__`
+ * reference to the constant `True` and rejects any assignment to the name at
+ * COMPILE time, so an injected global under that name is unreachable from the
+ * program — accepted by validation, unusable on the Python backend, which is
+ * exactly the split the shared set exists to prevent.
+ */
+export const RESERVED_BINDING_GLOBALS: ReadonlySet<string> = new Set([
+  'console',
+  '__dsh_main__', '__builtins__', '__name__', '__debug__',
+])
+
+/**
+ * `CodeBindingErrorClass.memberNameProperty` names EVERY backend refuses, as
+ * one shared contract so a request valid on one backend is valid on all. The
+ * JS `Error` exclusions (`name`, `message`, `stack`) and Python's
+ * exception-protocol members (`args`, `with_traceback`, `add_note`) are
+ * listed by name; dunder-form names (`__*__`) are refused wholesale — several
+ * are constrained CPython descriptors whose `setattr` raises while
+ * constructing the rejection, and the exact set is an interpreter version
+ * detail. Any other non-empty own property name is accepted everywhere.
+ */
+export const RESERVED_ERROR_MEMBERS: ReadonlySet<string> = new Set([
+  'name', 'message', 'stack',
+  'args', 'with_traceback', 'add_note',
+])
+
+/** Dunder form (`__*__`): object-protocol slots in Python, refused as {@link RESERVED_ERROR_MEMBERS | error members} on every backend. */
+export const DUNDER_MEMBER = /^__.*__$/
+
+/**
+ * Reserved words of EVERY shipped backend language (ECMAScript ∪ Python),
+ * refused as {@link CodeBindingNamespace.global} / error-class names by all
+ * backends. The portable-identifier contract promises a namespace list valid
+ * on one backend is valid on every backend; a per-language check would let
+ * `lambda` pass the TypeScript backend and fail the Python one. Extending the
+ * seam with a new language means widening this union (a breaking review of
+ * existing binding names, by design).
+ */
+export const PORTABLE_RESERVED_WORDS: ReadonlySet<string> = new Set([
+  // ECMAScript reserved words and reserved-in-strict-mode names.
+  'await', 'break', 'case', 'catch', 'class', 'const', 'continue', 'debugger', 'default', 'delete', 'do',
+  'else', 'enum', 'export', 'extends', 'false', 'finally', 'for', 'function', 'if', 'import', 'in',
+  'instanceof', 'new', 'null', 'return', 'super', 'switch', 'this', 'throw', 'true', 'try', 'typeof',
+  'var', 'void', 'while', 'with', 'yield', 'let', 'static', 'implements', 'interface', 'package',
+  'private', 'protected', 'public', 'arguments', 'eval',
+  // Python 3.x keywords and soft keywords not already above ('type' and '_'
+  // are soft keywords: legal names in practice, reserved here for safety).
+  'False', 'None', 'True', 'and', 'as', 'assert', 'async', 'def', 'del', 'elif', 'except', 'from',
+  'global', 'is', 'lambda', 'nonlocal', 'not', 'or', 'pass', 'raise', 'match', 'type', '_',
+])
+
 declare module 'cordis' {
   interface Context {
     codeRuntime: CodeRuntime

+ 14 - 3
packages/code-runtime/code-runtime/src/types.ts

@@ -28,9 +28,14 @@ export type CodeJsonValue = null | boolean | number | string | CodeJsonValue[] |
  * of a particular consumer such as Code Mode.
  */
 export interface CodeBindingErrorClass {
-  /** Constructor global and resulting `Error.name` (must be a usable JS identifier). */
+  /** Constructor global and resulting `Error.name`; same portable identifier rule as {@link CodeBindingNamespace.global}. */
   name: string
-  /** Non-empty own property for the member name; cannot replace `name`, `message`, or `stack`. */
+  /**
+   * Non-empty own property for the member name. The portable exclusion set is
+   * `RESERVED_ERROR_MEMBERS` plus dunder-form names (`__*__`), enforced
+   * identically by every backend; any other name — identifiers or not — is
+   * accepted everywhere.
+   */
   memberNameProperty: string
 }
 
@@ -42,7 +47,13 @@ export interface CodeBindingErrorClass {
  * collisions.
  */
 export interface CodeBindingNamespace {
-  /** The global identifier the program sees (must be a valid JS identifier). */
+  /**
+   * The global identifier the program sees. Must match the LANGUAGE-PORTABLE
+   * identifier subset `[A-Za-z_][A-Za-z0-9_]*` and no language's reserved
+   * words, so the same namespace list works against every backend regardless
+   * of `language` — a JS-only spelling like `$tools` is rejected by design,
+   * not just by the Python backend.
+   */
   global: string
   /** The callable members, keyed by the exact name the program calls. */
   functions: Record<string, CodeBindingFunction>

+ 51 - 0
packages/code-runtime/code-runtime/tests/reserved.spec.ts

@@ -0,0 +1,51 @@
+import { describe, expect, it } from 'vitest'
+import {
+  DUNDER_MEMBER,
+  PORTABLE_RESERVED_WORDS,
+  RESERVED_BINDING_GLOBALS,
+  RESERVED_ERROR_MEMBERS,
+} from '@deepseek-ai/dsh-code-runtime'
+
+/**
+ * The seam owns the portable-identifier exclusion sets so every backend
+ * enforces one contract: a namespace list valid on one backend is valid on
+ * all. These assertions pin the shared membership backends import rather than
+ * re-declare.
+ */
+describe('seam-owned portable identifier exclusions', () => {
+  it('RESERVED_BINDING_GLOBALS covers each backend-owned slot', () => {
+    expect(RESERVED_BINDING_GLOBALS.has('console')).toBe(true)
+    expect(RESERVED_BINDING_GLOBALS.has('__dsh_main__')).toBe(true)
+    expect(RESERVED_BINDING_GLOBALS.has('__builtins__')).toBe(true)
+    expect(RESERVED_BINDING_GLOBALS.has('__name__')).toBe(true)
+    expect(RESERVED_BINDING_GLOBALS.has('__debug__')).toBe(true)
+    expect(RESERVED_BINDING_GLOBALS.has('tools')).toBe(false)
+  })
+
+  it('RESERVED_ERROR_MEMBERS covers the JS Error and Python exception-protocol members', () => {
+    for (const name of ['name', 'message', 'stack', 'args', 'with_traceback', 'add_note']) {
+      expect(RESERVED_ERROR_MEMBERS.has(name)).toBe(true)
+    }
+    expect(RESERVED_ERROR_MEMBERS.has('code')).toBe(false)
+  })
+
+  it('DUNDER_MEMBER matches dunder-form names only', () => {
+    expect(DUNDER_MEMBER.test('__dict__')).toBe(true)
+    expect(DUNDER_MEMBER.test('__init__')).toBe(true)
+    expect(DUNDER_MEMBER.test('_private')).toBe(false)
+    expect(DUNDER_MEMBER.test('name')).toBe(false)
+    expect(DUNDER_MEMBER.test('__mid')).toBe(false)
+  })
+
+  it('PORTABLE_RESERVED_WORDS is the union of ECMAScript and Python reserved words', () => {
+    // ECMAScript-only keyword.
+    expect(PORTABLE_RESERVED_WORDS.has('function')).toBe(true)
+    // Python-only keyword — refused here so the list stays portable.
+    expect(PORTABLE_RESERVED_WORDS.has('lambda')).toBe(true)
+    expect(PORTABLE_RESERVED_WORDS.has('nonlocal')).toBe(true)
+    // Shared keyword.
+    expect(PORTABLE_RESERVED_WORDS.has('class')).toBe(true)
+    // Ordinary identifier is not reserved.
+    expect(PORTABLE_RESERVED_WORDS.has('tools')).toBe(false)
+  })
+})