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,16 +1,17 @@
1
1
  import {
2
2
  foldSessionTimeline,
3
3
  } from './session-journal-recovery.js'
4
+ import { isCanonicalSequence } from './session-journal.js'
4
5
  import { sessionEvents } from './session-events.js'
5
6
  import { readRuntimeMessage } from './runtime-messages.js'
6
7
 
7
- const SYSTEM_PROMPT_PLUGIN = '@deepseek-ai/dsh-system-prompt'
8
+ import { isHostRuntimeContextSource } from './message-sources.js'
8
9
  const SYSTEM_PROMPT_CLEARED = 'Current runtime context: none. Earlier runtime-context snapshots no longer apply.'
9
10
 
10
11
  /** DSH's empty aggregate uses an exact owner marker without a snapshot form. */
11
12
  export function systemPromptSnapshotSections(message) {
12
13
  const source = message?.source
13
- if (source?.kind !== 'plugin' || source.plugin !== SYSTEM_PROMPT_PLUGIN) return undefined
14
+ if (!isHostRuntimeContextSource(source)) return undefined
14
15
  if (source.form === undefined && Array.isArray(message.content) && message.content.length === 1
15
16
  && message.content[0]?.type === 'text' && message.content[0].text === SYSTEM_PROMPT_CLEARED) {
16
17
  return Object.freeze([])
@@ -101,7 +102,7 @@ export function projectSessionLog(agent, requestedEdit) {
101
102
  try {
102
103
  const nodes = agent?.session?.surface?.nodes
103
104
  const knownSeqs = new Set(events.map(event => event?.seq))
104
- if (Array.isArray(nodes) && nodes.every(seq => Number.isSafeInteger(seq) && seq >= 0 && knownSeqs.has(seq))) {
105
+ if (Array.isArray(nodes) && nodes.every(seq => isCanonicalSequence(seq) && knownSeqs.has(seq))) {
105
106
  visibleRuntimeMessages = Object.freeze(nodes.flatMap(seq => {
106
107
  const record = runtimeMessagesBySeq.get(seq)
107
108
  return record === undefined ? [] : [record]
@@ -126,6 +127,6 @@ export function projectSessionLog(agent, requestedEdit) {
126
127
  /** Return the target snapshot captured at one persisted edit call event. */
127
128
  export function editTargetForCall(agent, callId, callSeq) {
128
129
  if (typeof callId !== 'string' || callId.length === 0
129
- || !Number.isSafeInteger(callSeq) || callSeq < 0) return undefined
130
+ || !isCanonicalSequence(callSeq)) return undefined
130
131
  return projectSessionLog(agent, { callId, callSeq }).requestedEditTarget
131
132
  }
@@ -4,6 +4,7 @@ import { diagnostic, renderDiagnostic } from './diagnostic.js'
4
4
  import { createFailureTracker, messageOf } from './failure-reporting.js'
5
5
  import {
6
6
  createJournal,
7
+ isCanonicalSequence,
7
8
  liveToolCallSeq,
8
9
  normalizeJournal,
9
10
  reduceStateOperations,
@@ -29,6 +30,17 @@ import {
29
30
  } from './repl-memory-projection.js'
30
31
 
31
32
  const WORKER_URL = new URL('./kernel-worker.js', import.meta.url)
33
+ const UNAVAILABLE_SURFACE_GENERATION = Symbol('unavailableSurfaceGeneration')
34
+
35
+ function surfaceGeneration(session, provenanceRequired = true) {
36
+ try {
37
+ return session?.surface?.replaceGeneration
38
+ ?? (provenanceRequired ? UNAVAILABLE_SURFACE_GENERATION : undefined)
39
+ } catch {
40
+ return provenanceRequired ? UNAVAILABLE_SURFACE_GENERATION : undefined
41
+ }
42
+ }
43
+
32
44
  function recoveryDiagnostic(count) {
33
45
  return diagnostic({
34
46
  code: 'PTC-R002',
@@ -66,20 +78,16 @@ class ReplayCancelled extends Error {
66
78
  }
67
79
 
68
80
  class SessionKernel {
69
- constructor({ config, history, cwd, session, userBindingsCwd, withInitiator, observeValues }) {
81
+ constructor({ config, history, cwd, session, surfaceProvenanceRequired, userBindingsCwd, withInitiator, observeValues }) {
70
82
  this.config = config
71
83
  this.history = history
72
84
  this.initialRecoveryBoundary = recoveryBoundaryForHistory(history)
73
- this.surfaceGeneration = undefined
74
- try {
75
- this.surfaceGeneration = session?.surface?.replaceGeneration
76
- } catch {
77
- this.surfaceGeneration = undefined
78
- }
85
+ this.surfaceGeneration = surfaceGeneration(session, surfaceProvenanceRequired)
79
86
  this.cwd = cwd
80
87
  this.userBindingsCwd = userBindingsCwd
81
88
  this.observeValues = observeValues
82
89
  this.session = session
90
+ this.surfaceProvenanceRequired = surfaceProvenanceRequired
83
91
  this.withInitiator = withInitiator
84
92
  this.durability = durabilityState()
85
93
  this.bindingCatalog = new BindingCatalog()
@@ -103,10 +111,19 @@ class SessionKernel {
103
111
  cwd,
104
112
  compilerCache: compilerWorkerCache,
105
113
  onMessage: message => this.cellExecutor.onMessage(message),
114
+ onUnmatchedOutputFailure: message => {
115
+ const active = this.active
116
+ if (active === undefined) return false
117
+ active.resolve({ logs: [], error: { kind: 'worker-exit', message } }, true)
118
+ return true
119
+ },
106
120
  onFailure: message => {
107
121
  this.pendingInspection?.finish()
108
122
  this.workerObservation = undefined
109
- this.active?.resolve({ logs: [], error: { kind: 'worker-exit', message } }, true)
123
+ const active = this.active
124
+ if (active === undefined) return false
125
+ active.resolve({ logs: [], error: { kind: 'worker-exit', message } }, true)
126
+ return true
110
127
  },
111
128
  })
112
129
  this.failures = createFailureTracker()
@@ -181,10 +198,10 @@ class SessionKernel {
181
198
  const current = () => {
182
199
  if (this.disposed || !this.config.replViewEnabled || signal?.aborted
183
200
  || this.client.worker === undefined || this.unsettledCells > 0) return false
184
- try {
185
- return this.session?.surface?.replaceGeneration === this.surfaceGeneration
186
- && isDeepStrictEqual(expected, createReplMemorySnapshot(this.bindingCatalog.snapshot()))
187
- } catch { return false }
201
+ return surfaceGeneration(this.session, this.surfaceProvenanceRequired) === this.surfaceGeneration
202
+ && (!this.surfaceProvenanceRequired
203
+ || this.surfaceGeneration !== UNAVAILABLE_SURFACE_GENERATION)
204
+ && isDeepStrictEqual(expected, createReplMemorySnapshot(this.bindingCatalog.snapshot()))
188
205
  }
189
206
  return new Promise(resolve => {
190
207
  let finished = false
@@ -234,13 +251,10 @@ class SessionKernel {
234
251
  const finishResult = result => recoveryBoundaries.length === 0
235
252
  ? result
236
253
  : { ...result, recoveryBoundaries: recoveryBoundaries.map(boundary => ({ ...boundary })) }
237
- let currentGeneration
238
- try {
239
- currentGeneration = this.session?.surface?.replaceGeneration
240
- } catch {
241
- currentGeneration = undefined
242
- }
243
- if (currentGeneration !== undefined && currentGeneration !== this.surfaceGeneration) {
254
+ const currentGeneration = surfaceGeneration(this.session, this.surfaceProvenanceRequired)
255
+ const hasLiveState = this.liveCallSeqs.size > 0
256
+ if (this.surfaceProvenanceRequired
257
+ && (currentGeneration !== this.surfaceGeneration || hasLiveState)) {
244
258
  this.surfaceGeneration = currentGeneration
245
259
  const visibleCallSeqs = visibleExecutableCallSeqs(this.session)
246
260
  const contractsLiveState = [...this.liveCallSeqs].some(callSeq => (
@@ -597,6 +611,7 @@ function sessionOf(sessionContext) {
597
611
  callId: undefined,
598
612
  persistedCallSeq: undefined,
599
613
  cwd: undefined,
614
+ surfaceProvenanceRequired: false,
600
615
  }
601
616
  }
602
617
  const session = sessionContext.session
@@ -606,6 +621,7 @@ function sessionOf(sessionContext) {
606
621
  callId: sessionContext.callId,
607
622
  persistedCallSeq: sessionContext.persistedCallSeq,
608
623
  cwd: typeof session?.header?.cwd === 'string' ? session.header.cwd : undefined,
624
+ surfaceProvenanceRequired: true,
609
625
  }
610
626
  }
611
627
 
@@ -657,12 +673,19 @@ export class SessionRuntime {
657
673
  return completed({ logs: [], error: { kind: 'exception', message: messageOf(error) } })
658
674
  }
659
675
  request = { ...request, bindings: bindingDescriptors.namespaces, bindingDescriptors }
660
- const { id: sessionId, session, callId, persistedCallSeq, cwd } = sessionOf(sessionContext)
676
+ const {
677
+ id: sessionId,
678
+ session,
679
+ callId,
680
+ persistedCallSeq,
681
+ cwd,
682
+ surfaceProvenanceRequired,
683
+ } = sessionOf(sessionContext)
661
684
  let callSeq
662
685
  let sourceCallSeq
663
686
  try {
664
687
  if (persistedCallSeq !== undefined
665
- && (!Number.isSafeInteger(persistedCallSeq) || persistedCallSeq < 0)) {
688
+ && !isCanonicalSequence(persistedCallSeq)) {
666
689
  throw new Error('persisted tool call sequence must be a non-negative safe integer')
667
690
  }
668
691
  if (cellConfig.durableReplay) {
@@ -700,6 +723,7 @@ export class SessionRuntime {
700
723
  history,
701
724
  cwd,
702
725
  session,
726
+ surfaceProvenanceRequired,
703
727
  userBindingsCwd: this.userBindingsCwd,
704
728
  withInitiator: this.withInitiator,
705
729
  observeValues: () => this.config.replViewEnabled && this.observeSession(sessionId),
@@ -713,6 +737,7 @@ export class SessionRuntime {
713
737
  policies.bindingPolicy,
714
738
  policies.rewritesEnabled,
715
739
  policies.languageSemantics,
740
+ policies.moduleTransform,
716
741
  )
717
742
  const workerReservation = kernel.reserveWorkerConfiguration(cellConfig)
718
743
  let result
@@ -1,10 +1,12 @@
1
1
  // Runtime state transitions shared by the session coordinator and cell executor.
2
2
 
3
3
  import { userBindingCatalogEntries } from './user-bindings.js'
4
+ import { boundedDefinitionSource } from './repl-memory-projection.js'
4
5
  import { LEGACY_USER_BINDINGS_SHADOW_POLICY } from './session-journal-schema.js'
5
6
  import { advanceLegacyBindings } from './legacy-binding-catalog.js'
6
7
  import { dynamicBindingOrigin } from './dynamic-binding-evidence.js'
7
8
  import { LEGACY_LANGUAGE_SEMANTICS, normalizeLanguageSemantics } from './language-semantics.js'
9
+ import { sourceLineStarts, sourceOffsetAtPosition } from './source-position-map.js'
8
10
 
9
11
  /**
10
12
  * Reuse counts belong to a logical binding identity, not to one declaration
@@ -236,7 +238,7 @@ export class BindingCatalog {
236
238
  definition: previous?.origin === undefined && previous?.definition !== undefined
237
239
  ? previous.definition
238
240
  : fact.state === 'local' && typeof source === 'string' && source.length > 0
239
- ? Object.freeze({ source: source.slice(0, MAX_DEFINITION_SOURCE_LENGTH), line: 1, column: 1 }) : undefined,
241
+ ? Object.freeze({ source: boundedDefinitionSource(source), line: 1, column: 1 }) : undefined,
240
242
  writable: previous?.writable ?? true,
241
243
  userBindingState: fact.state,
242
244
  unavailable: fact.state !== 'local',
@@ -290,41 +292,17 @@ function applyRootBindingFacts(entries, facts, rootCandidates, dynamicOrigins, i
290
292
  }
291
293
  }
292
294
 
293
- const MAX_DEFINITION_SOURCE_LENGTH = 1024
294
-
295
- function lineOffsets(source) {
296
- const offsets = [0]
297
- for (let index = 0; index < source.length; index += 1) {
298
- if (source.charCodeAt(index) === 10) offsets.push(index + 1)
299
- }
300
- return offsets
301
- }
302
-
303
- function offsetAt(offsets, sourceLength, position) {
304
- if (position === null || typeof position !== 'object'
305
- || !Number.isSafeInteger(position.line) || position.line < 1
306
- || !Number.isSafeInteger(position.column) || position.column < 1
307
- || position.line > offsets.length) return undefined
308
- const lineStart = offsets[position.line - 1]
309
- const lineEnd = position.line < offsets.length ? offsets[position.line] - 1 : sourceLength
310
- const offset = lineStart + position.column - 1
311
- return offset <= lineEnd ? offset : undefined
312
- }
313
-
314
295
  function sourceDefinitionExtractor(source) {
315
- const offsets = typeof source === 'string' ? lineOffsets(source) : undefined
296
+ const offsets = typeof source === 'string' ? sourceLineStarts(source) : undefined
316
297
  const extracted = new Map()
317
298
  return span => {
318
299
  if (offsets === undefined || span === null || typeof span !== 'object') return undefined
319
- const start = offsetAt(offsets, source.length, span)
320
- const end = offsetAt(offsets, source.length, span.end)
300
+ const start = sourceOffsetAtPosition(source, span, offsets)
301
+ const end = sourceOffsetAtPosition(source, span.end, offsets)
321
302
  if (start === undefined || end === undefined || end <= start) return undefined
322
303
  const key = `${start}:${end}`
323
304
  if (extracted.has(key)) return extracted.get(key)
324
- const length = end - start
325
- const bounded = length <= MAX_DEFINITION_SOURCE_LENGTH
326
- ? source.slice(start, end)
327
- : `${source.slice(start, start + MAX_DEFINITION_SOURCE_LENGTH - 3)}...`
305
+ const bounded = boundedDefinitionSource(source.slice(start, end))
328
306
  if (bounded.length === 0) return undefined
329
307
  const definition = Object.freeze({ source: bounded, line: span.line, column: span.column })
330
308
  extracted.set(key, definition)
@@ -1,3 +1,55 @@
1
+ /**
2
+ * Whether the installed settings generation owns the plugin's exported `Config`
3
+ * document.
4
+ *
5
+ * A generation that still attaches through the provider section installer — on
6
+ * its settings service, or on the package helper an older release published —
7
+ * validates the plugin's own fields and has no volatile contract. The
8
+ * generation that serves the exported `Config` itself, and commits only fields
9
+ * marked volatile, publishes neither installer. Both probes read the installed
10
+ * public surface, so no release identity selects the shape.
11
+ *
12
+ * @param settingsModule - the installed public settings package surface.
13
+ * @returns whether that generation serves the plugin's Config document itself.
14
+ */
15
+ export function hostOwnsSettingsDocument(settingsModule) {
16
+ return typeof settingsModule?.installSettingsSection !== 'function'
17
+ && typeof settingsModule?.SettingsProvider?.prototype?.installSection !== 'function'
18
+ }
19
+
20
+ /**
21
+ * Build one exported `Config` field for the installed settings generation.
22
+ *
23
+ * A generation that serves the document itself keeps a form mounted and commits
24
+ * new values into the retained handle only for fields whose nearest schema
25
+ * ancestor is marked volatile, so the marker is applied there. A builder that
26
+ * does not publish that capability causes a dependency error during import; silently
27
+ * emitting ordinary fields would hide the form and every Client feature gated
28
+ * by its settings snapshot. The generation that installs its own section
29
+ * validates the same fields itself
30
+ * and must not receive the marker, because its validated defaults would become
31
+ * live handles its document path never commits.
32
+ *
33
+ * @param options.Schema - the schema builder the plugin imports.
34
+ * @param options.field - one field definition from `internal/config-spec.js`.
35
+ * @param options.settingsModule - the installed public settings package surface.
36
+ * @returns the Cordis schema field for that definition.
37
+ */
38
+ export function configFieldSchema({ Schema, field, settingsModule }) {
39
+ const base = field.type === 'boolean'
40
+ ? Schema.boolean().default(field.default)
41
+ : field.type === 'enum'
42
+ // Keep omission visible until resolveConfig migrates legacy policies.
43
+ ? Schema.union(field.options.map(option => Schema.const(option)))
44
+ : Schema.number().step(1).min(field.min).max(field.max).default(field.default)
45
+ const described = base.description(field.description)
46
+ if (!hostOwnsSettingsDocument(settingsModule)) return described
47
+ if (typeof described.volatile !== 'function') {
48
+ throw new Error('ptc-plus: the host settings contract requires @deepseek-ai/schemastery with volatile(); reinstall the plugin to update its dependencies')
49
+ }
50
+ return described.volatile()
51
+ }
52
+
1
53
  /** Install an optional settings section across current and legacy DSH settings APIs. */
2
54
  export function installSettingsSectionCompat({
3
55
  ctx,
@@ -6,6 +58,7 @@ export function installSettingsSectionCompat({
6
58
  schema,
7
59
  entry,
8
60
  hooks,
61
+ ownerFiber,
9
62
  onProvider,
10
63
  }) {
11
64
  ctx.inject(['settings'], settingsContext => {
@@ -16,7 +69,19 @@ export function installSettingsSectionCompat({
16
69
  return
17
70
  }
18
71
  if (typeof settingsModule?.installSettingsSection !== 'function') {
19
- throw new Error('ptc-plus: incompatible settings service; expected installSection or installSettingsSection')
72
+ // The newest settings generation owns its document and card from the
73
+ // plugin's exported Config, so it exposes no section installer to call.
74
+ // Treat that shape as a host-owned surface instead of a load failure;
75
+ // the host commits volatile-only changes into the retained handles and
76
+ // notifies this fiber through `loader/volatile-update`.
77
+ if (typeof provider?.configure === 'function') {
78
+ // The host records the own-page policy against the profile entry's own
79
+ // fiber, so the owner is the plugin's entry context rather than the
80
+ // scope this installation happens to run in.
81
+ const dispose = provider.configure({ auto: false }, ownerFiber ?? ctx.fiber)
82
+ ctx.effect?.(() => dispose, 'ptc-plus settings page policy')
83
+ }
84
+ return
20
85
  }
21
86
  const mountedOwner = new Proxy(ctx, {
22
87
  get(target, property) {
@@ -117,6 +117,25 @@ export function sourceOffsetAt(sourceMap, generatedOffset) {
117
117
  return segmentOffsetAt(segment, generatedOffset)
118
118
  }
119
119
 
120
+ /** Whether a complete generated interval still contains its exact original text. */
121
+ export function sourceRangeHasOriginalText(sourceMap, generatedSource, originalSource, generatedStart, generatedEnd) {
122
+ if (typeof generatedSource !== 'string' || typeof originalSource !== 'string'
123
+ || generatedEnd <= generatedStart || generatedStart < 0 || generatedEnd > generatedSource.length) return false
124
+ let cursor = generatedStart
125
+ for (let index = segmentIndexAt(sourceMap, generatedStart); index < sourceMap.length && cursor < generatedEnd; index += 1) {
126
+ const segment = sourceMap.at(index)
127
+ if (segment.generatedStart > cursor || segment.generatedEnd <= cursor) break
128
+ const end = Math.min(generatedEnd, segment.generatedEnd)
129
+ const length = end - cursor
130
+ const originalStart = segmentOffsetAt(segment, cursor)
131
+ if (segment.originalEnd <= segment.originalStart
132
+ || originalStart + length > originalSource.length
133
+ || generatedSource.slice(cursor, end) !== originalSource.slice(originalStart, originalStart + length)) return false
134
+ cursor = end
135
+ }
136
+ return cursor === generatedEnd
137
+ }
138
+
120
139
  function segmentOffsetAt(segment, generatedOffset) {
121
140
  const generatedLength = segment.generatedEnd - segment.generatedStart
122
141
  const originalLength = segment.originalEnd - segment.originalStart
@@ -268,6 +287,25 @@ export function sourceLineStarts(source) {
268
287
  return starts
269
288
  }
270
289
 
290
+ /** Convert one-based JavaScript source coordinates without admitting a
291
+ * position inside a line terminator. End coordinates may point immediately
292
+ * after the final content character. */
293
+ export function sourceOffsetAtPosition(source, position, starts) {
294
+ if (typeof source !== 'string' || position === null || typeof position !== 'object'
295
+ || !Number.isSafeInteger(position.line) || position.line < 1
296
+ || !Number.isSafeInteger(position.column) || position.column < 1) return undefined
297
+ const lineStarts = starts ?? sourceLineStarts(source)
298
+ if (!Array.isArray(lineStarts) || position.line > lineStarts.length) return undefined
299
+ const lineStart = lineStarts[position.line - 1]
300
+ const nextLineStart = lineStarts[position.line]
301
+ let lineEnd = source.length
302
+ if (nextLineStart !== undefined) {
303
+ lineEnd = nextLineStart - (source[nextLineStart - 2] === '\r' && source[nextLineStart - 1] === '\n' ? 2 : 1)
304
+ }
305
+ const offset = lineStart + position.column - 1
306
+ return offset <= lineEnd ? offset : undefined
307
+ }
308
+
271
309
  /** Visit maintained emitter coordinates without re-encoding available raw maps.
272
310
  * All line numbers at this boundary are zero-based; absent origins stay absent. */
273
311
  export function visitSourceMappings(transformed, visit) {
@@ -191,8 +191,10 @@ function rewriteRootReferences(code, sourceMap, runtime, candidateNames, candida
191
191
  }
192
192
  handled.add(path.node.start)
193
193
  } })
194
+ const classificationSource = applySourceEdits(code, sourceMap, classificationEdits)
194
195
  return { ...applySourceEdits(code, sourceMap, edits),
195
- classificationCode: applySourceEdits(code, sourceMap, classificationEdits).code,
196
+ classificationCode: classificationSource.code,
197
+ classificationSourceMap: classificationSource.sourceMap,
196
198
  implicitDeclarations: [...implicitDeclarations.values()],
197
199
  readNames: [...readNames] }
198
200
  }
@@ -1,7 +1,9 @@
1
1
  import loadAmaro from './compiler-amaro.cjs'
2
2
 
3
- export { LEGACY_USER_BINDING_TRANSFORM, USER_BINDING_TRANSFORM, PROTECTED_MODULE_TRANSFORM,
4
- moduleTransformForLanguage, supportedUserBindingTransform } from './module-transform-contract.js'
3
+ export { LEGACY_USER_BINDING_TRANSFORM, PREVIOUS_USER_BINDING_TRANSFORM, USER_BINDING_TRANSFORM,
4
+ PROTECTED_MODULE_TRANSFORM, isStatefulUserBindingTransform,
5
+ historicalModuleTransformForLanguage, moduleTransformForLanguage, moduleTransformMatchesLanguage,
6
+ normalizeModuleTransform, supportedUserBindingTransform } from './module-transform-contract.js'
5
7
 
6
8
  /** Keep binding-module lowering independent of the Host's experimental transform API. */
7
9
  export function transformTypeScriptModule(source) {