dsh-ptc-plus 0.4.3 → 0.4.10

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 (66) hide show
  1. package/README.en.md +15 -3
  2. package/README.md +15 -3
  3. package/client.js +1157 -533
  4. package/cordis.patch.yml +12 -2
  5. package/docs/README.md +2 -0
  6. package/docs/adr/0005-temporary-rejected-cell-edit-transport.md +60 -4
  7. package/docs/adr/0017-track-the-latest-dsh-public-surface.md +5 -1
  8. package/docs/adr/0019-plugin-settings-and-kill-switch.md +14 -3
  9. package/docs/adr/0020-optional-cordis-tools-in-ptc-mode.md +2 -2
  10. package/docs/adr/0023-global-user-bindings.md +1 -1
  11. package/docs/adr/0028-attach-to-the-host-ptc-execution-seam.md +82 -18
  12. package/docs/adr/0030-serve-both-typert-codec-generations-from-one-codec.md +3 -3
  13. package/docs/adr/0032-own-session-log-facts-in-a-host-only-projection.md +27 -0
  14. package/docs/adr/0033-gate-client-visual-usability-in-a-browser.md +23 -0
  15. package/docs/architecture.md +6 -4
  16. package/docs/binding-review-placement-design.md +29 -21
  17. package/docs/client-ui.md +20 -8
  18. package/docs/client-visual-contract.md +48 -0
  19. package/docs/durability-design.md +12 -0
  20. package/docs/installation.md +48 -9
  21. package/docs/publishing.md +4 -4
  22. package/docs/release-notes.md +109 -0
  23. package/docs/runtime-reference.md +8 -2
  24. package/docs/user-bindings.en.md +2 -2
  25. package/docs/user-bindings.md +3 -3
  26. package/docs/verification-optimization-work.md +26 -0
  27. package/docs/verification.md +7 -1
  28. package/index.js +28 -7
  29. package/internal/compiler-platform.js +5 -3
  30. package/internal/compiler-service.js +2 -9
  31. package/internal/cordis-tools-owner.js +75 -16
  32. package/internal/direct-surface-owner.js +333 -7
  33. package/internal/edit-transport-owner.js +8 -5
  34. package/internal/execution-provider-owner.js +157 -0
  35. package/internal/execution-seam-compat.js +69 -113
  36. package/internal/isolated-worker.js +12 -8
  37. package/internal/recovery-tips.js +50 -21
  38. package/internal/rejected-cell-editor.js +11 -21
  39. package/internal/runtime-bridge-owner.js +88 -57
  40. package/internal/runtime-contexts.js +13 -13
  41. package/internal/session-events.js +40 -4
  42. package/internal/session-journal-recovery.js +678 -181
  43. package/internal/session-log-view.js +189 -43
  44. package/internal/session-runtime.js +58 -10
  45. package/internal/settings-compat.js +12 -2
  46. package/internal/user-bindings-owner.js +6 -5
  47. package/locale/en.json +6 -0
  48. package/locale/zh.json +6 -0
  49. package/package.json +18 -13
  50. package/src/client-authoring-view.js +222 -141
  51. package/src/client-binding-review.js +2 -0
  52. package/src/client-code-editor.js +6 -0
  53. package/src/client-copy.js +10 -0
  54. package/src/client-feature-gates.js +12 -5
  55. package/src/client-host-compat.js +23 -2
  56. package/src/client-primitives.js +249 -0
  57. package/src/client-repl-view.js +3 -3
  58. package/src/client-settings-view.js +2 -29
  59. package/src/client-styles.js +92 -33
  60. package/src/client-tool-view.js +8 -3
  61. package/src/client-workbench.js +9 -7
  62. package/src/client.js +72 -46
  63. package/assets/ptc-plus-bindings-en.png +0 -0
  64. package/assets/ptc-plus-bindings-zh.png +0 -0
  65. package/assets/ptc-plus-repair-en.png +0 -0
  66. package/assets/ptc-plus-repair-zh.png +0 -0
