dsh-ptc-plus 0.3.2 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (199) hide show
  1. package/README.en.md +65 -78
  2. package/README.md +65 -78
  3. package/assets/ptc-plus-bindings-en.png +0 -0
  4. package/assets/ptc-plus-bindings-zh.png +0 -0
  5. package/assets/ptc-plus-repair-en.png +0 -0
  6. package/assets/ptc-plus-repair-zh.png +0 -0
  7. package/assets/ptc-plus-repl-workspace-en.png +0 -0
  8. package/assets/ptc-plus-repl-workspace-zh.png +0 -0
  9. package/assets/ptc-plus-settings-en.png +0 -0
  10. package/assets/ptc-plus-settings-zh.png +0 -0
  11. package/client.js +30596 -742
  12. package/compiler-amaro.cjs +5 -0
  13. package/compiler-amaro.cjs.map +6 -0
  14. package/compiler-asset-124f80c1bba92ff5fe79cfec6e8ad518f476de4a42514cae3919538887f510e8.txt +1 -0
  15. package/compiler-assets.json +3 -0
  16. package/compiler-core.cjs +554 -0
  17. package/compiler-core.cjs.map +6 -0
  18. package/compiler-platform.cjs +2 -0
  19. package/compiler-platform.cjs.map +6 -0
  20. package/compiler-typescript.cjs +388 -0
  21. package/compiler-typescript.cjs.map +6 -0
  22. package/docs/README.md +6 -0
  23. package/docs/adr/0004-declare-program-bindings-at-the-host.md +1 -1
  24. package/docs/adr/0006-rewrite-module-syntax-with-an-ast.md +5 -3
  25. package/docs/adr/0007-separate-worker-transport-from-session-semantics.md +1 -1
  26. package/docs/adr/0009-bound-expensive-acceptance-default.md +2 -0
  27. package/docs/adr/0010-session-log-derived-recovery-tips.md +59 -5
  28. package/docs/adr/0011-isolate-the-local-recovery-tip-provider.md +1 -1
  29. package/docs/adr/0012-preserve-mixed-destructuring-initialization.md +3 -1
  30. package/docs/adr/0013-isolate-repl-redeclaration-convenience.md +3 -1
  31. package/docs/adr/0014-persist-cell-rewrite-policy.md +6 -2
  32. package/docs/adr/0015-preserve-source-positions-through-rewrites.md +1 -1
  33. package/docs/adr/0016-archive-resolved-review-findings.md +1 -1
  34. package/docs/adr/0017-track-the-latest-dsh-public-surface.md +49 -0
  35. package/docs/adr/0018-use-versioned-fixture-for-ab-benchmark.md +3 -3
  36. package/docs/adr/0019-plugin-settings-and-kill-switch.md +5 -3
  37. package/docs/adr/0020-optional-cordis-tools-in-ptc-mode.md +2 -2
  38. package/docs/adr/0021-separate-host-bootstrap-from-session-runtime.md +12 -0
  39. package/docs/adr/0023-global-user-bindings.md +104 -0
  40. package/docs/adr/0024-repl-console-observation.md +33 -0
  41. package/docs/adr/0025-use-versioned-logical-binding-identities.md +119 -0
  42. package/docs/adr/0026-persist-the-delivery-verdict-against-the-verified-candidate.md +21 -0
  43. package/docs/adr/0027-run-the-session-kernel-in-a-killable-helper-process.md +37 -0
  44. package/docs/adr/0028-attach-to-the-host-ptc-execution-seam.md +103 -0
  45. package/docs/adr/0029-keep-plugin-unavailability-out-of-the-host-agent-lifecycle.md +27 -0
  46. package/docs/adr/0030-serve-both-typert-codec-generations-from-one-codec.md +27 -0
  47. package/docs/architecture.md +73 -21
  48. package/docs/binding-review-placement-design.md +200 -0
  49. package/docs/capability-projection.md +1 -1
  50. package/docs/client-ui.md +95 -19
  51. package/docs/durability-design.md +25 -10
  52. package/docs/evaluation.md +78 -6
  53. package/docs/installation.md +41 -6
  54. package/docs/publishing.md +5 -3
  55. package/docs/runtime-reference.md +72 -25
  56. package/docs/semantic-validation.md +29 -0
  57. package/docs/stateful-binding-updates-proposal.md +755 -0
  58. package/docs/user-bindings.en.md +64 -0
  59. package/docs/user-bindings.md +64 -0
  60. package/docs/value-wire.md +3 -3
  61. package/docs/verification.md +57 -0
  62. package/index.js +369 -92
  63. package/internal/ast-traversal.js +5 -2
  64. package/internal/binding-descriptors.js +1 -0
  65. package/internal/binding-pattern-traversal.js +25 -0
  66. package/internal/binding-pattern.js +26 -29
  67. package/internal/binding-source-analysis.js +391 -0
  68. package/internal/binding-update-policy.js +66 -0
  69. package/internal/bounded-worker-round.js +56 -0
  70. package/internal/callable-definition-environment.js +42 -0
  71. package/internal/callable-source-catalog.js +129 -0
  72. package/internal/callable-source-encoding.js +47 -0
  73. package/internal/callable-source-facts.js +459 -0
  74. package/internal/callable-source-format.js +2 -0
  75. package/internal/callable-source-lowering.js +123 -0
  76. package/internal/cell-analysis-contract.js +25 -0
  77. package/internal/cell-analysis.js +240 -290
  78. package/internal/cell-completion.js +15 -0
  79. package/internal/cell-error.js +7 -0
  80. package/internal/cell-parser.js +66 -0
  81. package/internal/cell-rewriter.js +3 -677
  82. package/internal/commonjs-export-evidence.js +63 -0
  83. package/internal/commonjs-source-evidence.js +72 -0
  84. package/internal/compiler-amaro.cjs +2 -0
  85. package/internal/compiler-compression.js +12 -0
  86. package/internal/compiler-data.js +58 -0
  87. package/internal/compiler-descriptors.js +54 -0
  88. package/internal/compiler-dynamic-cache.js +48 -0
  89. package/internal/compiler-entry.js +35 -0
  90. package/internal/compiler-generated-names.js +17 -0
  91. package/internal/compiler-intrinsics.js +84 -0
  92. package/internal/compiler-module-links.js +34 -0
  93. package/internal/compiler-operations.js +61 -0
  94. package/internal/compiler-platform-factory.js +84 -0
  95. package/internal/compiler-platform.js +136 -0
  96. package/internal/compiler-record-roles.js +53 -0
  97. package/internal/compiler-region-output.js +259 -0
  98. package/internal/compiler-resource-lowering.js +103 -0
  99. package/internal/compiler-scope-facts.js +115 -0
  100. package/internal/compiler-service.js +109 -0
  101. package/internal/compiler-source-cache.js +32 -0
  102. package/internal/compiler-source-pieces.js +93 -0
  103. package/internal/compiler-source-regions.js +132 -0
  104. package/internal/compiler-storage-source.js +54 -0
  105. package/internal/compiler-text.js +15 -0
  106. package/internal/compiler-typescript.cjs +2 -0
  107. package/internal/config-spec.js +83 -27
  108. package/internal/cordis-tools-owner.js +130 -21
  109. package/internal/diagnostic.js +27 -1
  110. package/internal/direct-surface-owner.js +76 -23
  111. package/internal/dynamic-binding-evidence.js +16 -0
  112. package/internal/dynamic-environment-compiler.js +779 -0
  113. package/internal/dynamic-environment-integration.js +87 -0
  114. package/internal/dynamic-environment-runtime.js +362 -0
  115. package/internal/dynamic-native-calls.js +150 -0
  116. package/internal/dynamic-native-runtime.js +248 -0
  117. package/internal/dynamic-scope-analysis.js +90 -0
  118. package/internal/edit-transport-owner.js +27 -2
  119. package/internal/execution-seam-compat.js +193 -0
  120. package/internal/failure-reporting.js +118 -18
  121. package/internal/host-rpc.js +66 -0
  122. package/internal/isolated-worker.js +548 -0
  123. package/internal/kernel-child.js +139 -0
  124. package/internal/kernel-worker.js +904 -120
  125. package/internal/language-semantics.js +14 -0
  126. package/internal/legacy-binding-catalog.js +48 -0
  127. package/internal/legacy-cell-analysis.js +600 -0
  128. package/internal/legacy-cell-parser.js +50 -0
  129. package/internal/legacy-cell-rewriter.js +631 -0
  130. package/internal/legacy-repl-convenience.js +291 -0
  131. package/internal/managed-module-operations.js +170 -0
  132. package/internal/module-compilation.js +321 -0
  133. package/internal/module-runtime-platform.js +19 -0
  134. package/internal/module-transform-contract.js +12 -0
  135. package/internal/native-await-declarations.js +68 -0
  136. package/internal/native-root-compilation.js +54 -0
  137. package/internal/native-root-dynamic.js +67 -0
  138. package/internal/recovery-tips.js +27 -15
  139. package/internal/rejected-cell-editor.js +13 -8
  140. package/internal/repl-convenience.js +2 -289
  141. package/internal/repl-memory-projection.js +81 -12
  142. package/internal/repl-observation-interest.js +70 -0
  143. package/internal/repl-rewrite-contract.js +6 -0
  144. package/internal/repl-scope-normalizer.js +2091 -0
  145. package/internal/repl-value-observer.js +114 -0
  146. package/internal/rpc-contract.js +52 -0
  147. package/internal/runtime-bridge-owner.js +172 -28
  148. package/internal/runtime-config.js +48 -52
  149. package/internal/runtime-contexts.js +82 -10
  150. package/internal/runtime-messages.js +111 -0
  151. package/internal/session-cell-executor.js +401 -87
  152. package/internal/session-journal-recovery.js +684 -0
  153. package/internal/session-journal-schema.js +106 -0
  154. package/internal/session-journal.js +184 -654
  155. package/internal/session-log-view.js +42 -9
  156. package/internal/session-runtime.js +256 -95
  157. package/internal/session-state.js +247 -71
  158. package/internal/source-position-map.js +211 -71
  159. package/internal/stateful-module-compiler.js +154 -0
  160. package/internal/stateful-module-runtime.js +303 -0
  161. package/internal/stateful-root-compiler.js +598 -0
  162. package/internal/stateful-root-runtime.js +340 -0
  163. package/internal/typert-codec-compat.js +28 -0
  164. package/internal/typescript-transform.js +29 -0
  165. package/internal/user-binding-authoring.js +70 -0
  166. package/internal/user-binding-console-worker.js +184 -0
  167. package/internal/user-binding-console.js +148 -0
  168. package/internal/user-binding-draft-projection.js +335 -0
  169. package/internal/user-binding-model-context.js +31 -0
  170. package/internal/user-binding-runner.js +56 -0
  171. package/internal/user-bindings-owner.js +1065 -0
  172. package/internal/user-bindings-store.js +293 -0
  173. package/internal/user-bindings.js +353 -0
  174. package/internal/value-wire-schema.js +28 -0
  175. package/internal/value-wire.js +29 -20
  176. package/internal/worker-client.js +57 -67
  177. package/internal/worker-environment.js +45 -0
  178. package/internal/worker-output-relay.js +29 -0
  179. package/internal/worker-shutdown.js +14 -0
  180. package/package.json +75 -7
  181. package/src/client-activity.js +88 -53
  182. package/src/client-authoring-view.js +580 -0
  183. package/src/client-binding-review.js +230 -0
  184. package/src/client-bindings-data.js +29 -0
  185. package/src/client-catalog.js +127 -0
  186. package/src/client-code-editor.js +84 -0
  187. package/src/client-console.js +112 -0
  188. package/src/client-copy.js +370 -0
  189. package/src/client-feature-gates.js +62 -0
  190. package/src/client-host-compat.js +150 -0
  191. package/src/client-indicator-view.js +267 -0
  192. package/src/client-repl-view.js +292 -0
  193. package/src/client-rpc.js +20 -0
  194. package/src/client-settings-view.js +221 -0
  195. package/src/client-styles.js +106 -0
  196. package/src/client-tool-view.js +108 -0
  197. package/src/client-transport.js +159 -0
  198. package/src/client-workbench.js +521 -0
  199. package/src/client.js +233 -615
