dsh-ptc-plus 0.4.1 → 0.4.3

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 (103) hide show
  1. package/README.en.md +164 -78
  2. package/README.md +165 -79
  3. package/assets/dsh-ptc-plus-banner-en.webp +0 -0
  4. package/assets/dsh-ptc-plus-banner-zh.webp +0 -0
  5. package/assets/ptc-plus-icon.svg +5 -0
  6. package/client.js +854 -336
  7. package/compiler-core.cjs +161 -157
  8. package/compiler-core.cjs.map +3 -3
  9. package/docs/README.md +4 -1
  10. package/docs/adr/0005-temporary-rejected-cell-edit-transport.md +8 -7
  11. package/docs/adr/0008-define-agent-operating-contract.md +2 -2
  12. package/docs/adr/0010-session-log-derived-recovery-tips.md +7 -0
  13. package/docs/adr/0016-archive-resolved-review-findings.md +3 -3
  14. package/docs/adr/0019-plugin-settings-and-kill-switch.md +3 -3
  15. package/docs/adr/0020-optional-cordis-tools-in-ptc-mode.md +2 -0
  16. package/docs/adr/0021-separate-host-bootstrap-from-session-runtime.md +14 -3
  17. package/docs/adr/0023-global-user-bindings.md +7 -7
  18. package/docs/adr/0025-use-versioned-logical-binding-identities.md +3 -3
  19. package/docs/adr/0026-persist-the-delivery-verdict-against-the-verified-candidate.md +23 -7
  20. package/docs/adr/0027-run-the-session-kernel-in-a-killable-helper-process.md +9 -5
  21. package/docs/adr/0028-attach-to-the-host-ptc-execution-seam.md +26 -6
  22. package/docs/adr/0031-close-semantic-obligations-by-owner.md +29 -0
  23. package/docs/adr/README.md +4 -4
  24. package/docs/architecture.md +15 -10
  25. package/docs/binding-review-placement-design.md +1 -1
  26. package/docs/client-ui.md +9 -5
  27. package/docs/durability-design.md +12 -9
  28. package/docs/installation.md +27 -9
  29. package/docs/runtime-reference.md +23 -12
  30. package/docs/semantic-validation.md +7 -3
  31. package/docs/stateful-binding-updates-proposal.md +8 -4
  32. package/docs/user-bindings.en.md +10 -7
  33. package/docs/user-bindings.md +9 -6
  34. package/docs/value-wire.md +1 -1
  35. package/docs/verification-optimization-work.md +41 -0
  36. package/docs/verification.md +53 -2
  37. package/index.js +39 -34
  38. package/internal/async-local-storage-intrinsics.js +43 -0
  39. package/internal/binding-declaration-contract.js +8 -0
  40. package/internal/binding-source-analysis.js +381 -64
  41. package/internal/binding-update-policy.js +7 -1
  42. package/internal/callable-source-facts.js +37 -1
  43. package/internal/cell-analysis.js +85 -17
  44. package/internal/compiler-intrinsics.js +9 -43
  45. package/internal/compiler-module-links.js +7 -4
  46. package/internal/config-spec.js +1 -1
  47. package/internal/cordis-tools-owner.js +20 -10
  48. package/internal/dynamic-environment-integration.js +8 -4
  49. package/internal/dynamic-environment-runtime.js +24 -3
  50. package/internal/dynamic-native-calls.js +4 -1
  51. package/internal/dynamic-native-runtime.js +15 -1
  52. package/internal/edit-transport-owner.js +80 -20
  53. package/internal/execution-seam-compat.js +192 -30
  54. package/internal/failure-reporting.js +38 -30
  55. package/internal/isolated-worker.js +17 -10
  56. package/internal/kernel-worker.js +645 -393
  57. package/internal/message-sources.js +14 -0
  58. package/internal/module-compilation.js +7 -5
  59. package/internal/module-transform-contract.js +32 -2
  60. package/internal/record-utils.js +24 -15
  61. package/internal/repl-memory-projection.js +10 -0
  62. package/internal/repl-scope-normalizer.js +72 -35
  63. package/internal/repl-value-observer.js +145 -46
  64. package/internal/runtime-config.js +62 -2
  65. package/internal/runtime-intrinsics.js +104 -0
  66. package/internal/runtime-messages.js +6 -4
  67. package/internal/session-cell-executor.js +26 -2
  68. package/internal/session-journal-recovery.js +267 -67
  69. package/internal/session-journal-schema.js +30 -13
  70. package/internal/session-journal.js +485 -44
  71. package/internal/session-log-view.js +5 -4
  72. package/internal/session-runtime.js +46 -21
  73. package/internal/session-state.js +7 -29
  74. package/internal/settings-compat.js +66 -1
  75. package/internal/source-position-map.js +38 -0
  76. package/internal/stateful-root-compiler.js +3 -1
  77. package/internal/typescript-transform.js +4 -2
  78. package/internal/user-binding-console-worker.js +115 -41
  79. package/internal/user-binding-draft-projection.js +3 -2
  80. package/internal/user-bindings-owner.js +2 -1
  81. package/internal/user-bindings.js +16 -3
  82. package/internal/validated-parse-repair.js +6 -2
  83. package/internal/value-wire.js +189 -94
  84. package/internal/worker-client.js +99 -8
  85. package/internal/worker-cwd-virtualization.js +82 -54
  86. package/internal/worker-environment.js +20 -0
  87. package/internal/worker-output-capture.js +187 -0
  88. package/internal/worker-output-fence.js +26 -0
  89. package/internal/worker-realm-surfaces.js +197 -0
  90. package/internal/worker-repl-realm.js +140 -0
  91. package/package.json +37 -4
  92. package/src/client-activity.js +12 -2
  93. package/src/client-authoring-view.js +121 -62
  94. package/src/client-console.js +4 -2
  95. package/src/client-copy.js +4 -2
  96. package/src/client-feature-gates.js +7 -9
  97. package/src/client-host-compat.js +104 -27
  98. package/src/client-repl-view.js +3 -1
  99. package/src/client-styles.js +12 -1
  100. package/src/client-tool-view.js +3 -1
  101. package/src/client-transport.js +2 -2
  102. package/src/client-workbench.js +4 -3
  103. package/src/client.js +56 -17