@@ -1,39 +1,11 @@
1
- /**
2
- * Own how PTC Plus attaches to the host's program-execution seam.
3
- *
4
- * DSH registers one execution seam per generation. The current release
5
- * registers `ctx.ptcRuntime` (`@deepseek-ai/dsh-ptc-runtime`); the preceding one
6
- * registered `ctx.codeRuntime` (`@deepseek-ai/dsh-code-runtime`). Both run one
7
- * model-written program against host async bindings. They differ in the service
8
- * name, in the call shape (`resolve(request)` then `run(spec)` against a single
9
- * `run(request)`), and in the capability descriptors a provider publishes.
10
- * Which seam is live is decided by the registered service, never by a version
11
- * string, so a deployment on either generation obtains the same behavior.
12
- *
13
- * PTC Plus replaces the execution of session cells with its own persistent
14
- * REPL. The object DSH describes to the model is therefore the plugin's
15
- * execution, not the wrapped provider's, so `takeOver()` answers the
16
- * descriptors the plugin can honor and withholds the ones it cannot. The REPL
17
- * runs every cell in one long-lived worker rather than a fresh process per
18
- * call, applies the session's configured budgets rather than a per-call host
19
- * deadline, and performs no file confinement. DSH gates `timeoutMs` on
20
- * `timeout` and resolves or escalates file policy on `sandboxMode`, so
21
- * withholding those descriptors keeps DSH's authority decisions with DSH
22
- * instead of having this plugin accept an input it would then ignore.
23
- *
24
- * A seam-neutral request carries exactly `program`, `bindings`, and `signal`:
25
- * the fields PTC Plus consumes. A resolved spec's directory, deadline, and
26
- * authority describe the provider whose execution was replaced, and are neither
27
- * forwarded to the session kernel nor re-derived here.
28
- */
29
-
30
1
  /** Cordis service name registered by the current DSH generation. */
31
2
  export const EXECUTION_SEAM_SERVICE = 'ptcRuntime'
32
3
 
33
- /** Cordis service name registered by the preceding DSH generation. */
4
+ /** Historical request-only service name, retained with fixture evidence. */
34
5
  export const LEGACY_EXECUTION_SEAM_SERVICE = 'codeRuntime'
35
6
 
36
7
  const SEAM_SERVICES = Object.freeze([EXECUTION_SEAM_SERVICE, LEGACY_EXECUTION_SEAM_SERVICE])
8
+ const PLUGIN_EXECUTION_SEAM = Symbol.for('dsh-ptc-plus.execution-seam')
37
9
 
38
10
  /**
39
11
  * How long the plugin waits for the host's execution seam before it reports the
@@ -48,34 +20,12 @@ const SEAM_SERVICES = Object.freeze([EXECUTION_SEAM_SERVICE, LEGACY_EXECUTION_SE
48
20
  */
49
21
  const SEAM_ATTACHMENT_TIMEOUT_MS = 30_000
50
22
 
51
- /**
52
- * Descriptors the plugin replaces with the capabilities of its own execution.
53
- * `executionInstructions` describes the wrapped provider's program model, which
54
- * no longer applies; `timeout` and `sandboxMode` gate host inputs the plugin
55
- * cannot honor. Withdrawal is unconditional because the rule is a property of
56
- * the plugin's execution, not of a generation: the preceding generation's
57
- * contract defines none of these members and nothing in its install closure
58
- * reads them, so withdrawing there is inert, while a provider that published
59
- * them stays unable to outlive the takeover.
60
- */
61
- const WITHHELD_DESCRIPTORS = Object.freeze(['executionInstructions', 'sandboxMode', 'timeout'])
62
-
63
23
  /** Read a registered service without declaring an injection requirement. */
64
24
  function readService(ctx, name) {
65
25
  if (typeof ctx?.get === 'function') return ctx.get(name)
66
26
  return ctx?.[name]
67
27
  }