@@ -1,16 +1,33 @@
1
1
  import { AsyncLocalStorage } from 'node:async_hooks'
2
- import { registerHooks } from 'node:module'
2
+ import { create as createDomain } from 'node:domain'
3
+ import { createRequire, registerHooks } from 'node:module'
4
+ import { managedModuleImport, managedRequire, readModuleImport, statefulModuleLink } from './stateful-module-runtime.js'
5
+ import { staticModuleLinkReference } from './compiler-module-links.js'
6
+ import { compileStatefulModule, createUserModuleCompilationHooks } from './stateful-module-compiler.js'
7
+ import { USER_BINDING_TRANSFORM, LEGACY_USER_BINDING_TRANSFORM, moduleTransformForLanguage } from './module-transform-contract.js'
3
8
  import { isAbsolute, resolve } from 'node:path'
4
9
  import repl from 'node:repl'
5
10
  import { PassThrough } from 'node:stream'
6
11
  import { pathToFileURL } from 'node:url'
7
12
  import { formatWithOptions } from 'node:util'
8
13
  import { MessageChannel, parentPort, workerData } from 'node:worker_threads'
14
+ import { runInContext } from 'node:vm'
9
15
  import { synchronizeBuiltinEsmExports } from './builtin-esm-sync.js'
10
- import { errorDetails, messageOf } from './failure-reporting.js'
16
+ import { createExceptionOriginScope, errorDetails, messageOf, programBindingError } from './failure-reporting.js'
11
17
  import { AMBIENT_GLOBALS, DURABLE_IMPORTS, FORBIDDEN_IMPORTS } from './module-policy.js'
12
18
  import { decodeValue, encodeValue } from './value-wire.js'
19
+ import { LEGACY_USER_BINDINGS_REUSE_POLICY, LIVE_USER_BINDINGS_SHADOW_POLICY, normalizeUserBindingNames } from './session-journal-schema.js'
13
20
  import { installWorkerCwdVirtualization } from './worker-cwd-virtualization.js'
21
+ import { createReplValueObserver, supportsAwaitLexicals, previewBindingValue } from './repl-value-observer.js'
22
+ import { createStatefulRootRuntime } from './stateful-root-runtime.js'
23
+ import { createNativeRootDynamic } from './native-root-dynamic.js'
24
+ import { moduleRuntimeIntrinsics } from './compiler-intrinsics.js'
25
+ import { compilerDescriptors } from './compiler-descriptors.js'
26
+ import { createCellCompletionObserver } from './cell-completion.js'
27
+ import { WORKER_SHUTDOWN_ACKNOWLEDGEMENT, WORKER_SHUTDOWN_REQUEST } from './worker-shutdown.js'
28
+
29
+ const { Object, mapGet, mapSet, mapHas, mapDelete, setHas, setAdd, setDelete } = moduleRuntimeIntrinsics
30
+ const hasProperty = Reflect.has
14
31
 
15
32
  if (parentPort === null) throw new Error('ptc-plus kernel worker started without a parent port')
16
33
  const { port1, port2: channel } = new MessageChannel()
@@ -18,6 +35,7 @@ const { port1, port2: channel } = new MessageChannel()
18
35
  const input = new PassThrough()
19
36
  const output = new PassThrough()
20
37
  output.resume()
38
+ const evaluationScope = new AsyncLocalStorage()
21
39
  const sessionCwd = typeof workerData?.cwd === 'string' ? workerData.cwd : undefined