@@ -1,5 +1,5 @@
1
- import { AsyncLocalStorage } from 'node:async_hooks'
2
1
  import { randomUUID } from 'node:crypto'
2
+ import { createPrivateAsyncLocalStorage } from './async-local-storage-intrinsics.js'
3
3
  import { create as createDomain } from 'node:domain'
4
4
  import { createRequire, registerHooks } from 'node:module'
5
5
  import { resolve } from 'node:path'
@@ -8,7 +8,6 @@ import { PassThrough } from 'node:stream'
8
8
  import { inspect } from 'node:util'
9
9
  import { pathToFileURL } from 'node:url'
10
10
  import { parentPort, workerData } from 'node:worker_threads'
11
- import { runInContext } from 'node:vm'
12
11
  import { prepareConsoleProgram } from './compiler-service.js'
13
12
  import { createStatefulRootRuntime } from './stateful-root-runtime.js'
14
13
  import { managedModuleImport, managedRequire, readModuleImport } from './stateful-module-runtime.js'
@@ -17,22 +16,31 @@ import { USER_BINDING_TRANSFORM } from './module-transform-contract.js'
17
16
  import { moduleRuntimeIntrinsics } from './compiler-intrinsics.js'
18
17
  import { stringifyCompilerData } from './compiler-data.js'
19
18
  import { createCellCompletionObserver } from './cell-completion.js'
19
+ import {
20
+ WORKER_REALM_MUTATION,
21
+ assignWorkerRealmProperty,
22
+ defineWorkerRealmProperty,
23
+ deleteWorkerRealmProperty,
24
+ } from './worker-realm-surfaces.js'
20
25
  import { WORKER_SHUTDOWN_ACKNOWLEDGEMENT, WORKER_SHUTDOWN_REQUEST } from './worker-shutdown.js'
26
+ import { WORKER_REPL_OPTIONS, createWorkerControlPromise, createWorkerReplErrorHandler,
27
+ disableWorkerReplDomain,
28
+ captureWorkerReplGlobals, protectWorkerReplAsyncContext, restoreWorkerReplGlobals, runInWorkerReplRealm,
29
+ workerReplContext } from './worker-repl-realm.js'
21
30
 