68
28
 
69
- function descriptorsMatch(left, right) {
70
- if (left === undefined || right === undefined) return left === right
71
- return left.value === right.value
72
- && left.get === right.get
73
- && left.set === right.set
74
- && left.writable === right.writable
75
- && left.enumerable === right.enumerable
76
- && left.configurable === right.configurable
77
- }
78
-
79
29
  /**
80
30
  * Build the seam handle for one live execution service.
81
31
  * @param {object} service - the registered `ctx.ptcRuntime` or `ctx.codeRuntime`.
@@ -85,6 +35,7 @@ export function createExecutionSeam(service, serviceName) {
85
35
  if (service === null || typeof service !== 'object') {
86
36
  throw new TypeError(`ptc-plus: ${serviceName} must be an execution service object`)
87
37
  }
38
+ if (typeof service[PLUGIN_EXECUTION_SEAM] === 'function') return service[PLUGIN_EXECUTION_SEAM]()
88
39
  const current = serviceName === EXECUTION_SEAM_SERVICE
89
40
  const upstreamRun = service.run
90
41
  if (typeof upstreamRun !== 'function') {
@@ -95,82 +46,87 @@ export function createExecutionSeam(service, serviceName) {
95
46
  throw new TypeError('ptc-plus: ptcRuntime.resolve must be a function')
96
47
  }
97
48
  let installed
98
- return Object.freeze({
49
+ const sourceListeners = new Set()
50
+ let available = true
51
+ const requireAvailable = () => {
52
+ if (!available) throw new Error(`ptc-plus: original ${serviceName} generation is unavailable`)
53
+ }
54
+ const requireCompatible = () => {
55
+ requireAvailable()
56
+ if (installed !== undefined && service.language !== installed.language) {
57
+ throw new Error(`ptc-plus: current original language ${JSON.stringify(service.language)} is incompatible with the active ${JSON.stringify(installed.language)} runtime`)
58
+ }
59
+ }
60
+ const provider = Object.freeze({
61
+ get language() { requireCompatible(); return service.language },
62
+ get isolation() { requireCompatible(); return installed === undefined ? service.isolation : 'worker-thread' },
63
+ get executionInstructions() { requireCompatible(); return installed === undefined ? service.executionInstructions ?? '' : '' },
64
+ get sandboxMode() { requireCompatible(); return installed === undefined ? service.sandboxMode : undefined },
65
+ get timeout() { requireCompatible(); return installed === undefined ? service.timeout : undefined },
66
+ resolve(request) {
67
+ requireCompatible()
68
+ return current ? upstreamResolve.call(service, request) : request
69
+ },
70
+ run(input) {
71
+ requireCompatible()
72
+ return installed === undefined ? upstreamRun.call(service, input) : installed.execute(input)
73
+ },
74
+ [PLUGIN_EXECUTION_SEAM]() { return seam },
75
+ })
76
+ const seam = Object.freeze({
99
77
  serviceName,
100
- language: service.language,
78
+ get language() { requireAvailable(); return service.language },
79
+ provider,
80
+ get active() { return installed !== undefined && service.language === installed.language },
81
+ onSourceChange(listener) {
82
+ sourceListeners.add(listener)
83
+ return () => sourceListeners.delete(listener)
84
+ },
85
+ sourceChanged() {
86
+ return Promise.all([...sourceListeners].map(listener => listener())).then(() => undefined)
87
+ },
101
88
  /**
102
89
  * Run one program through the provider this plugin wraps. The current
103
90
  * generation resolves the request before running it, exactly as its own
104
91
  * consumer does; the preceding generation accepts the request directly.
105
92
  */
106
93
  invokeUpstream(request) {
94
+ requireCompatible()
107
95
  return current
108
96
  ? upstreamRun.call(service, upstreamResolve.call(service, request))
109
97
  : upstreamRun.call(service, request)
110
98
  },