22
40
  if (sessionCwd !== undefined && !isAbsolute(sessionCwd)) {
23
41
  throw new Error(`ptc-plus session cwd must be absolute, got ${JSON.stringify(sessionCwd)}`)
@@ -30,22 +48,80 @@ const server = repl.start({
30
48
  useGlobal: false,
31
49
  ignoreUndefined: true,
32
50
  })
51
+ // domain.bind exposes its owner on a bound evaluator in older Node REPLs.
52
+ // Newer REPLs honor an explicitly entered domain. Capture before REPL error
53
+ // formatting can evaluate user-defined stack/name getters.
54
+ const errorDomain = server.eval.domain ?? createDomain()
55
+ errorDomain.removeAllListeners('error')
56
+ errorDomain.on('error', error => evaluationScope.getStore()?.(true, error))
33
57
  const context = server.context
58
+ const contextGlobal = runInContext('globalThis', context)
59
+ const completionObserver = createCellCompletionObserver(runInContext('Function', context))
60
+ let replModuleRealm
61
+ const statefulRoots = createStatefulRootRuntime({
62
+ refresh: name => refreshLegacyPublication(name),
63
+ importModule: (source, options) => managedModuleImport(replParent, source, options, replModuleRealm),
64
+ errors: runInContext('({ ReferenceError, TypeError })', context),
65
+ dynamicIntrinsics: runInContext('({intrinsicEval:eval,realmFunction:Function,globalObject:globalThis,errors:{ReferenceError,TypeError,SyntaxError},reflect:Reflect,object:Object})', context),
66
+ readAmbient: name => runInContext(name, context),
67
+ typeofAmbient: name => runInContext(`typeof ${name}`, context),
68
+ canWriteAmbient: name => writableRootProperty(name),
69
+ writeAmbient(name, value, strict) {
70
+ const argument = name === '__ptc_value' ? '__ptc_other_value' : '__ptc_value'
71
+ runInContext(`${strict ? '"use strict";' : ''}(${argument}) => (${name} = ${argument})`, context)(value)
72
+ },
73
+ deleteAmbient: name => runInContext(`delete ${name}`, context),
74
+ hasAmbient: name => setHas(installedGlobals, name)
75
+ ? mapGet(installedGlobalOriginals, name).descriptor !== undefined : hasProperty(context, name),
76
+ // Request namespaces stay on globalThis; source declarations own separate
77
+ // logical storage. Provider overlays retain their existing write-through.
78
+ hasOverlay: name => setHas(installedGlobals, name) && mapGet(dynamicNamespaces, name)?.shadowable !== true,
79
+ publish(name) {
80
+ // Logical lexical identities do not redefine the global object's properties.
81
+ if (mapHas(userBindingSources, name)) recordUserBindingAssignment(name)
82
+ },
83
+ })
84
+ const nativeRootDynamic = createNativeRootDynamic({
85
+ logicalReference: (name, writable) => statefulRoots.legacyReference(name, writable),
86
+ intrinsics: runInContext('({intrinsicEval:eval,realmFunction:Function,globalObject:globalThis,errors:{ReferenceError,TypeError,SyntaxError},reflect:Reflect,object:Object})', context),
87
+ importModule: (source, options) => managedModuleImport(replParent, source, options, replModuleRealm),
88
+ read: name => runInContext(name, context),
89
+ typeOf: name => runInContext(`typeof ${name}`, context),
90
+ write(name, value, strict) {
91
+ const argument = name === '__ptc_value' ? '__ptc_other_value' : '__ptc_value'
92
+ runInContext(`${strict ? '"use strict";' : ''}(${argument}) => (${name} = ${argument})`, context)(value)
93
+ },
94
+ remove: name => runInContext(`delete ${name}`, context),
95
+ })
96
+ let valueObserver
34
97
  const REPL_IMPORT_CANARY = 'data:text/javascript,export default 1'
35
98
  let replParent
36
99
  const sessionReplParent = sessionCwd === undefined ? undefined : pathToFileURL(resolve(sessionCwd, 'repl')).href
37
100
  const staticAdapterParents = new Set()
101
+ const userBindingModuleParents = new Map()
102
+ const userModuleCompilation = createUserModuleCompilationHooks({
103
+ transformForParent: parent => (parent === replParent || parent === sessionReplParent) && activeExecution !== undefined
104
+ ? moduleTransformForLanguage(activeExecution.languageSemantics) : undefined,
105
+ })
38
106
  registerHooks({
39
107
  resolve(specifier, context, nextResolve) {
40
108
  if (specifier === REPL_IMPORT_CANARY && replParent === undefined) replParent = context.parentURL
41
- return nextResolve(specifier, context.parentURL === replParent || staticAdapterParents.has(context.parentURL)
42
- ? { ...context, parentURL: sessionReplParent ?? replParent }
43
- : context)
109
+ return userModuleCompilation.resolve(specifier, context, (source, resolvedContext) => {
110
+ const userBindingParent = mapGet(userBindingModuleParents, resolvedContext.parentURL)
111
+ return nextResolve(source, userBindingParent !== undefined
112
+ ? { ...resolvedContext, parentURL: userBindingParent }
113
+ : resolvedContext.parentURL === replParent || setHas(staticAdapterParents, resolvedContext.parentURL)
114
+ ? { ...resolvedContext, parentURL: sessionReplParent ?? replParent }
115
+ : resolvedContext)
116
+ })
44
117
  },
118
+ load: userModuleCompilation.load,
45
119
  })
46
120
  const logScope = new AsyncLocalStorage()
121
+ const userBindingActivationScope = new AsyncLocalStorage()
47
122
  const pending = new Map()
48
123
  const installedGlobals = new Set()
124
+ const installedGlobalOriginals = new Map()
49
125
  const PROCESS_CONTROLS = new Set(['exit', 'abort', 'kill', 'chdir'])
50
126
  const CELL_FRAME_SUFFIX = '\n;'
51
127
  let filenameSequence = 0
@@ -63,6 +139,13 @@ let activeExecution
63
139
  let pendingVolatileReason
64
140
  let nextCallId = 0
65
141
  let nextStaticAdapterId = 0
142
+ let nextUserBindingModuleId = 0
143
+ const userBindingEntries = new Map()
144
+ const userBindingNames = new Map()
145
+ const userBindingSources = new Map()
146
+ const dynamicNamespaces = new Map()
147
+ const originalDynamicNamespaceGlobals = new Map()
148
+ let retainedUserBindingRuntime = false
66
149
 
67
150
  class StaticImportFailure {
68
151
  constructor(error, position) {
@@ -76,6 +159,10 @@ class CellReturn extends Error {
76
159
  super('cell returned')
77
160
  this.value = value
78
161
  }
162
+
163
+ static complete() {
164
+ evaluationScope.getStore().completed = true
165
+ }
79
166
  }
80
167
  function appendLog(...values) {
81
168
  const current = logScope.getStore()
@@ -155,15 +242,22 @@ installWorkerCwdVirtualization(sessionCwd, originalRequire)
155
242
  synchronizeBuiltinEsmExports()
156
243
  const originalGlobals = Object.fromEntries(
157
244
  [...AMBIENT_GLOBALS].filter(name => name !== 'require')
158
- .map(name => [name, globalThis[name]]),
245
+ .map(name => [name, name === 'eval' || name === 'Function' ? runInContext(name, context) : globalThis[name]]),
159
246
  )
247
+ const providedRequire = sessionReplParent === undefined ? originalRequire : createRequire(sessionReplParent)
248
+ function selectProvidedRequire(args) {
249
+ const specifier = args[0]
250
+ if (FORBIDDEN_IMPORTS.has(specifier)) throw new Error(`module ${specifier} is forbidden because it exposes kernel control`)
251
+ if (!DURABLE_IMPORTS.has(specifier)) markVolatile(`require(${JSON.stringify(specifier)})`)
252
+ return managedRequire(activeExecution?.languageSemantics === 'legacy-v1' ? replParent : sessionReplParent ?? replParent,
253
+ activeExecution?.languageSemantics === 'legacy-v1' ? originalRequire : providedRequire)
254
+ }
160
255
  Object.defineProperty(context, 'require', {
161
256
  configurable: true,
162
- value(specifier) {
163
- if (FORBIDDEN_IMPORTS.has(specifier)) throw new Error(`module ${specifier} is forbidden because it exposes kernel control`)
164
- if (!DURABLE_IMPORTS.has(specifier)) markVolatile(`require(${JSON.stringify(specifier)})`)
165
- return originalRequire(specifier)
166
- },
257
+ value: new Proxy(providedRequire, {
258
+ apply: (_, receiver, args) => Reflect.apply(selectProvidedRequire(args), receiver, args),
259
+ construct: (_, args, newTarget) => Reflect.construct(selectProvidedRequire(args), args, newTarget),
260
+ }),
167
261
  })
168
262
 
169
263
  for (const [name, value] of Object.entries(originalGlobals)) {
@@ -220,87 +314,154 @@ Object.defineProperty(context, 'Math', {
220
314
  value: Object.freeze(mathView),
221
315
  })
222
316
 
223
- function evaluate(program) {
317
+ function evaluate(program, completionSignal, asyncCompletion = false) {
224
318
  return new Promise((resolve, reject) => {
225
- // REPLServer routes runtime throws to its domain instead of the eval
226
- // callback. Cells are serialized, so temporarily replacing that one error
227
- // handler gives both syntax and runtime failures one settlement path.
228
- const domain = server._domain
229
- const prior = domain.listeners('error')
230
- domain.removeAllListeners('error')
231
319
  let settled = false
232
- const finish = (error, value) => {
320
+ const finish = (failed, value) => {
233
321
  if (settled) return
234
322
  settled = true
235
- domain.removeListener('error', onError)
236
- for (const listener of prior) domain.on('error', listener)
237
- if (error instanceof CellReturn) {
238
- Promise.resolve(error.value).then(
323
+ if (failed && value instanceof CellReturn) {
324
+ completionObserver.settle(value.value,
239
325
  value => resolve({ hasValue: true, value }),
240
326
  reject,
241
327
  )
242
- } else if (error) reject(error)
328
+ } else if (failed) reject(value)
243
329
  else {
244
- Promise.resolve(value).then(
330
+ completionObserver.settle(value,
245
331
  value => resolve({ hasValue: value !== undefined, value }),
246
332
  reject,
247
333
  )
248
334
  }
249
335
  }
250
- const onError = error => finish(error)
251
- domain.on('error', onError)
252
336
  activeFilename = `ptc-plus-repl-${++filenameSequence}`
253
- server.eval(program + CELL_FRAME_SUFFIX, context, activeFilename, finish)
337
+ evaluationScope.run(finish, () => errorDomain.run(() => {
338
+ // Older REPL callbacks conflate null/undefined await rejection with a
339
+ // successful empty result. A reached EOF distinguishes those outcomes.
340
+ // A block-scoped declaration has empty completion and preserves the
341
+ // preceding non-await expression value without adding a session binding.
342
+ // Keep the final semicolon so REPL cannot guess a block is an object literal.
343
+ const suffix = completionSignal === undefined || asyncCompletion ? CELL_FRAME_SUFFIX
344
+ : `${CELL_FRAME_SUFFIX}{ let completed = this[${JSON.stringify(completionSignal)}].complete(); }${CELL_FRAME_SUFFIX}`
345
+ server.eval(program + suffix, context, activeFilename, (error, value) => {
346
+ if (asyncCompletion && (error === null || error === undefined)) {
347
+ completionObserver.observe(value, value => finish(false, value), error => finish(true, error))
348
+ return
349
+ }
350
+ const failed = error !== null && error !== undefined || completionSignal !== undefined && finish.completed !== true
351
+ finish(failed, failed ? error : value)
352
+ })
353
+ }))
254
354
  })
255
355
  }
256
356
 
257
- function staticImportAttributes(options) {
258
- if (options === undefined) return ''
357
+ async function verifyEvaluation() {
358
+ await evaluate(CONFORMANCE_CELL)
359
+ const marker = new Error('PTC Plus REPL settlement probe')
360
+ Object.defineProperty(marker, 'stack', { get() { throw new Error('REPL must not format an error before settlement') } })
361
+ context.__ptc_settlement_probe__ = marker
362
+ context.__ptc_completion_probe__ = CellReturn
363
+ try {
364
+ for (const value of [marker, null, undefined, false, 0]) {
365
+ context.__ptc_settlement_probe__ = value
366
+ for (const source of ['throw __ptc_settlement_probe__', 'await Promise.reject(__ptc_settlement_probe__)']) {
367
+ let caught = false
368
+ try { await evaluate(source, '__ptc_completion_probe__') } catch (error) {
369
+ if (error !== value) throw new Error('REPL did not preserve the original thrown value')
370
+ caught = true
371
+ }
372
+ if (!caught) throw new Error('REPL did not reject a failed evaluation')
373
+ }
374
+ }
375
+ let syntaxRejected = false
376
+ try { await evaluate('const =') } catch (error) { syntaxRejected = error?.name === 'SyntaxError' }
377
+ if (!syntaxRejected) throw new Error('REPL did not preserve syntax failure')
378
+ context.__ptc_settlement_probe__ = new CellReturn(Promise.resolve(marker))
379
+ const returned = await evaluate('throw __ptc_settlement_probe__')
380
+ if (!returned.hasValue || returned.value !== marker) throw new Error('REPL did not preserve cell return')
381
+ await evaluate(CONFORMANCE_CELL)
382
+ await evaluate('{ const value = await Promise.resolve(42); if (value !== 42) throw new Error("invalid block await") }', '__ptc_completion_probe__')
383
+ const expression = await evaluate('42', '__ptc_completion_probe__')
384
+ if (expression.value !== 42) throw new Error('REPL did not preserve expression completion')
385
+ } finally {
386
+ delete context.__ptc_settlement_probe__
387
+ delete context.__ptc_completion_probe__
388
+ }
389
+ }
390
+
391
+ function staticImportClause(options) {
392
+ if (options === undefined) return { attributes: undefined, clause: '' }
259
393
  const [keyword, attributes] = Object.entries(options)[0]
260
394
  const entries = Object.entries(attributes)
261
395
  .map(([key, value]) => `${JSON.stringify(key)}: ${JSON.stringify(value)}`)
262
- return ` ${keyword} { ${entries.join(', ')} }`
396
+ return { attributes, clause: ` ${keyword} { ${entries.join(', ')} }` }
263
397
  }
264
398
 
265
399
  function staticAdapterSource(load) {
266
- const source = JSON.stringify(load.source)
267
- const attributes = staticImportAttributes(load.options)
268
- if (load.global === undefined) return `import ${source}${attributes};`
400
+ const { attributes, clause } = staticImportClause(load.options)
401
+ const source = JSON.stringify(statefulModuleLink(load.source, staticModuleLinkReference(load.source, attributes)))
402
+ if (load.global === undefined) return `import ${source}${clause};`
269
403
  const requirements = load.requiredExports?.map((name, index) => {
270
404
  const imported = name === 'default' ? 'default' : JSON.stringify(name)
271
405
  return `${imported} as __required_${index}__`
272
406
  }) ?? []
273
407
  return [
274
- `import * as namespace from ${source}${attributes};`,
408
+ `import * as namespace from ${source}${clause};`,
275
409
  ...(requirements.length === 0 ? [] : [
276
- `export { ${requirements.join(', ')} } from ${source}${attributes};`,
410
+ `export { ${requirements.join(', ')} } from ${source}${clause};`,
277
411
  ]),
278
412
  'export { namespace };',
279
413
  ].join('\n')
280
414
  }
281
415
 
282
416
  async function loadStaticModule(load) {
417
+ if (load.operation === 'native-dynamic') return { namespace: nativeRootDynamic.environment(load.callableSources,
418
+ load.awaitRoot === true, load.logicalRoots.filter(name => statefulRoots.has(name)
419
+ && rootBindingStorage(name, moduleRuntimeIntrinsics.includes(load.nativeLexicals, name)) !== 'lexical'), load.writableRoots) }
420
+ if (load.operation === 'import') {
421
+ return { namespace: (source, options) => managedModuleImport(replParent, source, options, replModuleRealm) }
422
+ }
283
423
  const adapter = `data:text/javascript,${encodeURIComponent(staticAdapterSource(load))}#${++nextStaticAdapterId}`
284
424
  staticAdapterParents.add(adapter)
425
+ userModuleCompilation.mark(adapter, { compiled: true,
426
+ transform: moduleTransformForLanguage(activeExecution?.languageSemantics) })
285
427
  try {
286
428
  const completion = await evaluate(`import(${JSON.stringify(adapter)})`)
287
- return completion.value.namespace
429
+ if (load.global === undefined) return { namespace: undefined }
430
+ const namespace = readModuleImport(adapter, load.source, null, () => completion.value.namespace, load.options?.with)
431
+ return { namespace }
288
432
  } finally {
289
433
  staticAdapterParents.delete(adapter)
290
434
  }
291
435
  }
292
436
 
293
- function callHost(runId, global, member, args, errorClass) {
294
- if (activeRun !== runId) return Promise.reject(new Error('PTC execution lease expired'))
437
+ function hasExecutionLease(runId) {
438
+ return runId !== undefined && activeRun === runId && logScope.getStore()?.id === runId
439
+ }
440
+
441
+ // BoundError is the error class installed by this cell's wrapper, not a name
442
+ // looked up later: the cell can replace or delete the context binding while a
443
+ // call is pending, and the call must keep its submitted error identity.
444
+ function callHost(runId, global, member, args, BoundError) {
445
+ if (!hasExecutionLease(runId)) {
446
+ return Promise.reject(programBindingError('lease', 'PTC execution lease expired'))
447
+ }
448
+ const activation = userBindingActivationScope.getStore()
449
+ if (activation !== undefined) {
450
+ activation.called = true
451
+ if (activation.failed) {
452
+ markVolatile(`failed user binding ${JSON.stringify(activation.id)} issued a host call`)
453
+ }
454
+ }
295
455
  const id = ++nextCallId
456
+ const valueLimits = activeExecution.valueLimits
296
457
  let settle
297
- const result = new Promise((resolve, reject) => { settle = { resolve, reject, errorClass, member } })
298
- void result.catch(() => {})
299
- pending.set(id, { ...settle, runId })
458
+ const result = new Promise((resolve, reject) => { settle = { resolve, reject } })
459
+ const settled = result.then(() => {}, () => {})
460
+ pending.set(id, { ...settle, runId, valueLimits, settled, member, BoundError })
300
461
  try {
301
462
  channel.postMessage({
302
463
  type: 'call', runId, id, global, member,
303
- args: encodeValue(args, activeExecution?.valueLimits),
464
+ args: encodeValue(args, valueLimits),
304
465
  })
305
466
  } catch (error) {
306
467
  pending.delete(id)
@@ -309,13 +470,116 @@ function callHost(runId, global, member, args, errorClass) {
309
470
  return result
310
471
  }
311
472
 
473
+ function dynamicNamespace(name) {
474
+ const assertLease = () => {
475
+ if (!hasExecutionLease(logScope.getStore()?.id)) {
476
+ throw programBindingError('lease', 'PTC execution lease expired')
477
+ }
478
+ }
479
+ return new Proxy(Object.create(null), {
480
+ get(_target, property) {
481
+ if (typeof property !== 'string') return undefined
482
+ assertLease()
483
+ const descriptor = dynamicNamespaces.get(name)
484
+ if (descriptor === undefined || !descriptor.members.has(property)) return undefined
485
+ return (...args) => {
486
+ if (!hasExecutionLease(logScope.getStore()?.id)) {
487
+ return Promise.reject(programBindingError('lease', 'PTC execution lease expired'))
488
+ }
489
+ const current = dynamicNamespaces.get(name)
490
+ if (current === undefined || !current.members.has(property)) {
491
+ return Promise.reject(programBindingError('capability', `unknown binding ${name}.${property}`))
492
+ }
493
+ return callHost(
494
+ logScope.getStore()?.id,
495
+ name,
496
+ property,
497
+ args.length === 0 && current.emptyObjectMembers.has(property) ? {} : args[0],
498
+ current.BoundError,
499
+ )
500
+ }
501
+ },
502
+ has(_target, property) {
503
+ assertLease()
504
+ return typeof property === 'string' && dynamicNamespaces.get(name)?.members.has(property) === true
505
+ },
506
+ ownKeys() {
507
+ assertLease()
508
+ return [...dynamicNamespaces.get(name)?.members ?? []]
509
+ },
510
+ getOwnPropertyDescriptor(_target, property) {
511
+ assertLease()
512
+ return typeof property === 'string' && dynamicNamespaces.get(name)?.members.has(property) === true
513
+ ? compilerDescriptors.descriptor({ configurable: true, enumerable: true })
514
+ : undefined
515
+ },
516
+ set() { return false },
517
+ })
518
+ }
519
+
520
+ function installDynamicNamespaceGlobals() {
521
+ const pendingGlobals = []
522
+ for (const name of dynamicNamespaces.keys()) {
523
+ if (originalDynamicNamespaceGlobals.has(name)) continue
524
+ const original = Object.getOwnPropertyDescriptor(globalThis, name)
525
+ if (original !== undefined) {
526
+ throw new Error(
527
+ `program namespace ${JSON.stringify(name)} cannot be bridged into Global User Binding modules because the worker global already exists`,
528
+ )
529
+ }
530
+ pendingGlobals.push(name)
531
+ }
532
+ for (const name of pendingGlobals) {
533
+ originalDynamicNamespaceGlobals.set(name, undefined)
534
+ Object.defineProperty(globalThis, name, {
535
+ configurable: true,
536
+ writable: false,
537
+ value: dynamicNamespace(name),
538
+ })
539
+ }
540
+ }
541
+
542
+ function restoreDynamicNamespaceGlobals() {
543
+ // installDynamicNamespaceGlobals refuses an existing worker global, so every
544
+ // recorded bridge install owns a previously absent name.
545
+ for (const name of originalDynamicNamespaceGlobals.keys()) delete globalThis[name]
546
+ originalDynamicNamespaceGlobals.clear()
547
+ }
548
+
312
549
  function installBindings(message) {
313
- for (const name of installedGlobals) delete context[name]
550
+ for (const name of installedGlobals) {
551
+ const original = installedGlobalOriginals.get(name)
552
+ if (original.userGlobalEntryId !== undefined
553
+ && (userBindingNames.get(name) !== original.userGlobalEntryId
554
+ || userBindingEntries.get(original.userGlobalEntryId)?.descriptors.get(name) !== original.attachment)) {
555
+ delete context[name]
556
+ } else if (original.descriptor === undefined) delete context[name]
557
+ else Object.defineProperty(context, name, original.descriptor)
558
+ }
314
559
  installedGlobals.clear()
560
+ installedGlobalOriginals.clear()
561
+ dynamicNamespaces.clear()
315
562
 
316
563
  for (const namespace of message.namespaces) {
564
+ const descriptor = namespace.errorClass
565
+ // The wrapper owns the constructor, so a pending call keeps its submitted
566
+ // error identity even after the cell shadows the context binding.
567
+ const BoundError = descriptor === undefined ? undefined : class extends Error {
568
+ constructor(member, detail, cause) {
569
+ super(detail)
570
+ this.name = descriptor.name
571
+ Object.defineProperty(this, descriptor.memberNameProperty, { enumerable: true, value: member })
572
+ if (cause !== undefined) Object.defineProperty(this, 'ptcCause', { value: cause })
573
+ }
574
+ }
317
575
  const view = Object.create(null)
318
576
  const emptyObjectMembers = new Set(namespace.emptyObjectMembers ?? [])
577
+ dynamicNamespaces.set(namespace.global, {
578
+ members: new Set(namespace.members),
579
+ shadowable: namespace.shadowable === true,
580
+ emptyObjectMembers,
581
+ BoundError,
582
+ })
319
583
  for (const member of namespace.members) {
320
584
  Object.defineProperty(view, member, {
321
585
  enumerable: true,
@@ -324,36 +588,511 @@ function installBindings(message) {
324
588
  namespace.global,
325
589
  member,
326
590
  args.length === 0 && emptyObjectMembers.has(member) ? {} : args[0],
327
- namespace.errorClass,
591
+ BoundError,
328
592
  ),
329
593
  })
330
594
  }
331
595
  Object.freeze(view)
596
+ installedGlobalOriginals.set(namespace.global, capturedGlobalDescriptor(namespace.global))
332
597
  Object.defineProperty(context, namespace.global, { configurable: true, value: view })
333
598
  installedGlobals.add(namespace.global)
334
599
 
335
- if (namespace.errorClass !== undefined) {
336
- const descriptor = namespace.errorClass
337
- const BoundError = class extends Error {
338
- constructor(member, detail, cause) {
339
- super(detail)
340
- this.name = descriptor.name
341
- Object.defineProperty(this, descriptor.memberNameProperty, { enumerable: true, value: member })
342
- if (cause !== undefined) Object.defineProperty(this, 'ptcCause', { value: cause })
343
- }
344
- }
600
+ if (descriptor !== undefined) {
601
+ installedGlobalOriginals.set(descriptor.name, capturedGlobalDescriptor(descriptor.name))
345
602
  Object.defineProperty(context, descriptor.name, { configurable: true, value: BoundError })
346
603
  installedGlobals.add(descriptor.name)
347
604
  }
348
605
  }
349
606
  }
350
607
 
608
+ function capturedGlobalDescriptor(name) {
609
+ const descriptor = Object.getOwnPropertyDescriptor(context, name)
610
+ const userGlobalEntryId = userBindingNames.get(name)
611
+ const installed = userBindingEntries.get(userGlobalEntryId)?.descriptors.get(name)
612
+ return {
613
+ descriptor,
614
+ ...(userGlobalEntryId !== undefined && descriptorsEqual(descriptor, installed)
615
+ ? { userGlobalEntryId, attachment: installed }
616
+ : {}),
617
+ }
618
+ }
619
+
620
+ function underlyingUserBindingDescriptor(name) {
621
+ return installedGlobals.has(name)
622
+ ? installedGlobalOriginals.get(name).descriptor
623
+ : Object.getOwnPropertyDescriptor(context, name)
624
+ }
625
+
626
+ function recordUserBindingAssignment(name, legacy = false) {
627
+ if (!legacy || activeExecution?.userBindingsShadowPolicy === LIVE_USER_BINDINGS_SHADOW_POLICY) {
628
+ if (!setHas(installedGlobals, name)) {
629
+ mapDelete(userBindingNames, name)
630
+ mapSet(userBindingSources, name, { state: 'local' })
631
+ }
632
+ } else {
633
+ // Historical setters record invocation before defining the property. A
634
+ // restored accessor does not erase that operation, even for the same value.
635
+ if (activeExecution !== undefined) setAdd(activeExecution.legacyAssignedUserBindingNames, name)
636
+ }
637
+ }
638
+
639
+ // A temporary private getter distinguishes native lexical storage without
640
+ // invoking provider/user getters or comparing arbitrary values. A failed read
641
+ // proves no initialized value; it must never be promoted from the static catalog.
642
+ function rootBindingStorage(name, nativeLexical = false) {
643
+ const original = Object.getOwnPropertyDescriptor(context, name)
644
+ if (original?.configurable === false) {
645
+ if (!nativeLexical && !Object.hasOwn(original, 'value')) return 'unknown'
646
+ try {
647
+ // Both an own data property and an initialized lexical can be read
648
+ // without executing an accessor. Either proves an available local name.
649
+ runInContext(name, context, { displayErrors: false })
650
+ return nativeLexical ? 'lexical' : 'local'
651
+ } catch {
652
+ return 'unknown'
653
+ }
654
+ }
655
+ let propertyRead = false
656
+ try {
657
+ Object.defineProperty(context, name, { configurable: true, get() { propertyRead = true } })
658
+ runInContext(name, context, { displayErrors: false })
659
+ return propertyRead ? 'property' : 'lexical'
660
+ } catch {
661
+ return 'unknown'
662
+ } finally {
663
+ if (original === undefined) delete context[name]
664
+ else Object.defineProperty(context, name, original)
665
+ }
666
+ }
667
+
668
+ function writableRootProperty(name) {
669
+ const descriptor = Object.getOwnPropertyDescriptor(context, name)
670
+ return descriptor !== undefined && (Object.hasOwn(descriptor, 'value')
671
+ ? descriptor.writable === true : typeof descriptor.set === 'function')
672
+ }
673
+
674
+ function userBindingRootStorage(name) {
675
+ if (statefulRoots.has(name)) return 'local'
676
+ const namespace = activeExecution.importBindingNamespaces?.get(name)
677
+ if (namespace === undefined) return rootBindingStorage(name)
678
+ // Compiler-validated imports use native namespace slots, not public alias
679
+ // properties. Probe only the slot; reading an export could invoke user code.
680
+ return rootBindingStorage(namespace) === 'lexical' ? 'lexical' : 'unknown'
681
+ }
682
+
683
+ function reconcileUserBindingNames() {
684
+ for (const name of userBindingSources.keys()) {
685
+ const storage = userBindingRootStorage(name)
686
+ const descriptor = underlyingUserBindingDescriptor(name)
687
+ const id = userBindingNames.get(name)
688
+ const installed = userBindingEntries.get(id)?.descriptors.get(name)
689
+ const state = storage === 'lexical' || storage === 'local' ? 'local'
690
+ : storage === 'unknown' ? 'unknown'
691
+ : descriptor === undefined ? 'absent'
692
+ : installed !== undefined && descriptorsEqual(descriptor, installed) ? 'provider' : 'local'
693
+ userBindingSources.set(name, { state, ...(state === 'provider' ? { entryId: id } : {}) })
694
+ if (state !== 'provider') userBindingNames.delete(name)
695
+ }
696
+ }
697
+
698
+ function removePerNameUserBindingEntry(id) {
699
+ const entry = userBindingEntries.get(id)
700
+ for (const name of entry.names) {
701
+ if (userBindingNames.get(name) !== id) continue
702
+ const installed = entry.descriptors.get(name)
703
+ userBindingNames.delete(name)
704
+ if (installedGlobals.has(name)) {
705
+ const original = installedGlobalOriginals.get(name)
706
+ if (original.attachment === installed) original.descriptor = undefined
707
+ } else if (descriptorsEqual(Object.getOwnPropertyDescriptor(context, name), installed)) delete context[name]
708
+ // Lifecycle removal permits reattachment; explicit deletion retains absent.
709
+ userBindingSources.delete(name)
710
+ }
711
+ userBindingEntries.delete(id)
712
+ }
713
+
714
+ async function activatePerNameUserBindings(snapshot, shadowedNames, cwd, reusePolicy, initialFailures = []) {
715
+ // Import preceding legacy attachments into the new source owner once.
716
+ for (const [name, entryId] of userBindingNames) {
717
+ if (userBindingSources.has(name)) continue
718
+ userBindingSources.set(name, { state: 'provider', entryId })
719
+ // The legacy catalog includes actual setter writes as well as descriptor
720
+ // changes. Do not reattach a restored local accessor by descriptor equality.
721
+ if (shadowedNames.has(name) && !installedGlobals.has(name)) userBindingNames.delete(name)
722
+ }
723
+ reconcileUserBindingNames()
724
+ const blockedIds = new Set(initialFailures.map(failure => failure.id))
725
+ const desired = new Map((snapshot?.entries ?? []).filter(entry => !blockedIds.has(entry.id)).map(entry => [entry.id, entry]))
726
+ try {
727
+ if (desired.size > 0 || retainedUserBindingRuntime) installDynamicNamespaceGlobals()
728
+ } catch (error) {
729
+ for (const id of userBindingEntries.keys()) removePerNameUserBindingEntry(id)
730
+ return { activated: [], failures: [...initialFailures, ...[...desired.keys()].map(id => ({ id, error: messageOf(error) }))], error }
731
+ }
732
+ for (const [id, current] of userBindingEntries) {
733
+ const next = desired.get(id)
734
+ if (next === undefined || current.transform !== snapshot.transform || !(reusePolicy === LEGACY_USER_BINDINGS_REUSE_POLICY
735
+ ? next.fingerprint === current.entry.fingerprint : userBindingImplementationMatches(next, current.entry))) {
736
+ removePerNameUserBindingEntry(id)
737
+ }
738
+ }
739
+ const activated = []
740
+ const failures = [...initialFailures]
741
+ for (const entry of desired.values()) {
742
+ const current = userBindingEntries.get(entry.id)
743
+ if (current !== undefined) {
744
+ current.entry = entry
745
+ activated.push(entry.id)
746
+ continue
747
+ }
748
+ const names = entry.scope === 'namespace' ? [entry.name] : entry.symbols
749
+ const previousSources = new Map(names.map(name => [name, userBindingSources.get(name)]))
750
+ const installedNames = []
751
+ let evaluated
752
+ const activation = { id: entry.id, called: false, failed: false }
753
+ try {
754
+ if (entry.durability === 'volatile') markVolatile(`user binding ${JSON.stringify(entry.id)}: ${entry.volatileReason ?? 'non-replayable source'}`)
755
+ evaluated = await userBindingActivationScope.run(activation, () => evaluateUserBinding(entry, cwd, snapshot.transform))
756
+ const namespace = evaluated.namespace
757
+ for (const symbol of entry.symbols) {
758
+ if (!Object.hasOwn(namespace, symbol)) throw new Error(`named export ${JSON.stringify(symbol)} is unavailable after evaluation`)
759
+ }
760
+ const view = Object.create(null)
761
+ for (const symbol of entry.symbols) Object.defineProperty(view, symbol, { enumerable: true, get: () => namespace[symbol] })
762
+ Object.freeze(view)
763
+ const descriptors = new Map()
764
+ for (const name of names) {
765
+ const storage = userBindingRootStorage(name)
766
+ const prior = userBindingSources.get(name)
767
+ if (storage === 'lexical' || storage === 'unknown') {
768
+ userBindingSources.set(name, { state: storage === 'lexical' ? 'local' : 'unknown' })
769
+ } else if (prior === undefined && shadowedNames.has(name)) {
770
+ const descriptor = underlyingUserBindingDescriptor(name)
771
+ userBindingSources.set(name, { state: descriptor === undefined ? 'unknown' : 'local' })
772
+ }
773
+ const source = userBindingSources.get(name)
774
+ if (source !== undefined && source.state !== 'provider') continue
775
+ const descriptor = Object.getOwnPropertyDescriptor(context, name)
776
+ Object.defineProperty(context, name, {
777
+ configurable: true,
778
+ enumerable: true,
779
+ get: () => entry.scope === 'namespace' ? view : namespace[name],
780
+ set(value) {
781
+ const receiver = this === contextGlobal ? context : this
782
+ Object.defineProperty(receiver, name, { configurable: true, enumerable: true, writable: true, value })
783
+ if (receiver === context) recordUserBindingAssignment(name)
784
+ },
785
+ })
786
+ installedNames.push({ name, descriptor })
787
+ descriptors.set(name, Object.getOwnPropertyDescriptor(context, name))
788
+ userBindingNames.set(name, entry.id)
789
+ userBindingSources.set(name, { state: 'provider', entryId: entry.id })
790
+ }
791
+ userBindingEntries.set(entry.id, { entry, transform: snapshot.transform, moduleUrl: evaluated.moduleUrl, names, descriptors })
792
+ retainedUserBindingRuntime = true
793
+ activated.push(entry.id)
794
+ } catch (error) {
795
+ activation.failed = true
796
+ if (activation.called) markVolatile(`failed user binding ${JSON.stringify(entry.id)} issued a host call`)
797
+ for (const installed of installedNames.reverse()) {
798
+ userBindingNames.delete(installed.name)
799
+ if (installed.descriptor === undefined) delete context[installed.name]
800
+ else Object.defineProperty(context, installed.name, installed.descriptor)
801
+ }
802
+ for (const [name, prior] of previousSources) {
803
+ if (prior === undefined) userBindingSources.delete(name)
804
+ else userBindingSources.set(name, prior)
805
+ }
806
+ if (evaluated !== undefined) userBindingModuleParents.delete(evaluated.moduleUrl)
807
+ failures.push({ id: entry.id, error: messageOf(error) })
808
+ }
809
+ }
810
+ if (userBindingEntries.size === 0 && !retainedUserBindingRuntime) restoreDynamicNamespaceGlobals()
811
+ return { activated, failures }
812
+ }
813
+
814
+ function removeUserBindingEntry(id, shadowedNames) {
815
+ const previous = userBindingEntries.get(id)
816
+ if (previous === undefined) return
817
+ for (const name of previous.names) {
818
+ if (userBindingNames.get(name) !== id) continue
819
+ userBindingNames.delete(name)
820
+ if (!shadowedNames.has(name)) delete context[name]
821
+ }
822
+ userBindingEntries.delete(id)
823
+ }
824
+
825
+ function descriptorsEqual(left, right) {
826
+ if (left === undefined || right === undefined) return left === right
827
+ return left.configurable === right.configurable
828
+ && left.enumerable === right.enumerable
829
+ && left.get === right.get
830
+ && left.set === right.set
831
+ && left.value === right.value
832
+ && left.writable === right.writable
833
+ }
834
+
835
+ function reconcileUserBindingShadows(shadowedNames) {
836
+ for (const [id, entry] of userBindingEntries) {
837
+ for (const name of entry.names) {
838
+ if (userBindingNames.get(name) !== id) continue
839
+ const installed = entry.descriptors.get(name)
840
+ const current = Object.getOwnPropertyDescriptor(context, name)
841
+ if (!descriptorsEqual(current, installed)) shadowedNames.add(name)
842
+ }
843
+ }
844
+ }
845
+
846
+ async function evaluateUserBinding(entry, cwd, transform) {
847
+ if (typeof cwd !== 'string' || !isAbsolute(cwd)) {
848
+ throw new Error('user binding activation requires an absolute storage directory')
849
+ }
850
+ const prepared = compileStatefulModule(entry.source, { transform })
851
+ const url = `data:text/javascript,${encodeURIComponent(prepared.code)}#ptc-plus-${entry.fingerprint}-${++nextUserBindingModuleId}`
852
+ userBindingModuleParents.set(url, pathToFileURL(resolve(cwd, 'bindings.json')).href)
853
+ userModuleCompilation.mark(url, { transform, compiled: true, moduleInterface: prepared.moduleInterface, sourceRegions: prepared.sourceRegions })
854
+ try {
855
+ return { namespace: transform === LEGACY_USER_BINDING_TRANSFORM ? await import(url)
856
+ : await managedModuleImport(url, url), moduleUrl: url }
857
+ } catch (error) {
858
+ userBindingModuleParents.delete(url)
859
+ throw error
860
+ }
861
+ }
862
+
863
+ function userBindingImplementationMatches(left, right) {
864
+ return left.source === right.source
865
+ && left.scope === right.scope
866
+ && (left.scope !== 'namespace' || left.name === right.name)
867
+ && left.symbols.length === right.symbols.length
868
+ && left.symbols.every((symbol, index) => symbol === right.symbols[index])
869
+ }
870
+
871
+ async function activateUserBindings(snapshot, shadowedNames, cwd, reusePolicy, initialFailures = []) {
872
+ reconcileUserBindingShadows(shadowedNames)
873
+ const blockedIds = new Set(initialFailures.map(failure => failure.id))
874
+ const desired = new Map((snapshot?.entries ?? [])
875
+ .filter((entry) => {
876
+ const names = entry.scope === 'namespace' ? [entry.name] : entry.symbols
877
+ return !blockedIds.has(entry.id) && !names.some(name => shadowedNames.has(name))
878
+ })
879
+ .map(entry => [entry.id, entry]))
880
+ if (desired.size > 0 || retainedUserBindingRuntime) installDynamicNamespaceGlobals()
881
+ for (const id of [...userBindingEntries.keys()]) {
882
+ const current = userBindingEntries.get(id)
883
+ const next = desired.get(id)
884
+ // Historical cells retain fingerprint-based resets, including presentation edits.
885
+ const reusable = next !== undefined && current.transform === snapshot.transform && (reusePolicy === LEGACY_USER_BINDINGS_REUSE_POLICY
886
+ ? next.fingerprint === current.entry.fingerprint
887
+ : userBindingImplementationMatches(next, current.entry))
888
+ if (!reusable) {
889
+ removeUserBindingEntry(id, shadowedNames)
890
+ }
891
+ }
892
+ const activated = []
893
+ const failures = [...initialFailures]
894
+ for (const entry of desired.values()) {
895
+ const current = userBindingEntries.get(entry.id)
896
+ if (current !== undefined) {
897
+ current.entry = entry
898
+ activated.push(entry.id)
899
+ continue
900
+ }
901
+ let evaluated
902
+ const installedNames = []
903
+ const activation = { id: entry.id, called: false, failed: false }
904
+ try {
905
+ if (entry.durability === 'volatile') {
906
+ markVolatile(`user binding ${JSON.stringify(entry.id)}: ${entry.volatileReason ?? 'non-replayable source'}`)
907
+ }
908
+ evaluated = await userBindingActivationScope.run(
909
+ activation,
910
+ () => evaluateUserBinding(entry, cwd, snapshot.transform),
911
+ )
912
+ const namespace = evaluated.namespace
913
+ for (const symbol of entry.symbols) {
914
+ if (!Object.hasOwn(namespace, symbol)) {
915
+ throw new Error(`named export ${JSON.stringify(symbol)} is unavailable after evaluation`)
916
+ }
917
+ }
918
+ const names = []
919
+ if (entry.scope === 'namespace') {
920
+ const view = Object.create(null)
921
+ for (const symbol of entry.symbols) {
922
+ Object.defineProperty(view, symbol, {
923
+ enumerable: true,
924
+ get: () => namespace[symbol],
925
+ })
926
+ }
927
+ Object.freeze(view)
928
+ const descriptor = Object.getOwnPropertyDescriptor(context, entry.name)
929
+ Object.defineProperty(context, entry.name, {
930
+ configurable: true,
931
+ enumerable: true,
932
+ get: () => view,
933
+ set(value) {
934
+ recordUserBindingAssignment(entry.name, true)
935
+ Object.defineProperty(context, entry.name, {
936
+ configurable: true,
937
+ enumerable: true,
938
+ writable: true,
939
+ value,
940
+ })
941
+ },
942
+ })
943
+ installedNames.push({
944
+ name: entry.name,
945
+ descriptor,
946
+ })
947
+ userBindingNames.set(entry.name, entry.id)
948
+ names.push(entry.name)
949
+ } else {
950
+ for (const symbol of entry.symbols) {
951
+ const descriptor = Object.getOwnPropertyDescriptor(context, symbol)
952
+ Object.defineProperty(context, symbol, {
953
+ configurable: true,
954
+ enumerable: true,
955
+ get: () => namespace[symbol],
956
+ set(value) {
957
+ recordUserBindingAssignment(symbol, true)
958
+ Object.defineProperty(context, symbol, {
959
+ configurable: true,
960
+ enumerable: true,
961
+ writable: true,
962
+ value,
963
+ })
964
+ },
965
+ })
966
+ installedNames.push({ name: symbol, descriptor })
967
+ userBindingNames.set(symbol, entry.id)
968
+ names.push(symbol)
969
+ }
970
+ }
971
+ userBindingEntries.set(entry.id, {
972
+ entry,
973
+ transform: snapshot.transform,
974
+ moduleUrl: evaluated.moduleUrl,
975
+ names,
976
+ descriptors: new Map(names.map(name => [name, Object.getOwnPropertyDescriptor(context, name)])),
977
+ })
978
+ retainedUserBindingRuntime = true
979
+ activated.push(entry.id)
980
+ } catch (error) {
981
+ activation.failed = true
982
+ if (activation.called) {
983
+ markVolatile(`failed user binding ${JSON.stringify(entry.id)} issued a host call`)
984
+ }
985
+ for (const installed of installedNames.reverse()) {
986
+ userBindingNames.delete(installed.name)
987
+ if (installed.descriptor === undefined) delete context[installed.name]
988
+ else Object.defineProperty(context, installed.name, installed.descriptor)
989
+ }
990
+ if (evaluated !== undefined) userBindingModuleParents.delete(evaluated.moduleUrl)
991
+ removeUserBindingEntry(entry.id, shadowedNames)
992
+ failures.push({
993
+ id: entry.id,
994
+ error: messageOf(error),
995
+ })
996
+ }
997
+ }
998
+ if (userBindingEntries.size === 0 && !retainedUserBindingRuntime) restoreDynamicNamespaceGlobals()
999
+ return { activated, failures }
1000
+ }
1001
+
1002
+ async function closeExecution(execution) {
1003
+ activeRun = undefined
1004
+ execution.open = false
1005
+ await Promise.all([...pending.values()]
1006
+ .filter(call => call.runId === execution.id)
1007
+ .map(call => call.settled))
1008
+ }
1009
+
1010
+ function failureOutcome(error, phase, program) {
1011
+ if (phase === 'encode') return { invalidOutput: messageOf(error) }
1012
+ const failure = error instanceof StaticImportFailure ? error.error : error
1013
+ // The V8 frame is reported in the executed text; validate it there so an
1014
+ // out-of-range frame is an explicit absence instead of a mapped position.
1015
+ const detail = errorDetails(failure, activeFilename, program)
1016
+ // A compiler-created call error has only an adapter stack. Its source fact
1017
+ // owns the operation; ordinary exceptions retain their native stack position.
1018
+ const position = error instanceof StaticImportFailure ? error.position
1019
+ : activeExecution.exceptionOrigins.sourceFailure(failure) ? undefined : detail.position
1020
+ return {
1021
+ error: detail.message,
1022
+ errorName: detail.name,
1023
+ ...(detail.toolName === undefined ? {} : { toolName: detail.toolName }),
1024
+ ...(error instanceof StaticImportFailure ? { moduleLoadFailed: true } : {}),
1025
+ ...(position === undefined ? {} : { position }),
1026
+ ...(position === undefined ? { exceptionOrigins: activeExecution.exceptionOrigins.origins(failure) } : {}),
1027
+ ...(detail.cause === undefined ? {} : { cause: detail.cause }),
1028
+ ...(detail.failureOrigin === undefined ? {} : { failureOrigin: detail.failureOrigin }),
1029
+ }
1030
+ }
1031
+
1032
+ function observeBindings(names) {
1033
+ const observation = valueObserver.observe(names)
1034
+ return { ...observation, entries: observation.entries.map(entry => {
1035
+ const local = statefulRoots.localValue(entry.name)
1036
+ return local === undefined ? entry : { name: entry.name, ...previewBindingValue(local.value) }
1037
+ }) }
1038
+ }
1039
+
1040
+ function refreshLegacyPublication(name) {
1041
+ const publications = activeExecution?.nativePublications
1042
+ const publication = publications === undefined ? undefined : mapGet(publications, name)
1043
+ // A completed native declaration is visible to saved logical closures in
1044
+ // the same cell. A failed or pending initializer retains the prior source.
1045
+ if (publication === undefined) return
1046
+ const storage = rootBindingStorage(publication.import?.namespace ?? name, publication.nativeLexical)
1047
+ if (storage === 'lexical' || publication.import === undefined
1048
+ && (storage === 'local' || storage === 'property' && Object.hasOwn(context, name))) {
1049
+ mapDelete(publications, name)
1050
+ statefulRoots.publishLegacy(name, publication.writable, publication.import, storage !== 'lexical')
1051
+ }
1052
+ }
1053
+
1054
+ function sendCompletion(message, execution, userBindings, committedRedeclarations, outcome) {
1055
+ for (const name of execution.nativePublications?.keys() ?? []) refreshLegacyPublication(name)
1056
+ const shadowedNames = execution.legacyAssignedUserBindingNames
1057
+ const perName = message.userBindingsShadowPolicy === LIVE_USER_BINDINGS_SHADOW_POLICY
1058
+ if (perName) reconcileUserBindingNames()
1059
+ else reconcileUserBindingShadows(shadowedNames)
1060
+ const names = message.observeNames ?? []
1061
+ channel.postMessage({
1062
+ type: 'done',
1063
+ id: message.id,
1064
+ logs: execution.logs,
1065
+ ...completionDurability(execution),
1066
+ committedRedeclarations: [...committedRedeclarations],
1067
+ ...(message.rootBindings === undefined ? {} : { rootBindingFacts: statefulRoots.facts() }),
1068
+ ...(perName ? { userBindingNames: normalizeUserBindingNames([...userBindingSources].map(([name, source]) => ({
1069
+ name, state: source.state, ...(source.state === 'provider' ? { entryId: source.entryId } : {}),
1070
+ }))) } : {}),
1071
+ ...(message.userBindings === undefined ? {} : {
1072
+ activatedUserBindings: userBindings.activated,
1073
+ userBindingFailures: userBindings.failures,
1074
+ ...(perName ? {} : { shadowedUserBindings: [...shadowedNames] }),
1075
+ }),
1076
+ ...outcome,
1077
+ observing: names.length > 0,
1078
+ })
1079
+ if (outcome.error === undefined) valueObserver.record(message.program)
1080
+ if (names.length > 0) {
1081
+ channel.postMessage({ type: 'observation', id: message.id, observation: observeBindings(names) })
1082
+ }
1083
+ }
1084
+
351
1085
  async function runCell(message) {
352
- if (activeRun !== undefined) throw new Error('kernel received overlapping cells')
1086
+ if (activeExecution !== undefined) throw new Error('kernel received overlapping cells')
353
1087
  activeRun = message.id
354
1088
  installBindings(message)
355
1089
  const execution = {
356
1090
  id: message.id,
1091
+ exceptionOrigins: createExceptionOriginScope(),
1092
+ languageSemantics: message.languageSemantics,
1093
+ userBindingsShadowPolicy: message.userBindingsShadowPolicy,
1094
+ importBindingNamespaces: message.importBindingNamespaces,
1095
+ legacyAssignedUserBindingNames: new Set(),
357
1096
  logs: [],
358
1097
  open: true,
359
1098
  outputLimited: false,
@@ -370,6 +1109,8 @@ async function runCell(message) {
370
1109
 
371
1110
  try {
372
1111
  let completion
1112
+ let outcome
1113
+ let userBindings = { activated: [], failures: [] }
373
1114
  try {
374
1115
  completion = await logScope.run(execution, async () => {
375
1116
  Object.defineProperty(context, message.returnSignal, {
@@ -380,14 +1121,51 @@ async function runCell(message) {
380
1121
  Object.defineProperty(context, message.commitSignal, {
381
1122
  configurable: true,
382
1123
  value(name) {
383
- committedRedeclarations.add(name)
1124
+ setAdd(committedRedeclarations, name)
384
1125
  },
385
1126
  })
386
1127
  cellGlobals.push(message.commitSignal)
1128
+ if (message.rootRuntimeName !== undefined) {
1129
+ const nativeLexicals = new Set(message.rootBindings.legacyNativeLexicals)
1130
+ const legacyStorage = new Map(message.rootBindings.legacyLexicals
1131
+ .filter(name => !statefulRoots.has(name)).map(name => [name, rootBindingStorage(name, nativeLexicals.has(name))]))
1132
+ const legacyLexicals = message.rootBindings.legacyLexicals.filter(name => legacyStorage.get(name) === 'lexical')
1133
+ const legacyNative = new Set(message.rootBindings.legacyNative)
1134
+ const legacyWritable = message.rootBindings.legacyWritable.filter(name => {
1135
+ const storage = legacyStorage.get(name)
1136
+ return storage === 'lexical' || legacyNative.has(name)
1137
+ && (storage === 'local' || storage === 'property') && writableRootProperty(name)
1138
+ })
1139
+ Object.defineProperty(context, message.rootRuntimeName, {
1140
+ configurable: true,
1141
+ value: statefulRoots.begin({ ...message.rootBindings,
1142
+ legacyLexicals,
1143
+ legacyWritable,
1144
+ legacyObjects: legacyWritable.filter(name => legacyStorage.get(name) !== 'lexical'),
1145
+ committed(name) {
1146
+ setDelete(committedRedeclarations, name)
1147
+ setAdd(committedRedeclarations, name)
1148
+ } }),
1149
+ })
1150
+ cellGlobals.push(message.rootRuntimeName)
1151
+ }
1152
+ const activate = message.userBindingsShadowPolicy === LIVE_USER_BINDINGS_SHADOW_POLICY
1153
+ ? activatePerNameUserBindings : activateUserBindings
1154
+ userBindings = await activate(
1155
+ message.userBindings,
1156
+ new Set(message.shadowedUserBindingNames ?? []),
1157
+ message.userBindingsCwd,
1158
+ message.userBindingsReusePolicy,
1159
+ message.userBindingFailures,
1160
+ )
1161
+ if (userBindings.error !== undefined) throw userBindings.error
1162
+ for (const failure of userBindings.failures) {
1163
+ appendText(execution, `Global binding ${JSON.stringify(failure.id)} was not activated: ${failure.error}`)
1164
+ }
387
1165
  for (const load of message.moduleLoads ?? []) {
388
1166
  let namespace
389
1167
  try {
390
- namespace = await loadStaticModule(load)
1168
+ namespace = (await loadStaticModule(load)).namespace
391
1169
  } catch (error) {
392
1170
  throw new StaticImportFailure(error, load.position)
393
1171
  }
@@ -399,97 +1177,103 @@ async function runCell(message) {
399
1177
  cellGlobals.push(load.global)
400
1178
  }
401
1179
  }
402
- return evaluate(message.program)
1180
+ // Preload failure leaves prior aliases authoritative. Once evaluation
1181
+ // starts, new slots must prove their own initialization, including TDZ.
1182
+ execution.importBindingNamespaces = message.preparedImportBindingNamespaces
1183
+ execution.nativePublications = new Map((message.moduleLoads ?? []).flatMap(load =>
1184
+ (load.nativePublications ?? []).map(publication => [publication.name, publication])))
1185
+ return execution.exceptionOrigins.run(() => evaluate(message.program, message.returnSignal, message.asyncCompletion))
403
1186
  })
404
- activeRun = undefined
405
- execution.open = false
406
- const calls = [...pending.values()]
407
- .filter(call => call.runId === message.id)
408
- .map(call => new Promise(resolve => {
409
- const originalResolve = call.resolve
410
- const originalReject = call.reject
411
- call.resolve = value => { originalResolve(value); resolve() }
412
- call.reject = error => { originalReject(error); resolve() }
413
- }))
414
- if (calls.length > 0) await Promise.all(calls)
415
1187
  } catch (error) {
416
- activeRun = undefined
417
- execution.open = false
418
- const failure = error instanceof StaticImportFailure ? error.error : error
419
- const detail = errorDetails(failure, activeFilename)
420
- const position = error instanceof StaticImportFailure ? error.position : detail.position
421
- channel.postMessage({
422
- type: 'done',
423
- id: message.id,
424
- logs: execution.logs,
425
- error: detail.message,
426
- errorName: detail.name,
427
- ...(detail.toolName === undefined ? {} : { toolName: detail.toolName }),
428
- ...(error instanceof StaticImportFailure ? { moduleLoadFailed: true } : {}),
429
- ...(position === undefined ? {} : { position }),
430
- ...(detail.cause === undefined ? {} : { cause: detail.cause }),
431
- ...completionDurability(execution),
432
- committedRedeclarations: [...committedRedeclarations],
433
- })
434
- return
1188
+ outcome = failureOutcome(error, 'execute', message.program)
1189
+ } finally {
1190
+ await closeExecution(execution)
435
1191
  }
436
1192
 
437
- let response
438
- try {
439
- const encodedValue = completion.hasValue
440
- ? encodeValue(completion.value, execution.valueLimits)
441
- : undefined
442
- response = {
443
- type: 'done',
444
- id: message.id,
445
- logs: execution.logs,
446
- hasValue: completion.hasValue,
447
- ...(encodedValue === undefined ? {} : { value: encodedValue }),
448
- ...completionDurability(execution),
449
- committedRedeclarations: [...committedRedeclarations],
450
- }
451
- } catch (error) {
452
- const detail = messageOf(error)
453
- response = {
454
- type: 'done',
455
- id: message.id,
456
- logs: execution.logs,
457
- invalidOutput: detail,
458
- ...completionDurability(execution),
459
- committedRedeclarations: [...committedRedeclarations],
1193
+ if (outcome === undefined) {
1194
+ try {
1195
+ const encodedValue = completion.hasValue
1196
+ ? encodeValue(completion.value, execution.valueLimits)
1197
+ : undefined
1198
+ outcome = {
1199
+ hasValue: completion.hasValue,
1200
+ ...(encodedValue === undefined ? {} : { value: encodedValue }),
1201
+ }
1202
+ } catch (error) {
1203
+ outcome = failureOutcome(error, 'encode', message.program)
460
1204
  }
461
1205
  }
462
- channel.postMessage(response)
1206
+ sendCompletion(message, execution, userBindings, committedRedeclarations, outcome)
463
1207
  } finally {
464
1208
  for (const name of cellGlobals) delete context[name]
465
1209
  activeRun = undefined
466
1210
  activeExecution = undefined
1211
+ execution.exceptionOrigins.close()
467
1212
  execution.open = false
468
1213
  }
469
1214
  }
470
1215
 
1216
+ // The host stops this worker cooperatively so the release below runs while the
1217
+ // realms, REPL and ports it owns are still alive.
1218
+ parentPort.on('message', (message) => {
1219
+ if (message?.type !== WORKER_SHUTDOWN_REQUEST) return
1220
+ server.close()
1221
+ input.destroy()
1222
+ output.destroy()
1223
+ errorDomain.removeAllListeners('error')
1224
+ channel.close()
1225
+ parentPort.postMessage({ type: WORKER_SHUTDOWN_ACKNOWLEDGEMENT })
1226
+ parentPort.close()
1227
+ })
1228
+
471
1229
  channel.on('message', (message) => {
1230
+ if (message?.type === 'prepare') {
1231
+ channel.postMessage({ type: 'ready', id: message.id })
1232
+ return
1233
+ }
1234
+ if (message?.type === 'observe') {
1235
+ channel.postMessage({ type: 'observation-started', id: message.id })
1236
+ channel.postMessage({ type: 'observation', id: message.id,
1237
+ observation: observeBindings(message.names) })
1238
+ return
1239
+ }
472
1240
  if (message?.type === 'reply') {
473
1241
  const call = pending.get(message.id)
474
1242
  if (call === undefined || call.runId !== message.runId) return
475
1243
  pending.delete(message.id)
476
- if (message.ok) call.resolve(decodeValue(message.value, activeExecution?.valueLimits))
477
- else if (call.errorClass === undefined) {
478
- const error = new Error(message.error)
479
- if (message.cause !== undefined) error.ptcCause = message.cause
1244
+ try {
1245
+ if (message.ok) call.resolve(decodeValue(message.value, call.valueLimits))
1246
+ else if (call.BoundError === undefined) {
1247
+ const error = new Error(message.error)
1248
+ if (message.cause !== undefined) error.ptcCause = message.cause
1249
+ call.reject(error)
1250
+ } else call.reject(new call.BoundError(call.member, message.error, message.cause))
1251
+ } catch (error) {
480
1252
  call.reject(error)
481
- } else call.reject(new context[call.errorClass.name](call.member, message.error, message.cause))
1253
+ }
482
1254
  return
483
1255
  }
484
1256
  if (message?.type === 'run') void runCell(message)
485
1257
  })
486
1258
 
1259
+ let startupTimer
487
1260
  try {
488
- await evaluate(CONFORMANCE_CELL)
1261
+ await Promise.race([
1262
+ verifyEvaluation(),
1263
+ new Promise((_, reject) => {
1264
+ startupTimer = setTimeout(() => reject(new Error('REPL settlement probe timed out')), 5000)
1265
+ }),
1266
+ ])
1267
+ replModuleRealm = (await evaluate('({Promise,stringify:value=>`${value}`,importModule:(source,options)=>import(source,options)})')).value
1268
+ valueObserver = createReplValueObserver(context, {
1269
+ awaitLexicals: await supportsAwaitLexicals(context, evaluate),
1270
+ })
489
1271
  parentPort.postMessage({ type: 'ready', port: port1 }, [port1])
490
1272
  } catch (error) {
491
1273
  parentPort.postMessage({
492
1274
  type: 'startup-error',
493
- error: `Node REPL does not satisfy the PTC Plus cell framing contract: ${messageOf(error)}`,
1275
+ error: `PTC runtime prerequisite failed on Node ${process.version}: ${messageOf(error)}. No user cell executed; use a DSH-supported runtime that passes the REPL conformance probe. Editing the cell cannot fix this host failure.`,
494
1276
  })
1277
+ } finally {
1278
+ clearTimeout(startupTimer)
495
1279
  }