22
31
  const { Object, Set, setHas, setAdd, setDelete, appendArray } = moduleRuntimeIntrinsics
32
+ const Promise = globalThis.Promise
33
+ const process = globalThis.process
34
+ const hasProperty = globalThis.Reflect.has
35
+ const ControlPromise = createWorkerControlPromise()
23
36
 
24
- const input = new PassThrough()
25
- const output = new PassThrough()
26
- output.resume()
27
- const evaluations = new AsyncLocalStorage()
28
- const server = repl.start({ input, output, terminal: false, prompt: '', useGlobal: false })
29
- const completionObserver = createCellCompletionObserver(runInContext('Function', server.context))
30
- server.context.console = console
31
- const errorDomain = server.eval.domain ?? createDomain()
32
- errorDomain.removeAllListeners('error')
33
- errorDomain.on('error', error => evaluations.getStore()?.finish(true, error))
37
+ const evaluations = createPrivateAsyncLocalStorage()
38
+ const context = globalThis
39
+ const completionObserver = createCellCompletionObserver(runInWorkerReplRealm('Function'))
40
+ assignWorkerRealmProperty(WORKER_REALM_MUTATION.STABLE, 'console', context, 'console', console)
41
+ let retainedEvaluator
34
42
  const completionKey = `__ptc_console_${randomUUID().replaceAll('-', '')}`