111
- /**
112
- * Install the plugin's execution entry, and its capability descriptors, on
113
- * the live service. Only one takeover is live at a time.
114
- * @param {(request: object) => Promise<object>} execute - the plugin's entry.
115
- * @returns {() => void} restores every descriptor this call replaced.
116
- */
117
- takeOver(execute) {
118
- if (installed !== undefined) throw new Error('ptc-plus: execution seam already taken over')
119
- const saved = new Map()
120
- const written = new Map()
121
- const requested = new Map()
122
- requested.set('run', {
123
- configurable: true,
124
- writable: true,
125
- value: current
126
- ? spec => execute({
127
- program: spec.program,
128
- bindings: spec.bindings,
129
- signal: spec.signal,
130
- })
131
- : request => execute(request),
132
- })
133
- for (const name of WITHHELD_DESCRIPTORS) {
134
- requested.set(name, { configurable: true, value: name === 'executionInstructions' ? '' : undefined })
135
- }
136
- const extensible = Object.isExtensible(service)
137
- for (const [name, descriptor] of requested) {
138
- const previous = Object.getOwnPropertyDescriptor(service, name)
139
- saved.set(name, previous)
140
- const next = { enumerable: previous?.enumerable ?? false, ...descriptor }
141
- requested.set(name, next)
142
- const probe = {}
143
- if (previous !== undefined) Object.defineProperty(probe, name, previous)
144
- if (!extensible) Object.preventExtensions(probe)
145
- Object.defineProperty(probe, name, next)
146
- }
147
- try {
148
- for (const [name, descriptor] of requested) {
149
- Object.defineProperty(service, name, descriptor)
150
- written.set(name, Object.getOwnPropertyDescriptor(service, name))
151
- }
152
- } catch (error) {
153
- for (const name of [...written.keys()].reverse()) {
154
- if (!descriptorsMatch(Object.getOwnPropertyDescriptor(service, name), written.get(name))) continue
155
- const descriptor = saved.get(name)
156
- if (descriptor === undefined) delete service[name]
157
- else Object.defineProperty(service, name, descriptor)
158
- }
159
- throw error
160
- }
99
+ runUpstream(spec) {
100
+ requireCompatible()
101
+ return upstreamRun.call(service, spec)
102
+ },
103
+ installExecute(execute) {
104
+ requireAvailable()
105
+ if (typeof execute !== 'function') throw new TypeError('ptc-plus: execution entry must be a function')
106
+ if (installed !== undefined) throw new Error('ptc-plus: execution seam already installed')
107
+ const entry = { execute, language: service.language }
108
+ installed = entry
161
109
  const restore = () => {
162
- if (installed !== restore) return
110
+ if (installed !== entry) return
163
111
  installed = undefined
164
- for (const [name, descriptor] of saved) {
165
- if (!descriptorsMatch(Object.getOwnPropertyDescriptor(service, name), written.get(name))) continue
166
- if (descriptor === undefined) delete service[name]
167
- else Object.defineProperty(service, name, descriptor)
168
- }
169
112
  }
170
- installed = restore
171
113
  return restore
172
114
  },
115
+ retire() {
116
+ available = false
117
+ installed = undefined
118
+ sourceListeners.clear()
119
+ },
173
120
  })
121
+ return seam
122
+ }
123
+
124
+ function registeredExecutionSeam(service, serviceName) {
125
+ const seam = createExecutionSeam(service, serviceName)
126
+ if (typeof service[PLUGIN_EXECUTION_SEAM] !== 'function') {
127
+ throw new Error(`ptc-plus: ${serviceName} requires the public execution-provider bundle composition`)
128
+ }
129
+ return seam
174
130
  }
175
131
 
176
132
  /**
@@ -259,7 +215,7 @@ export function installExecutionSeam(ctx, {
259
215
  entry = { name, scope }
260
216
  live = entry
261
217
  registerLifecycle(entry, name, scope)
262
- const attached = attach(scope, createExecutionSeam(scope?.[name] ?? readService(ctx, name), name))
218
+ const attached = attach(scope, registeredExecutionSeam(scope?.[name] ?? readService(ctx, name), name))
263
219
  entry.attached = attached
264
220
  return attached
265
221
  })
@@ -292,7 +248,7 @@ export function installExecutionSeam(ctx, {
292
248
  const entry = { name, scope }
293
249
  live = entry
294
250
  try {
295
- entry.attached = attach(scope, createExecutionSeam(scope?.[name] ?? readService(ctx, name), name))
251
+ entry.attached = attach(scope, registeredExecutionSeam(scope?.[name] ?? readService(ctx, name), name))
296
252
  } catch (error) {
297
253
  live = undefined
298
254
  throw error
@@ -15,6 +15,17 @@ const ISOLATED_STOP_MS = 5000
15
15
  /** Bound on the helper's output pipes really ending after its process exited. */
16
16
  const ISOLATED_OUTPUT_MS = 5000
17
17
 
18
+ export function isolatedWorkerEntry(entry) {
19
+ if (!(entry instanceof URL) && (typeof entry !== 'string' || entry.length === 0)) {
20
+ throw new TypeError('isolated worker entry must be a URL or a non-empty string filename')
21
+ }
22
+ return entry instanceof URL
23
+ ? entry.href
24
+ : entry.slice(0, 5).toLowerCase() === 'file:'
25
+ ? new URL(entry).href
26
+ : pathToFileURL(resolve(entry)).href
27
+ }
28
+
18
29
  /**
19
30
  * A worker-shaped transport whose worker lives in a killable helper process.
20
31
  *
@@ -29,17 +40,10 @@ export class IsolatedWorker extends EventEmitter {
29
40
  super()
30
41
  // The entry follows the worker constructor's contract: a URL or a string
31
42
  // filename. Anything else fails here, before a helper process exists.
32
- if (!(entry instanceof URL) && (typeof entry !== 'string' || entry.length === 0)) {
33
- throw new TypeError('isolated worker entry must be a URL or a non-empty string filename')
34
- }
35
43
  // The init message crosses advanced IPC, which cannot rebuild a URL object, so
36
44
  // the boundary normalizes it to one absolute file URL string. A path follows
37
45
  // the worker constructor's rule: resolved from the current working directory.
38
- const entrySpec = entry instanceof URL
39
- ? entry.href
40
- : entry.slice(0, 5).toLowerCase() === 'file:'
41
- ? new URL(entry).href
42
- : pathToFileURL(resolve(entry)).href
46
+ const entrySpec = isolatedWorkerEntry(entry)
43
47
  this.stdout = new PassThrough()
44
48
  this.stderr = new PassThrough()
45
49
  this.protocol = protocol
@@ -1,4 +1,4 @@
1
- import { recoveryTipIdentity } from './runtime-messages.js'
1
+ import { PTC_STATE_NAMES, recoveryTipIdentity } from './runtime-messages.js'
2
2
 
3
3
  const PLATFORM_CAUSE_CODES = new Set(['EACCES', 'EINVAL', 'ENOTDIR', 'ENOENT', 'UNKNOWN'])
4
4
  const TIP_CONTEXT_PREFIX = 'tools:ptc-plus-tip/'
@@ -8,27 +8,58 @@ const TIP_PREFIXES = Object.freeze({
8
8
  })
9
9
 
10
10
  // TODO(dsh-tips-api): replace this local provider with an adapter after dsh-tips publishes a stable facts and decision interface.
11
- function tipHistory(view) {
12
- const active = new Map()
13
- const seen = new Set()
14
- const history = []
11
+ export function createRuntimeHistory() {
12
+ return { seenNames: {}, tips: {}, hadState: false, hadCatalog: false }
13
+ }
14
+
15
+ export function advanceRuntimeHistory(previous, { sections = [], notice, snapshot = false, catalog = false,
16
+ index, contextStep, lastSuccessfulRunIndex, resetSuccess = false }) {
17
+ let next = previous
18
+ const revise = () => {
19
+ if (next === previous) next = { ...previous, tips: { ...previous.tips }, seenNames: { ...previous.seenNames } }
20
+ }
21
+ if (resetSuccess) {
22
+ revise()
23
+ for (const [id, tip] of Object.entries(next.tips)) next.tips[id] = { ...tip, unresolved: 0 }
24
+ for (const [name, deliveredIndex] of Object.entries(next.seenNames)) {
25
+ const identity = recoveryTipIdentity(name)
26
+ if (deliveredIndex > (lastSuccessfulRunIndex ?? -1)) next.tips[identity.id].unresolved += 1
27
+ }
28
+ }
29
+ const hadState = snapshot || sections.some(section => PTC_STATE_NAMES.includes(section.name))
30
+ if ((hadState && !next.hadState) || (catalog && !next.hadCatalog)) {
31
+ revise()
32
+ next.hadState ||= hadState
33
+ next.hadCatalog ||= catalog
34
+ }
35
+ for (const name of [...sections.map(section => section.name), ...(notice === undefined ? [] : [notice])]) {
36
+ const identity = recoveryTipIdentity(name)
37
+ if (identity === undefined || Object.hasOwn(next.seenNames, name)) continue
38
+ revise()
39
+ next.seenNames[name] = index
40
+ const earlier = next.tips[identity.id]
41
+ next.tips[identity.id] = {
42
+ highestOrdinal: Math.max(earlier?.highestOrdinal ?? 0, identity.ordinal),
43
+ contextStep,
44
+ unresolved: (earlier?.unresolved ?? 0) + (index > (lastSuccessfulRunIndex ?? -1) ? 1 : 0),
45
+ }
46
+ }
47
+ return next
48
+ }
49
+
50
+ export function runtimeHistoryForView(view) {
51
+ if (view.runtimeHistory !== undefined) return view.runtimeHistory
52
+ let history = createRuntimeHistory()
53
+ history.hadState = view.ptcMessages?.some(record => record.form === 'snapshot') === true
54
+ history.hadCatalog = view.ptcMessages?.some(record => record.form === 'catalog') === true
15
55
  const snapshots = [
16
- ...view.systemPromptSnapshots,
56
+ ...(view.systemPromptSnapshots ?? []),
17
57
  ...(view.ptcMessages ?? []).filter(record => record.form === 'notice').map(record => ({
18
58
  ...record, sections: [{ name: record.name, text: record.text }],
19
59
  })),
20
60
  ].sort((left, right) => left.index - right.index)
21
61
  for (const snapshot of snapshots) {
22
- const next = new Map(snapshot.sections.map(section => [section.name, section.text]))
23
- for (const [name] of next) {
24
- if (active.has(name) || seen.has(name)) continue
25
- const identity = recoveryTipIdentity(name)
26
- if (identity === undefined) continue
27
- seen.add(name)
28
- history.push({ ...identity, index: snapshot.index, contextStep: snapshot.contextStep })
29
- }
30
- active.clear()
31
- for (const entry of next) active.set(...entry)
62
+ history = advanceRuntimeHistory(history, { ...snapshot, lastSuccessfulRunIndex: view.lastSuccessfulRunIndex })
32
63
  }
33
64
  return history
34
65
  }
@@ -79,14 +110,12 @@ export function latestRecoveryTip(view, config) {
79
110
  if (candidate === undefined) return undefined
80
111
  // Cooldown, escalation, and ordinal are per trigger kind: one kind's tip must
81
112
  // not suppress a different kind.
82
- const matching = tipHistory(view).filter(item => item.id === candidate.id)
83
- const lastTip = matching.at(-1)
113
+ const lastTip = runtimeHistoryForView(view).tips[candidate.id]
84
114
  if (lastTip !== undefined && view.contextStep - lastTip.contextStep < config.cooldownMessages) return undefined
85
- const unresolved = matching.filter(item => item.index > (view.lastSuccessfulRunIndex ?? -1))
86
- const ordinal = matching.reduce((highest, item) => Math.max(highest, item.ordinal), 0) + 1
115
+ const ordinal = (lastTip?.highestOrdinal ?? 0) + 1
87
116
  if (!Number.isSafeInteger(ordinal)) return undefined
88
117
  return {
89
118
  name: `${TIP_CONTEXT_PREFIX}${candidate.id}/${ordinal}`,
90
- text: renderTip(candidate.id, unresolved.length >= config.escalationFailures, candidate),
119
+ text: renderTip(candidate.id, (lastTip?.unresolved ?? 0) >= config.escalationFailures, candidate),
91
120
  }
92
121
  }
@@ -252,29 +252,15 @@ function assemble(source, replacements) {
252
252
  return changed ? { code: chunks.join('') } : { error: 'edits must change at least one matched fragment' }
253
253
  }
254
254
 
255
- function editRunCodeParameterBranch(operation, operationSchema) {
256
- return {
257
- type: 'object',
258
- additionalProperties: false,
259
- properties: {
260
- [operation]: operationSchema,
261
- [EXPECTED_TARGET_CALL_SEQ]: {
262
- type: 'integer', minimum: 0,
263
- description: 'Optional target precondition copied from a validated diagnostic. The edit is rejected if the captured cell has another call sequence.',
264
- },
265
- },
266
- required: [operation],
267
- }
268
- }
269
-
270
255
  export function editRunCodeSchema() {
271
256
  return {
272
257
  name: 'edit_run_code',
273
258
  description: 'Edit the most recent eligible cell captured at dispatch and run the complete corrected cell. A successful edit becomes the next target. This reruns the entire cell, including prior effects; use it after pre-execution rejection or when repeating those effects is safe under the called API\'s retry rules. Otherwise continue in a new run_code cell using retained values. A diagnostic may supply a localized correction; use it when it matches your intent and preserve its expected_target_call_seq. Send exactly one edits or regex_edits array: edits replaces unique literal fragments; regex_edits replaces a counted set of pattern matches. All ranges must be non-overlapping in the original cell. Regex replacements use JavaScript $1/$<name> captures, not \\1.',
274
259
  parameters: {
275
260
  type: 'object',
276
- oneOf: [
277
- editRunCodeParameterBranch('edits', {
261
+ additionalProperties: false,
262
+ properties: {
263
+ edits: {
278
264
  type: 'array', minItems: 1, maxItems: EDIT_LIMITS.exactEdits,
279
265
  description: 'Atomic exact replacements, all resolved against the original target cell.',
280
266
  items: {
@@ -285,8 +271,8 @@ export function editRunCodeSchema() {
285
271
  },
286
272
  required: ['old_string', 'new_string'],
287
273
  },
288
- }),
289
- editRunCodeParameterBranch('regex_edits', {
274
+ },
275
+ regex_edits: {
290
276
  type: 'array', minItems: 1, maxItems: EDIT_LIMITS.regexEdits,
291
277
  description: 'Atomic regular-expression replacements, all matched against the original target cell with JavaScript pattern and replacement semantics; each replacement is a JavaScript template (see replacement).',
292
278
  items: {
@@ -299,8 +285,12 @@ export function editRunCodeSchema() {
299
285
  },
300
286
  required: ['pattern', 'flags', 'replacement', 'expected_matches'],
301
287
  },
302
- }),
303
- ],
288
+ },
289
+ [EXPECTED_TARGET_CALL_SEQ]: {
290
+ type: 'integer', minimum: 0,
291
+ description: 'Optional target precondition copied from a validated diagnostic. The edit is rejected if the captured cell has another call sequence.',
292
+ },
293
+ },
304
294
  },
305
295
  }
306
296
  }