35
- Object.defineProperty(server.context, completionKey, { value: value => {
43
+ defineWorkerRealmProperty(WORKER_REALM_MUTATION.TEMPORARY, undefined, context, completionKey, { value: value => {
36
44
  evaluations.getStore().completed = true
37
45
  return value
38
46
  } })
@@ -44,16 +52,16 @@ const adapterParents = new Set()
44
52
  const knownBindings = new Set()
45
53
  const roots = createStatefulRootRuntime({
46
54
  importModule: (source, options) => managedModuleImport(replParent, source, options, replModuleRealm),
47
- errors: runInContext('({ ReferenceError, TypeError })', server.context),
48
- dynamicIntrinsics: runInContext('({intrinsicEval:eval,realmFunction:Function,globalObject:globalThis,errors:{ReferenceError,TypeError,SyntaxError},reflect:Reflect,object:Object})', server.context),
49
- readAmbient: name => runInContext(name, server.context),
50
- typeofAmbient: name => runInContext(`typeof ${name}`, server.context),
55
+ errors: runInWorkerReplRealm('({ ReferenceError, TypeError })'),
56
+ dynamicIntrinsics: runInWorkerReplRealm('({intrinsicEval:eval,realmFunction:Function,globalObject:globalThis,errors:{ReferenceError,TypeError,SyntaxError},reflect:Reflect,object:Object})'),
57
+ readAmbient: name => runInWorkerReplRealm(name),
58
+ typeofAmbient: name => runInWorkerReplRealm(`typeof ${name}`),
51
59
  writeAmbient(name, value, strict) {
52
60
  const argument = name === '__ptc_value' ? '__ptc_other_value' : '__ptc_value'
53
- runInContext(`${strict ? '"use strict";' : ''}(${argument}) => (${name} = ${argument})`, server.context)(value)
61
+ runInWorkerReplRealm(`${strict ? '"use strict";' : ''}(${argument}) => (${name} = ${argument})`)(value)
54
62
  },
55
- deleteAmbient: name => runInContext(`delete ${name}`, server.context),
56
- hasAmbient: name => Reflect.has(server.context, name),
63
+ deleteAmbient: name => runInWorkerReplRealm(`delete ${name}`),
64
+ hasAmbient: name => hasProperty(context, name),
57
65
  publish: name => setAdd(knownBindings, name),
58
66
  })
59
67
  class ConsoleReturn {
@@ -62,7 +70,9 @@ class ConsoleReturn {
62
70
  const moduleCompilation = createUserModuleCompilationHooks({
63
71
  transformForParent: parent => parent === replParent || parent === parentUrl ? USER_BINDING_TRANSFORM : undefined,
64
72
  })
65
- server.context.require = managedRequire(parentUrl, createRequire(parentUrl))
73
+ const providedRequire = managedRequire(parentUrl, createRequire(parentUrl))
74
+ defineWorkerRealmProperty(WORKER_REALM_MUTATION.STABLE, 'cell module metadata',
75
+ context, 'require', { configurable: true, value: providedRequire })
66
76
  const importCanary = 'data:text/javascript,export default 1'
67
77
  registerHooks({ resolve(specifier, context, nextResolve) {
68
78
  if (specifier === importCanary) replParent = context.parentURL
@@ -74,14 +84,38 @@ const describe = value => inspect(value, {
74
84
  colors: false, customInspect: false, getters: false, depth: 4,
75
85
  maxArrayLength: 40, maxStringLength: Math.min(workerData.maxOutputBytes, 8192), breakLength: 100,
76
86
  })
87
+ process.on('uncaughtException', error => {
88
+ const state = evaluations.getStore()
89
+ if (state?.finish(true, error) === true || state?.settled === true) return
90
+ parentPort.postMessage({ fatal: true, error: describe(error) })
91
+ })
77
92
 
78
- async function evaluate(source) {
93
+ function evaluate(source) {
79
94
  const prepared = prepareConsoleProgram(source, { languageSemantics: 'stateful-v1', knownBindings })
80
95
  const helpers = [prepared.rootRuntimeName, prepared.returnSignal]
81
- Object.defineProperty(server.context, prepared.rootRuntimeName, {
96
+ defineWorkerRealmProperty(WORKER_REALM_MUTATION.TEMPORARY, undefined,
97
+ context, prepared.rootRuntimeName, {
82
98
  configurable: true, value: roots.begin({ ...prepared.rootBindings, committed: () => {} }),
83
99
  })
84
- Object.defineProperty(server.context, prepared.returnSignal, { configurable: true, value: ConsoleReturn })
100
+ defineWorkerRealmProperty(WORKER_REALM_MUTATION.TEMPORARY, undefined,
101
+ context, prepared.returnSignal, { configurable: true, value: ConsoleReturn })
102
+ if (prepared.moduleLoads.length === 0) {
103
+ const operation = evaluatePrepared(prepared.code, prepared.asyncCompletion)
104
+ return new ControlPromise((resolve, reject) => operation.then(
105
+ value => { removeHelpers(helpers); resolve(value) },
106
+ error => { removeHelpers(helpers); reject(error) },
107
+ ))
108
+ }
109
+ return evaluateWithModules(prepared, helpers)
110
+ }
111
+
112
+ function removeHelpers(helpers) {
113
+ for (let index = 0; index < helpers.length; index++) {
114
+ deleteWorkerRealmProperty(WORKER_REALM_MUTATION.RESTORE, undefined, context, helpers[index])
115
+ }
116
+ }
117
+
118
+ async function evaluateWithModules(prepared, helpers) {
85
119
  try {
86
120
  for (let index = 0; index < prepared.moduleLoads.length; index++) {
87
121
  const load = prepared.moduleLoads[index]
@@ -109,30 +143,50 @@ async function evaluate(source) {
109
143
  } finally { setDelete(adapterParents, adapter) }
110
144
  if (load.global !== undefined) {
111
145
  appendArray(helpers, load.global)
112
- Object.defineProperty(server.context, load.global, { configurable: true, value: namespace })
146
+ defineWorkerRealmProperty(WORKER_REALM_MUTATION.TEMPORARY, undefined,
147
+ context, load.global, { configurable: true, value: namespace })
113
148
  }
114
149
  }
115
150
  return await evaluatePrepared(prepared.code, prepared.asyncCompletion)
116
151
  } finally {
117
- for (let index = 0; index < helpers.length; index++) delete server.context[helpers[index]]
152
+ removeHelpers(helpers)
118
153
  }
119
154
  }
120
155
 
121
156
  function evaluatePrepared(javascript, asyncCompletion = false) {
122
- return new Promise((resolve, reject) => {
157
+ releaseRetainedEvaluator()
158
+ const input = new PassThrough()
159
+ const output = new PassThrough()
160
+ output.resume()
161
+ const workerGlobalBaseline = captureWorkerReplGlobals(context)
162
+ const server = repl.start({ input, output, terminal: false, prompt: '', ...WORKER_REPL_OPTIONS,
163
+ handleError: createWorkerReplErrorHandler(evaluations, (state, error) => state.finish(true, error)) })
164
+ protectWorkerReplAsyncContext(server)
165
+ workerReplContext(server)
166
+ restoreWorkerReplGlobals(workerGlobalBaseline, context)
167
+ defineWorkerRealmProperty(WORKER_REALM_MUTATION.STABLE, 'cell module metadata',
168
+ context, 'require', { configurable: true, value: providedRequire })
169
+ const errorDomain = disableWorkerReplDomain(server.eval.domain ?? createDomain())
170
+ const evaluator = { server, input, output, errorDomain }
171
+ retainedEvaluator = evaluator
172
+ return new ControlPromise((resolve, reject) => {
123
173
  const complete = `this[${stringifyCompilerData(completionKey)}]`
124
174
  // Preserve expression results while distinguishing rejection with null/undefined.
125
175
  const code = `${javascript}\n;{let completed = ${complete}();}\n;`
126
176
  let settled = false
127
177
  const state = { completed: false, finish(failed, value) {
128
- if (settled) return
178
+ if (settled) return false
129
179
  settled = true
180
+ state.settled = true
130
181
  if (failed && value instanceof ConsoleReturn) completionObserver.settle(value.value, resolve, reject)
131
182
  else if (failed) reject(value)
132
183
  else completionObserver.settle(value, resolve, reject)
184
+ return true
133
185
  } }
186
+ evaluator.state = state
187
+ errorDomain.on('error', error => state.finish(true, error))
134
188
  evaluations.run(state, () => errorDomain.run(() => {
135
- server.eval(`${code}\n`, server.context, 'ptc-plus-console', (error, value) => {
189
+ server.eval(`${code}\n`, context, 'ptc-plus-console', (error, value) => {
136
190
  if (asyncCompletion && (error === null || error === undefined)) {
137
191
  completionObserver.observe(value, value => state.finish(false, value), error => state.finish(true, error))
138
192
  return
@@ -143,6 +197,16 @@ function evaluatePrepared(javascript, asyncCompletion = false) {
143
197
  }))
144
198
  })
145
199
  }
200
+
201
+ function releaseRetainedEvaluator() {
202
+ const evaluator = retainedEvaluator
203
+ if (evaluator === undefined) return undefined
204
+ retainedEvaluator = undefined
205
+ evaluator.errorDomain.removeAllListeners('error')
206
+ evaluator.server.close()
207
+ evaluator.input.destroy()
208
+ evaluator.output.destroy()
209
+ }
146
210
  try {
147
211
  replModuleRealm = await evaluatePrepared('({Promise,stringify:value=>`${value}`,importModule:(source,options)=>import(source,options)})')
148
212
  const prepared = compileStatefulModule(workerData.source)
@@ -154,30 +218,40 @@ try {
154
218
  for (let index = 0; index < names.length; index++) {
155
219
  const name = names[index]
156
220
  setAdd(knownBindings, name)
157
- Object.defineProperty(server.context, name, { configurable: true, enumerable: true, get: () => namespace[name] })
221
+ defineWorkerRealmProperty(WORKER_REALM_MUTATION.USER, undefined,
222
+ context, name, { configurable: true, enumerable: true, get: () => namespace[name] })
158
223
  }
159
- parentPort.on('message', async (message) => {
224
+ parentPort.on('message', (message) => {
160
225
  if (message?.type === WORKER_SHUTDOWN_REQUEST) {
161
226
  // Release the realm, REPL and domain this console owns, then report the
162
227
  // release before the host gives up on the thread; see worker-shutdown.js.
163
- server.close()
164
- input.destroy()
165
- output.destroy()
166
- errorDomain.removeAllListeners('error')
228
+ releaseRetainedEvaluator()
167
229
  parentPort.postMessage({ type: WORKER_SHUTDOWN_ACKNOWLEDGEMENT })
168
230
  parentPort.close()
169
231
  return
170
232
  }
171
233
  const { id, code } = message
172
- let result
234
+ const send = result => {
235
+ const flushed = new ControlPromise(resolve => process.stdout.write('', () => process.stderr.write('', resolve)))
236
+ moduleRuntimeIntrinsics.observeOwnedPromise(flushed, () => {
237
+ parentPort.postMessage(result)
238
+ releaseRetainedEvaluator()
239
+ /* c8 ignore next 3 -- flushed has no reject capability */
240
+ }, error => {
241
+ releaseRetainedEvaluator()
242
+ parentPort.postMessage({ id, error: describe(error) })
243
+ })
244
+ }
245
+ let operation
173
246
  try {
174
- const value = await evaluate(code)
175
- result = { id, output: describe(value) }
247
+ operation = evaluate(code)
176
248
  } catch (error) {
177
- result = { id, error: describe(error) }
249
+ send({ id, error: describe(error) })
250
+ return
178
251
  }
179
- await new Promise(resolve => process.stdout.write('', () => process.stderr.write('', resolve)))
180
- parentPort.postMessage(result)
252
+ moduleRuntimeIntrinsics.observeOwnedPromise(operation,
253
+ value => send({ id, output: describe(value) }),
254
+ error => send({ id, error: describe(error) }))
181
255
  })
182
256
  } catch (error) {
183
257
  parentPort.postMessage({ fatal: true, error: describe(error) })
@@ -1,3 +1,4 @@
1
+ import { isPtcMessageSource, PTC_MESSAGE_SOURCE_KIND } from './message-sources.js'
1
2
  import { isRecord } from './record-utils.js'
2
3
  import { normalizeBindingModelContext } from './user-binding-model-context.js'
3
4
 
@@ -51,13 +52,13 @@ function normalizeAction(value) {
51
52
 
52
53
  export function bindingActionNotice(action) {
53
54
  return {
54
- source: { kind: 'plugin', plugin: 'ptc-plus', form: 'notice', summary: 'Global User Binding action' },
55
+ source: { kind: PTC_MESSAGE_SOURCE_KIND, form: 'notice', summary: 'Global User Binding action' },
55
56
  content: [{ type: 'text', text: ACTION_PREFIX + JSON.stringify(normalizeAction(action)) }],
56
57
  }
57
58
  }
58
59
 
59
60
  export function readBindingAction(message) {
60
- if (message?.source?.kind !== 'plugin' || message.source.plugin !== 'ptc-plus'
61
+ if (!isPtcMessageSource(message?.source)
61
62
  || message.source.form !== 'notice' || message.source.summary !== 'Global User Binding action'
62
63
  || message.content?.length !== 1 || message.content[0]?.type !== 'text') return undefined
63
64
  try {
@@ -1,3 +1,4 @@
1
+ import { PTC_MESSAGE_SOURCE_KIND } from './message-sources.js'
1
2
  import { RPC_CONTRACTS } from './rpc-contract.js'
2
3
  import { randomUUID } from 'node:crypto'
3
4
  import { dirname } from 'node:path'
@@ -87,7 +88,7 @@ function authoringTask(command, current) {
87
88
  function taskMessage(text) {
88
89
  return createUserMessage({
89
90
  content: [{ type: 'text', text }],
90
- source: { kind: 'plugin', plugin: 'ptc-plus', form: 'instructions' },
91
+ source: { kind: PTC_MESSAGE_SOURCE_KIND, form: 'instructions' },
91
92
  })
92
93
  }
93
94
 
@@ -1,6 +1,7 @@
1
1
  import { createHash } from 'node:crypto'
2
2
  import { identifier, exportedSymbols, sourceDurability } from './compiler-service.js'
3
3
  import { assertOwnFields, isRecord } from './record-utils.js'
4
+ import { boundedDefinitionSource } from './repl-memory-projection.js'
4
5
  import { bindingModelPreferences, normalizeBindingModelContext } from './user-binding-model-context.js'
5
6
  import { supportedUserBindingTransform, USER_BINDING_TRANSFORM } from './module-transform-contract.js'
6
7
 
@@ -62,11 +63,19 @@ function selectedDescriptors(source, symbols, transform) {
62
63
 
63
64
 
64
65
  function declarationFor(entry, descriptors) {
66
+ const typeNamespace = `__ptcBinding_${createHash('sha256').update(entry.id).digest('hex').slice(0, 12)}`
67
+ const typeNamespaceTokens = new Set(descriptors.map(item => item.typeNamespaceToken))
68
+ if (typeNamespaceTokens.size !== 1 || typeNamespaceTokens.has(undefined)) throw new TypeError('binding symbols do not share one compiler type namespace')
69
+ const [typeNamespaceToken] = typeNamespaceTokens
70
+ const render = text => text.replaceAll(typeNamespaceToken, typeNamespace)
71
+ const typeDeclarations = [...new Set(descriptors.flatMap(item => item.typeDeclarations ?? []))]
72
+ const typePrefix = typeDeclarations.length === 0 ? '' : `declare namespace ${typeNamespace} {\n${typeDeclarations
73
+ .map(item => render(item).split('\n').map(line => ` ${line}`).join('\n')).join('\n')}\n}\n`
65
74
  const comment = entry.purpose === '' ? '' : `/** ${entry.purpose.replaceAll('*/', '* /')} */\n`
66
75
  if (entry.scope === 'namespace') {
67
- return `${comment}declare const ${entry.name}: {\n${descriptors.map(item => ` ${item.member};`).join('\n')}\n}`
76
+ return `${typePrefix}${comment}declare const ${entry.name}: {\n${descriptors.map(item => ` ${render(item.member)};`).join('\n')}\n}`
68
77
  }
69
- return descriptors.map((item, index) => `${index === 0 ? comment : ''}declare ${item.declaration}`).join('\n')
78
+ return `${typePrefix}${descriptors.map((item, index) => `${index === 0 ? comment : ''}declare ${render(item.declaration)}`).join('\n')}`
70
79
  }
71
80
 
72
81
  export function normalizeUserBindingEntry(value, { transform = USER_BINDING_TRANSFORM } = {}) {
@@ -288,7 +297,11 @@ export function userBindingCatalogEntries(snapshot) {
288
297
  kind: binding.kind,
289
298
  entryId: entry.id,
290
299
  fingerprint: entry.fingerprint,
291
- definition: Object.freeze({ source: binding.declaration, line: 1, column: 1 }),
300
+ definition: Object.freeze({
301
+ source: boundedDefinitionSource(binding.declaration),
302
+ line: 1,
303
+ column: 1,
304
+ }),
292
305
  })))
293
306
  }
294
307
 
@@ -1,6 +1,10 @@
1
1
  import { EXPECTED_TARGET_CALL_SEQ, editRejectedCell } from './rejected-cell-editor.js'
2
2
 
3
3
  const CLOSING_DELIMITERS = Object.freeze(['}', ')', ']'])
4
+ const CLOSING_SUFFIXES = Object.freeze([
5
+ ...CLOSING_DELIMITERS,
6
+ ...CLOSING_DELIMITERS.flatMap(first => CLOSING_DELIMITERS.map(second => first + second)),
7
+ ])
4
8
  const LINE_TERMINATORS = /\r\n|[\n\r\u2028\u2029]/gu
5
9
  export const MAX_PARSE_REPAIR_ANCHOR_CODE_UNITS = 256
6
10
 
@@ -52,14 +56,14 @@ function renderInvocation(argumentsValue) {
52
56
  return `edit_run_code(${json})`
53
57
  }
54
58
 
55
- /** Returns a source-exact edit only when one bounded EOF-closing candidate is valid. */
59
+ /** Requires exactly one valid suffix across all one- and two-token EOF closures. */
56
60
  export function validatedEofClosureRepair({ source, position, prepare, targetCallSeq }) {
57
61
  if (!Number.isSafeInteger(targetCallSeq) || targetCallSeq < 0
58
62
  || !isSourceEnd(source, position)) return undefined
59
63
  const anchor = boundedUniqueSuffix(source)
60
64
  if (anchor === undefined) return undefined
61
65
  const candidates = []
62
- for (const delimiter of CLOSING_DELIMITERS) {
66
+ for (const delimiter of CLOSING_SUFFIXES) {
63
67
  const code = source + delimiter
64
68
  let prepared
65
69
  try {