dsh-ptc-plus 0.3.1 → 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 (200) hide show
  1. package/README.en.md +66 -73
  2. package/README.md +66 -73
  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 +30610 -722
  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 +6 -4
  25. package/docs/adr/0007-separate-worker-transport-from-session-semantics.md +15 -3
  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 +9 -3
  31. package/docs/adr/0014-persist-cell-rewrite-policy.md +11 -5
  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 +8 -6
  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 +17 -3
  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 +82 -22
  48. package/docs/binding-review-placement-design.md +200 -0
  49. package/docs/capability-projection.md +1 -1
  50. package/docs/client-ui.md +96 -20
  51. package/docs/durability-design.md +61 -24
  52. package/docs/evaluation.md +78 -6
  53. package/docs/installation.md +41 -6
  54. package/docs/program-data-plane.md +6 -0
  55. package/docs/publishing.md +5 -3
  56. package/docs/runtime-reference.md +79 -25
  57. package/docs/semantic-validation.md +29 -0
  58. package/docs/stateful-binding-updates-proposal.md +755 -0
  59. package/docs/user-bindings.en.md +64 -0
  60. package/docs/user-bindings.md +64 -0
  61. package/docs/value-wire.md +3 -3
  62. package/docs/verification.md +57 -0
  63. package/index.js +374 -92
  64. package/internal/ast-traversal.js +5 -2
  65. package/internal/binding-descriptors.js +1 -0
  66. package/internal/binding-pattern-traversal.js +25 -0
  67. package/internal/binding-pattern.js +26 -29
  68. package/internal/binding-source-analysis.js +391 -0
  69. package/internal/binding-update-policy.js +66 -0
  70. package/internal/bounded-worker-round.js +56 -0
  71. package/internal/callable-definition-environment.js +42 -0
  72. package/internal/callable-source-catalog.js +129 -0
  73. package/internal/callable-source-encoding.js +47 -0
  74. package/internal/callable-source-facts.js +459 -0
  75. package/internal/callable-source-format.js +2 -0
  76. package/internal/callable-source-lowering.js +123 -0
  77. package/internal/cell-analysis-contract.js +25 -0
  78. package/internal/cell-analysis.js +252 -242
  79. package/internal/cell-completion.js +15 -0
  80. package/internal/cell-error.js +7 -0
  81. package/internal/cell-parser.js +66 -0
  82. package/internal/cell-rewriter.js +3 -553
  83. package/internal/commonjs-export-evidence.js +63 -0
  84. package/internal/commonjs-source-evidence.js +72 -0
  85. package/internal/compiler-amaro.cjs +2 -0
  86. package/internal/compiler-compression.js +12 -0
  87. package/internal/compiler-data.js +58 -0
  88. package/internal/compiler-descriptors.js +54 -0
  89. package/internal/compiler-dynamic-cache.js +48 -0
  90. package/internal/compiler-entry.js +35 -0
  91. package/internal/compiler-generated-names.js +17 -0
  92. package/internal/compiler-intrinsics.js +84 -0
  93. package/internal/compiler-module-links.js +34 -0
  94. package/internal/compiler-operations.js +61 -0
  95. package/internal/compiler-platform-factory.js +84 -0
  96. package/internal/compiler-platform.js +136 -0
  97. package/internal/compiler-record-roles.js +53 -0
  98. package/internal/compiler-region-output.js +259 -0
  99. package/internal/compiler-resource-lowering.js +103 -0
  100. package/internal/compiler-scope-facts.js +115 -0
  101. package/internal/compiler-service.js +109 -0
  102. package/internal/compiler-source-cache.js +32 -0
  103. package/internal/compiler-source-pieces.js +93 -0
  104. package/internal/compiler-source-regions.js +132 -0
  105. package/internal/compiler-storage-source.js +54 -0
  106. package/internal/compiler-text.js +15 -0
  107. package/internal/compiler-typescript.cjs +2 -0
  108. package/internal/config-spec.js +91 -25
  109. package/internal/cordis-tools-owner.js +130 -21
  110. package/internal/diagnostic.js +27 -1
  111. package/internal/direct-surface-owner.js +76 -23
  112. package/internal/dynamic-binding-evidence.js +16 -0
  113. package/internal/dynamic-environment-compiler.js +779 -0
  114. package/internal/dynamic-environment-integration.js +87 -0
  115. package/internal/dynamic-environment-runtime.js +362 -0
  116. package/internal/dynamic-native-calls.js +150 -0
  117. package/internal/dynamic-native-runtime.js +248 -0
  118. package/internal/dynamic-scope-analysis.js +90 -0
  119. package/internal/edit-transport-owner.js +28 -3
  120. package/internal/execution-seam-compat.js +193 -0
  121. package/internal/failure-reporting.js +118 -18
  122. package/internal/host-rpc.js +66 -0
  123. package/internal/isolated-worker.js +548 -0
  124. package/internal/kernel-child.js +139 -0
  125. package/internal/kernel-worker.js +911 -116
  126. package/internal/language-semantics.js +14 -0
  127. package/internal/legacy-binding-catalog.js +48 -0
  128. package/internal/legacy-cell-analysis.js +600 -0
  129. package/internal/legacy-cell-parser.js +50 -0
  130. package/internal/legacy-cell-rewriter.js +631 -0
  131. package/internal/legacy-repl-convenience.js +291 -0
  132. package/internal/managed-module-operations.js +170 -0
  133. package/internal/module-compilation.js +321 -0
  134. package/internal/module-runtime-platform.js +19 -0
  135. package/internal/module-transform-contract.js +12 -0
  136. package/internal/native-await-declarations.js +68 -0
  137. package/internal/native-root-compilation.js +54 -0
  138. package/internal/native-root-dynamic.js +67 -0
  139. package/internal/recovery-tips.js +27 -15
  140. package/internal/rejected-cell-editor.js +13 -8
  141. package/internal/repl-convenience.js +2 -201
  142. package/internal/repl-memory-projection.js +81 -12
  143. package/internal/repl-observation-interest.js +70 -0
  144. package/internal/repl-rewrite-contract.js +21 -0
  145. package/internal/repl-scope-normalizer.js +2091 -0
  146. package/internal/repl-value-observer.js +114 -0
  147. package/internal/rpc-contract.js +52 -0
  148. package/internal/runtime-bridge-owner.js +172 -28
  149. package/internal/runtime-config.js +48 -51
  150. package/internal/runtime-contexts.js +82 -10
  151. package/internal/runtime-messages.js +111 -0
  152. package/internal/session-cell-executor.js +455 -91
  153. package/internal/session-journal-recovery.js +684 -0
  154. package/internal/session-journal-schema.js +106 -0
  155. package/internal/session-journal.js +237 -440
  156. package/internal/session-log-view.js +42 -9
  157. package/internal/session-runtime.js +325 -80
  158. package/internal/session-state.js +249 -40
  159. package/internal/source-position-map.js +211 -71
  160. package/internal/stateful-module-compiler.js +154 -0
  161. package/internal/stateful-module-runtime.js +303 -0
  162. package/internal/stateful-root-compiler.js +598 -0
  163. package/internal/stateful-root-runtime.js +340 -0
  164. package/internal/typert-codec-compat.js +28 -0
  165. package/internal/typescript-transform.js +29 -0
  166. package/internal/user-binding-authoring.js +70 -0
  167. package/internal/user-binding-console-worker.js +184 -0
  168. package/internal/user-binding-console.js +148 -0
  169. package/internal/user-binding-draft-projection.js +335 -0
  170. package/internal/user-binding-model-context.js +31 -0
  171. package/internal/user-binding-runner.js +56 -0
  172. package/internal/user-bindings-owner.js +1065 -0
  173. package/internal/user-bindings-store.js +293 -0
  174. package/internal/user-bindings.js +353 -0
  175. package/internal/value-wire-schema.js +28 -0
  176. package/internal/value-wire.js +29 -20
  177. package/internal/worker-client.js +57 -67
  178. package/internal/worker-environment.js +45 -0
  179. package/internal/worker-output-relay.js +29 -0
  180. package/internal/worker-shutdown.js +14 -0
  181. package/package.json +81 -11
  182. package/src/client-activity.js +109 -45
  183. package/src/client-authoring-view.js +580 -0
  184. package/src/client-binding-review.js +230 -0
  185. package/src/client-bindings-data.js +29 -0
  186. package/src/client-catalog.js +127 -0
  187. package/src/client-code-editor.js +84 -0
  188. package/src/client-console.js +112 -0
  189. package/src/client-copy.js +370 -0
  190. package/src/client-feature-gates.js +62 -0
  191. package/src/client-host-compat.js +150 -0
  192. package/src/client-indicator-view.js +267 -0
  193. package/src/client-repl-view.js +292 -0
  194. package/src/client-rpc.js +20 -0
  195. package/src/client-settings-view.js +221 -0
  196. package/src/client-styles.js +106 -0
  197. package/src/client-tool-view.js +108 -0
  198. package/src/client-transport.js +159 -0
  199. package/src/client-workbench.js +521 -0
  200. package/src/client.js +233 -615
@@ -0,0 +1,293 @@
1
+ import { mkdir, readFile } from 'node:fs/promises'
2
+ import { basename, dirname, extname, join, resolve } from 'node:path'
3
+ import { withFileLock, writeFileAtomic } from '@deepseek-ai/dsh-atomic-write'
4
+ import { resolveDshHome } from '@deepseek-ai/dsh-home-paths'
5
+ import {
6
+ createUserBindingsSnapshot,
7
+ normalizeUserBindingEntry,
8
+ normalizeUserBindingsDocument,
9
+ storedUserBindingsDocument,
10
+ USER_BINDING_ID_MAX_LENGTH,
11
+ } from './user-bindings.js'
12
+
13
+ const EMPTY_DOCUMENT = Object.freeze({ entries: Object.freeze([]) })
14
+
15
+ function isENOENT(error) {
16
+ return error?.code === 'ENOENT'
17
+ }
18
+
19
+ function messageOf(error) {
20
+ return error instanceof Error ? error.message : String(error)
21
+ }
22
+
23
+ function observedDocumentError(error, text) {
24
+ Object.defineProperty(error, 'observedText', { value: text })
25
+ return error
26
+ }
27
+
28
+ function documentText(document) {
29
+ return `${JSON.stringify(storedUserBindingsDocument(document), null, 2)}\n`
30
+ }
31
+
32
+ function parseDocument(text, filename) {
33
+ let value
34
+ try {
35
+ value = JSON.parse(text)
36
+ } catch (error) {
37
+ throw observedDocumentError(
38
+ new SyntaxError(`invalid bindings document at ${filename}: ${messageOf(error)}`),
39
+ text,
40
+ )
41
+ }
42
+ try {
43
+ const document = normalizeUserBindingsDocument(value)
44
+ createUserBindingsSnapshot(value)
45
+ return document
46
+ } catch (error) {
47
+ throw observedDocumentError(
48
+ new TypeError(`invalid bindings document at ${filename}: ${messageOf(error)}`),
49
+ text,
50
+ )
51
+ }
52
+ }
53
+
54
+ async function readDocument(filename) {
55
+ try {
56
+ const text = await readFile(filename, 'utf8')
57
+ return { document: parseDocument(text, filename), text }
58
+ } catch (error) {
59
+ if (isENOENT(error)) return { document: EMPTY_DOCUMENT, text: undefined }
60
+ throw error
61
+ }
62
+ }
63
+
64
+ function entryView(entry, includeSource = false) {
65
+ return Object.freeze({
66
+ id: entry.id,
67
+ name: entry.name,
68
+ scope: entry.scope,
69
+ symbols: Object.freeze([...entry.symbols]),
70
+ purpose: entry.purpose,
71
+ origin: 'global',
72
+ enabled: entry.enabled,
73
+ declaration: entry.declaration,
74
+ ...(entry.modelContext === undefined ? {} : { modelContext: entry.modelContext }),
75
+ ...(includeSource ? { source: entry.source } : {}),
76
+ })
77
+ }
78
+
79
+ function importedId(path, entries) {
80
+ const rawStem = basename(path, extname(path)).replace(/[^A-Za-z0-9._-]+/g, '-').replace(/^[^A-Za-z0-9]+/, '') || 'binding'
81
+ const ids = new Set(entries.map(entry => entry.id))
82
+ const stem = rawStem.slice(0, USER_BINDING_ID_MAX_LENGTH)
83
+ if (!ids.has(stem)) return stem
84
+ for (let suffix = 2; ; suffix += 1) {
85
+ const ending = `-${suffix}`
86
+ const candidate = `${rawStem.slice(0, USER_BINDING_ID_MAX_LENGTH - ending.length)}${ending}`
87
+ if (!ids.has(candidate)) return candidate
88
+ }
89
+ }
90
+
91
+ function importedName(path) {
92
+ const stem = basename(path, extname(path)).replace(/[^A-Za-z0-9_$]+/g, '_').replace(/^[^A-Za-z_$]+/, '')
93
+ return stem || 'bindings'
94
+ }
95
+
96
+ async function refreshStore(store) {
97
+ try {
98
+ const loaded = await readDocument(store.filename)
99
+ store.document = loaded.document
100
+ store.text = loaded.text
101
+ store.error = undefined
102
+ } catch (error) {
103
+ store.document = EMPTY_DOCUMENT
104
+ store.text = typeof error?.observedText === 'string' ? error.observedText : undefined
105
+ store.error = messageOf(error)
106
+ }
107
+ store.loaded = true
108
+ store.revision += 1
109
+ return store.describe()
110
+ }
111
+
112
+ /** Owns the single user-global binding document and its process-local revision. */
113
+ export class UserBindingsStore {
114
+ constructor(options = {}) {
115
+ this.filename = resolve(options.filename ?? join(resolveDshHome(options.dshHome), 'ptc-plus', 'bindings.json'))
116
+ this.document = EMPTY_DOCUMENT
117
+ this.text = undefined
118
+ this.revision = 0
119
+ this.loaded = false
120
+ this.error = undefined
121
+ this.tail = Promise.resolve()
122
+ }
123
+
124
+ enqueue(operation) {
125
+ const task = this.tail.then(operation, operation)
126
+ this.tail = task.then(() => undefined, () => undefined)
127
+ return task
128
+ }
129
+
130
+ async load() {
131
+ if (this.loaded) return
132
+ await this.enqueue(async () => {
133
+ if (!this.loaded) await refreshStore(this)
134
+ })
135
+ }
136
+
137
+ reload() {
138
+ return this.enqueue(() => refreshStore(this))
139
+ }
140
+
141
+ describe() {
142
+ return Object.freeze({
143
+ revision: this.revision,
144
+ path: this.filename,
145
+ entries: Object.freeze(this.document.entries.map(entry => entryView(entry))),
146
+ ...(this.error === undefined ? {} : { error: this.error }),
147
+ })
148
+ }
149
+
150
+ async list() {
151
+ await this.load()
152
+ return this.describe()
153
+ }
154
+
155
+ async entry(id) {
156
+ await this.load()
157
+ const entry = this.document.entries.find(candidate => candidate.id === id)
158
+ if (entry === undefined) throw new Error(`binding entry ${JSON.stringify(id)} does not exist`)
159
+ return Object.freeze({ revision: this.revision, path: this.filename, entry: entryView(entry, true) })
160
+ }
161
+
162
+ async snapshot() {
163
+ await this.load()
164
+ return createUserBindingsSnapshot(storedUserBindingsDocument(this.document), this.revision)
165
+ }
166
+
167
+ async validationDocument() {
168
+ await this.load()
169
+ if (this.error !== undefined) throw new Error(this.error)
170
+ const document = storedUserBindingsDocument(this.document)
171
+ return Object.freeze({
172
+ revision: this.revision,
173
+ entries: Object.freeze(document.entries.map(entry => Object.freeze(entry))),
174
+ })
175
+ }
176
+
177
+ async mutate(expectedRevision, transform) {
178
+ return this.enqueue(async () => {
179
+ if (!this.loaded) {
180
+ const loaded = await readDocument(this.filename)
181
+ this.document = loaded.document
182
+ this.text = loaded.text
183
+ this.loaded = true
184
+ this.revision += 1
185
+ }
186
+ if (!Number.isSafeInteger(expectedRevision) || expectedRevision < 0) {
187
+ throw new TypeError('expectedRevision must be a non-negative safe integer')
188
+ }
189
+ if (expectedRevision !== this.revision) {
190
+ throw Object.assign(new Error(`bindings document moved from revision ${expectedRevision} to ${this.revision}`), {
191
+ code: 'BINDINGS_CONFLICT',
192
+ })
193
+ }
194
+ await mkdir(dirname(this.filename), { recursive: true, mode: 0o700 })
195
+ return withFileLock(this.filename, async () => {
196
+ const disk = await readDocument(this.filename)
197
+ if (disk.text !== this.text) {
198
+ this.document = disk.document
199
+ this.text = disk.text
200
+ this.error = undefined
201
+ this.revision += 1
202
+ throw Object.assign(new Error('bindings document changed outside this process; reload and retry'), {
203
+ code: 'BINDINGS_CONFLICT',
204
+ })
205
+ }
206
+ const next = normalizeUserBindingsDocument(storedUserBindingsDocument(
207
+ await transform(this.document),
208
+ ))
209
+ createUserBindingsSnapshot(storedUserBindingsDocument(next), this.revision + 1)
210
+ const text = documentText(next)
211
+ await writeFileAtomic(this.filename, text, { mode: 0o600, dirMode: 0o700 })
212
+ this.document = next
213
+ this.text = text
214
+ this.error = undefined
215
+ this.revision += 1
216
+ return this.describe()
217
+ })
218
+ })
219
+ }
220
+
221
+ save(value, expectedRevision) {
222
+ const entry = normalizeUserBindingEntry(value)
223
+ return this.mutate(expectedRevision, (document) => {
224
+ const index = document.entries.findIndex(candidate => candidate.id === entry.id)
225
+ const entries = [...document.entries]
226
+ if (index < 0) entries.push(entry)
227
+ else entries[index] = entry
228
+ return { entries }
229
+ })
230
+ }
231
+
232
+ create(value, expectedRevision) {
233
+ const entry = normalizeUserBindingEntry(value)
234
+ return this.mutate(expectedRevision, (document) => {
235
+ if (document.entries.some(candidate => candidate.id === entry.id)) {
236
+ throw new Error(`binding entry ${JSON.stringify(entry.id)} already exists`)
237
+ }
238
+ return { entries: [...document.entries, entry] }
239
+ })
240
+ }
241
+
242
+ /** Replace one existing entry; a missing id is a conflict, never a creation. */
243
+ update(value, expectedRevision) {
244
+ const entry = normalizeUserBindingEntry(value)
245
+ return this.mutate(expectedRevision, (document) => {
246
+ const index = document.entries.findIndex(candidate => candidate.id === entry.id)
247
+ if (index < 0) throw new Error(`binding entry ${JSON.stringify(entry.id)} does not exist`)
248
+ const entries = [...document.entries]
249
+ entries[index] = entry
250
+ return { entries }
251
+ })
252
+ }
253
+
254
+ setEnabled(id, enabled, expectedRevision) {
255
+ if (typeof enabled !== 'boolean') throw new TypeError('enabled must be a boolean')
256
+ return this.mutate(expectedRevision, (document) => {
257
+ const index = document.entries.findIndex(candidate => candidate.id === id)
258
+ if (index < 0) throw new Error(`binding entry ${JSON.stringify(id)} does not exist`)
259
+ const entries = [...document.entries]
260
+ entries[index] = { ...entries[index], enabled }
261
+ return { entries }
262
+ })
263
+ }
264
+
265
+ remove(id, expectedRevision) {
266
+ return this.mutate(expectedRevision, (document) => {
267
+ const entries = document.entries.filter(candidate => candidate.id !== id)
268
+ if (entries.length === document.entries.length) {
269
+ throw new Error(`binding entry ${JSON.stringify(id)} does not exist`)
270
+ }
271
+ return { entries }
272
+ })
273
+ }
274
+
275
+ async importFile(path, expectedRevision, options = {}) {
276
+ if (typeof path !== 'string' || extname(path).toLowerCase() !== '.ts') {
277
+ throw new TypeError('binding import path must name a local .ts file')
278
+ }
279
+ const absolute = resolve(path)
280
+ const source = await readFile(absolute, 'utf8')
281
+ await this.load()
282
+ const entry = {
283
+ id: options.id ?? importedId(absolute, this.document.entries),
284
+ name: options.name ?? importedName(absolute),
285
+ scope: options.scope ?? 'namespace',
286
+ symbols: options.symbols,
287
+ purpose: options.purpose ?? '',
288
+ enabled: options.enabled ?? false,
289
+ source,
290
+ }
291
+ return this.save(entry, expectedRevision)
292
+ }
293
+ }
@@ -0,0 +1,353 @@
1
+ import { createHash } from 'node:crypto'
2
+ import { identifier, exportedSymbols, sourceDurability } from './compiler-service.js'
3
+ import { assertOwnFields, isRecord } from './record-utils.js'
4
+ import { bindingModelPreferences, normalizeBindingModelContext } from './user-binding-model-context.js'
5
+ import { supportedUserBindingTransform, USER_BINDING_TRANSFORM } from './module-transform-contract.js'
6
+
7
+ export const USER_BINDINGS_META_KEY = 'dshPtcPlusUserBindings'
8
+ export const USER_BINDINGS_SNAPSHOT_VERSION = 2
9
+
10
+ const MAX_ENTRIES = 64
11
+ export const USER_BINDING_ID_MAX_LENGTH = 64
12
+ const MAX_NAME_LENGTH = 128
13
+ const MAX_PURPOSE_LENGTH = 240
14
+ const MAX_SOURCE_LENGTH = 64 * 1024
15
+ const MAX_SOURCE_TOTAL_LENGTH = 256 * 1024
16
+ const MAX_DECLARATION_TOTAL_LENGTH = 16 * 1024
17
+ const DOCUMENT_FIELDS = new Set(['entries'])
18
+ const ENTRY_FIELDS = new Set(['id', 'name', 'scope', 'symbols', 'purpose', 'enabled', 'source', 'modelContext'])
19
+ const LEGACY_SNAPSHOT_FIELDS = new Set(['version', 'revision', 'fingerprint', 'entries'])
20
+ const SNAPSHOT_FIELDS = new Set([...LEGACY_SNAPSHOT_FIELDS, 'transform'])
21
+ const SNAPSHOT_ENTRY_FIELDS = new Set([...ENTRY_FIELDS, 'fingerprint', 'declaration', 'bindings', 'durability', 'volatileReason'])
22
+ const BINDING_FIELDS = new Set(['name', 'kind', 'declaration'])
23
+ const SCOPES = new Set(['namespace', 'top-level'])
24
+ const KINDS = new Set(['variable', 'function', 'class'])
25
+
26
+ function displayName(value) {
27
+ if (typeof value !== 'string') throw new TypeError('binding name must be a string')
28
+ const name = value.replace(/\s+/g, ' ').trim()
29
+ if (name.length === 0 || name.length > MAX_NAME_LENGTH) {
30
+ throw new TypeError(`binding name must contain 1-${MAX_NAME_LENGTH} characters`)
31
+ }
32
+ return name
33
+ }
34
+
35
+
36
+ function normalizePurpose(value) {
37
+ if (typeof value !== 'string') throw new TypeError('binding purpose must be a string')
38
+ const purpose = value.replace(/\s+/g, ' ').trim()
39
+ if (purpose.length > MAX_PURPOSE_LENGTH) {
40
+ throw new TypeError(`binding purpose must not exceed ${MAX_PURPOSE_LENGTH} characters`)
41
+ }
42
+ return purpose
43
+ }
44
+
45
+ function entryFingerprint(value) {
46
+ return createHash('sha256').update(JSON.stringify(value)).digest('hex')
47
+ }
48
+
49
+ function selectedDescriptors(source, symbols, transform) {
50
+ const exported = exportedSymbols(source, transform)
51
+ const selected = symbols === undefined || symbols.length === 0 ? [...exported.keys()] : symbols
52
+ const unique = new Set()
53
+ return selected.map((name) => {
54
+ identifier(name, 'binding symbol')
55
+ if (unique.has(name)) throw new TypeError(`binding symbol ${JSON.stringify(name)} is duplicated`)
56
+ unique.add(name)
57
+ const descriptor = exported.get(name)
58
+ if (descriptor === undefined) throw new TypeError(`binding symbol ${JSON.stringify(name)} is not a named value export`)
59
+ return descriptor
60
+ })
61
+ }
62
+
63
+
64
+ function declarationFor(entry, descriptors) {
65
+ const comment = entry.purpose === '' ? '' : `/** ${entry.purpose.replaceAll('*/', '* /')} */\n`
66
+ if (entry.scope === 'namespace') {
67
+ return `${comment}declare const ${entry.name}: {\n${descriptors.map(item => ` ${item.member};`).join('\n')}\n}`
68
+ }
69
+ return descriptors.map((item, index) => `${index === 0 ? comment : ''}declare ${item.declaration}`).join('\n')
70
+ }
71
+
72
+ export function normalizeUserBindingEntry(value, { transform = USER_BINDING_TRANSFORM } = {}) {
73
+ if (!isRecord(value)) throw new TypeError('binding entry must be an object')
74
+ const permitted = new Set([...ENTRY_FIELDS].filter(field => field !== 'symbols'))
75
+ if (Object.hasOwn(value, 'symbols')) permitted.add('symbols')
76
+ assertOwnFields(value, permitted, 'binding entry')
77
+ if (typeof value.id !== 'string' || !/^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/.test(value.id)) {
78
+ throw new TypeError(`binding id must match [A-Za-z0-9][A-Za-z0-9._-]{0,${USER_BINDING_ID_MAX_LENGTH - 1}}`)
79
+ }
80
+ const scope = value.scope ?? 'namespace'
81
+ if (!SCOPES.has(scope)) throw new TypeError('binding scope must be namespace or top-level')
82
+ const name = scope === 'namespace'
83
+ ? identifier(value.name, 'binding name')
84
+ : displayName(value.name)
85
+ if (typeof value.source !== 'string' || value.source.length === 0 || value.source.length > MAX_SOURCE_LENGTH) {
86
+ throw new TypeError(`binding source must contain 1-${MAX_SOURCE_LENGTH} characters`)
87
+ }
88
+ if (typeof value.enabled !== 'boolean') throw new TypeError('binding enabled must be a boolean')
89
+ const purpose = normalizePurpose(value.purpose ?? '')
90
+ if (value.symbols !== undefined && !Array.isArray(value.symbols)) {
91
+ throw new TypeError('binding symbols must be an array')
92
+ }
93
+ const descriptors = selectedDescriptors(value.source, value.symbols, transform)
94
+ if (scope === 'namespace') identifier(name, 'binding name', true)
95
+ else descriptors.forEach(item => identifier(item.name, 'binding symbol', true))
96
+ const durability = sourceDurability(value.source, transform)
97
+ const symbols = descriptors.map(item => item.name)
98
+ const modelContext = normalizeBindingModelContext(value.modelContext)
99
+ const stored = Object.freeze({
100
+ id: value.id,
101
+ name,
102
+ scope,
103
+ symbols: Object.freeze(symbols),
104
+ purpose,
105
+ enabled: value.enabled,
106
+ source: value.source,
107
+ ...(modelContext === undefined ? {} : { modelContext }),
108
+ })
109
+ const effectivePurpose = purpose || normalizePurpose(
110
+ descriptors.map(item => item.purpose).find(candidate => candidate !== '') ?? '',
111
+ )
112
+ const declarationEntry = effectivePurpose === purpose
113
+ ? stored
114
+ : { ...stored, purpose: effectivePurpose }
115
+ const bindings = scope === 'namespace'
116
+ ? [Object.freeze({ name, kind: 'variable', declaration: declarationFor(declarationEntry, descriptors) })]
117
+ : descriptors.map(item => Object.freeze({
118
+ name: item.name,
119
+ kind: item.kind,
120
+ declaration: declarationFor({
121
+ ...stored,
122
+ purpose: item === descriptors[0] ? effectivePurpose : '',
123
+ }, [item]),
124
+ }))
125
+ return Object.freeze({
126
+ ...stored,
127
+ fingerprint: entryFingerprint(stored),
128
+ declaration: declarationFor(declarationEntry, descriptors),
129
+ bindings: Object.freeze(bindings),
130
+ ...durability,
131
+ })
132
+ }
133
+
134
+ function validateConflicts(entries) {
135
+ const ids = new Set()
136
+ const activeNames = new Map()
137
+ for (const entry of entries) {
138
+ if (ids.has(entry.id)) throw new TypeError(`binding id ${JSON.stringify(entry.id)} is duplicated`)
139
+ ids.add(entry.id)
140
+ if (!entry.enabled) continue
141
+ const names = entry.scope === 'namespace' ? [entry.name] : entry.symbols
142
+ for (const name of names) {
143
+ const existing = activeNames.get(name)
144
+ if (existing !== undefined) {
145
+ throw new TypeError(`active binding ${JSON.stringify(name)} conflicts between entries ${JSON.stringify(existing)} and ${JSON.stringify(entry.id)}`)
146
+ }
147
+ activeNames.set(name, entry.id)
148
+ }
149
+ }
150
+ }
151
+
152
+ function validateAggregateBudgets(entries, includeDeclarations) {
153
+ const sourceLength = entries.reduce((total, entry) => total + entry.source.length, 0)
154
+ if (sourceLength > MAX_SOURCE_TOTAL_LENGTH) {
155
+ throw new TypeError(`binding sources exceed the ${MAX_SOURCE_TOTAL_LENGTH} character document limit`)
156
+ }
157
+ if (!includeDeclarations) return
158
+ const declarationLength = entries.reduce((total, entry) => total + entry.declaration.length, 0)
159
+ + Math.max(0, entries.length - 1) * 2
160
+ if (declarationLength > MAX_DECLARATION_TOTAL_LENGTH) {
161
+ throw new TypeError(`binding declarations exceed the ${MAX_DECLARATION_TOTAL_LENGTH} character model-context limit`)
162
+ }
163
+ const modelLength = entries.reduce((total, entry) => {
164
+ const { includeDeclaration, instructions } = bindingModelPreferences(entry.modelContext)
165
+ return total + (includeDeclaration ? entry.declaration.length : 0) + instructions.length
166
+ }, 0)
167
+ if (modelLength > MAX_DECLARATION_TOTAL_LENGTH) {
168
+ throw new TypeError(`binding model context exceeds the ${MAX_DECLARATION_TOTAL_LENGTH} character model-context limit`)
169
+ }
170
+ }
171
+
172
+ export function normalizeUserBindingsDocument(value) {
173
+ if (!isRecord(value)) throw new TypeError('bindings document must be an object')
174
+ assertOwnFields(value, DOCUMENT_FIELDS, 'bindings document')
175
+ if (!Array.isArray(value.entries) || value.entries.length > MAX_ENTRIES) {
176
+ throw new TypeError(`bindings document must contain at most ${MAX_ENTRIES} entries`)
177
+ }
178
+ const entries = value.entries.map(normalizeUserBindingEntry)
179
+ validateAggregateBudgets(entries, false)
180
+ validateConflicts(entries)
181
+ return Object.freeze({ entries: Object.freeze(entries) })
182
+ }
183
+
184
+ function snapshotFingerprint(revision, entries, transform) {
185
+ return entryFingerprint({
186
+ revision,
187
+ entries: entries.map(entry => entry.fingerprint),
188
+ ...(transform === undefined ? {} : { transform }),
189
+ })
190
+ }
191
+
192
+ export function createUserBindingsSnapshot(document, revision = 0) {
193
+ if (!Number.isSafeInteger(revision) || revision < 0) throw new TypeError('binding revision must be a non-negative safe integer')
194
+ const normalized = normalizeUserBindingsDocument(document)
195
+ const entries = normalized.entries.filter(entry => entry.enabled)
196
+ validateAggregateBudgets(entries, true)
197
+ return Object.freeze({
198
+ version: USER_BINDINGS_SNAPSHOT_VERSION,
199
+ transform: USER_BINDING_TRANSFORM,
200
+ revision,
201
+ fingerprint: snapshotFingerprint(revision, entries, USER_BINDING_TRANSFORM),
202
+ entries: Object.freeze(entries),
203
+ })
204
+ }
205
+
206
+ export function normalizeUserBindingsSnapshot(value) {
207
+ if (!isRecord(value)) throw new TypeError('user binding snapshot must be an object')
208
+ const legacy = value.version === 1
209
+ assertOwnFields(value, legacy ? LEGACY_SNAPSHOT_FIELDS : SNAPSHOT_FIELDS, 'user binding snapshot')
210
+ if ((!legacy && value.version !== USER_BINDINGS_SNAPSHOT_VERSION)
211
+ || !Number.isSafeInteger(value.revision) || value.revision < 0
212
+ || typeof value.fingerprint !== 'string' || !/^[a-f0-9]{64}$/.test(value.fingerprint)
213
+ || !Array.isArray(value.entries) || value.entries.length > MAX_ENTRIES) {
214
+ throw new TypeError('invalid user binding snapshot')
215
+ }
216
+ // Unrecorded or unsupported lowering cannot prove historical module values.
217
+ if (legacy ? value.entries.length > 0 : !supportedUserBindingTransform(value.transform)) {
218
+ throw new TypeError('user binding snapshot cannot prove its historical TypeScript transform')
219
+ }
220
+ const entries = value.entries.map((entry) => {
221
+ if (!isRecord(entry)) throw new TypeError('invalid user binding snapshot entry')
222
+ assertOwnFields(entry, SNAPSHOT_ENTRY_FIELDS, 'user binding snapshot entry')
223
+ const normalized = normalizeUserBindingEntry(Object.fromEntries(
224
+ [...ENTRY_FIELDS].filter(field => Object.hasOwn(entry, field)).map(field => [field, entry[field]]),
225
+ ), { transform: value.transform })
226
+ if (entry.fingerprint !== normalized.fingerprint || entry.declaration !== normalized.declaration
227
+ || entry.durability !== normalized.durability
228
+ || entry.volatileReason !== normalized.volatileReason
229
+ || !Array.isArray(entry.bindings) || entry.bindings.length !== normalized.bindings.length) {
230
+ throw new TypeError('user binding snapshot entry does not match its source')
231
+ }
232
+ entry.bindings.forEach((binding, index) => {
233
+ assertOwnFields(binding, BINDING_FIELDS, 'user binding snapshot binding')
234
+ const expected = normalized.bindings[index]
235
+ if (!KINDS.has(binding.kind) || binding.name !== expected.name
236
+ || binding.kind !== expected.kind || binding.declaration !== expected.declaration) {
237
+ throw new TypeError('user binding snapshot binding does not match its source')
238
+ }
239
+ })
240
+ if (!normalized.enabled) throw new TypeError('user binding snapshot cannot contain a disabled entry')
241
+ return normalized
242
+ })
243
+ validateAggregateBudgets(entries, true)
244
+ validateConflicts(entries)
245
+ if (value.fingerprint !== snapshotFingerprint(value.revision, entries, value.transform)) {
246
+ throw new TypeError('user binding snapshot fingerprint does not match its entries')
247
+ }
248
+ return Object.freeze({
249
+ version: value.version,
250
+ ...(legacy ? {} : { transform: value.transform }),
251
+ revision: value.revision,
252
+ fingerprint: value.fingerprint,
253
+ entries: Object.freeze(entries),
254
+ })
255
+ }
256
+
257
+ export function userBindingsDeclaration(snapshot) {
258
+ const normalized = normalizeUserBindingsSnapshot(snapshot)
259
+ return normalized.entries.map(entry => entry.declaration).join('\n\n')
260
+ }
261
+
262
+ export function userBindingsConfiguredContext(snapshot) {
263
+ const entries = normalizeUserBindingsSnapshot(snapshot).entries
264
+ .filter(entry => {
265
+ const { includeDeclaration, instructions } = bindingModelPreferences(entry.modelContext)
266
+ return includeDeclaration || instructions !== ''
267
+ })
268
+ .sort((left, right) => left.id < right.id ? -1 : left.id > right.id ? 1 : 0)
269
+ if (entries.length === 0) return undefined
270
+ const content = entries.map(entry => {
271
+ const { includeDeclaration, instructions } = bindingModelPreferences(entry.modelContext)
272
+ return [
273
+ `Binding: ${entry.name}`,
274
+ instructions,
275
+ includeDeclaration ? `\`\`\`ts\n${entry.declaration}\n\`\`\`` : '',
276
+ ].filter(Boolean).join('\n\n')
277
+ }).join('\n\n')
278
+ return {
279
+ name: 'tools:ptc-plus-user-binding-defaults',
280
+ text: content,
281
+ }
282
+ }
283
+
284
+ export function userBindingCatalogEntries(snapshot) {
285
+ const normalized = normalizeUserBindingsSnapshot(snapshot)
286
+ return normalized.entries.flatMap(entry => entry.bindings.map(binding => ({
287
+ name: binding.name,
288
+ kind: binding.kind,
289
+ entryId: entry.id,
290
+ fingerprint: entry.fingerprint,
291
+ definition: Object.freeze({ source: binding.declaration, line: 1, column: 1 }),
292
+ })))
293
+ }
294
+
295
+ export function selectUserBindingsSnapshot(snapshot, entryIds) {
296
+ const normalized = normalizeUserBindingsSnapshot(snapshot)
297
+ const ids = entryIds instanceof Set ? entryIds : new Set(entryIds)
298
+ const entries = Object.freeze(normalized.entries.filter(entry => ids.has(entry.id)))
299
+ return Object.freeze({
300
+ ...normalized,
301
+ fingerprint: snapshotFingerprint(normalized.revision, entries, normalized.transform),
302
+ entries,
303
+ })
304
+ }
305
+
306
+ export function storedUserBindingsDocument(document) {
307
+ const projected = isRecord(document) && Array.isArray(document.entries)
308
+ ? {
309
+ entries: document.entries.map(entry => isRecord(entry)
310
+ ? Object.fromEntries([...ENTRY_FIELDS]
311
+ .filter(field => Object.hasOwn(entry, field))
312
+ .map(field => [field, entry[field]]))
313
+ : entry),
314
+ }
315
+ : document
316
+ const normalized = normalizeUserBindingsDocument(projected)
317
+ return {
318
+ entries: normalized.entries.map(entry => ({
319
+ id: entry.id,
320
+ name: entry.name,
321
+ scope: entry.scope,
322
+ symbols: [...entry.symbols],
323
+ purpose: entry.purpose,
324
+ enabled: entry.enabled,
325
+ source: entry.source,
326
+ ...(entry.modelContext === undefined ? {} : { modelContext: entry.modelContext }),
327
+ })),
328
+ }
329
+ }
330
+
331
+ /** Merge the exact request snapshot into private tool-result metadata. */
332
+ export function withUserBindingsSnapshot(meta, snapshot) {
333
+ const base = isRecord(meta) ? { ...meta } : meta === undefined ? {} : { value: meta }
334
+ base[USER_BINDINGS_META_KEY] = normalizeUserBindingsSnapshot(snapshot)
335
+ return base
336
+ }
337
+
338
+ /** Read one validated snapshot from private result metadata. */
339
+ export function userBindingsSnapshotFromMeta(meta) {
340
+ if (!isRecord(meta) || !Object.hasOwn(meta, USER_BINDINGS_META_KEY)) return undefined
341
+ return normalizeUserBindingsSnapshot(meta[USER_BINDINGS_META_KEY])
342
+ }
343
+
344
+ /** Compare snapshots without trusting caller-owned object identity. */
345
+ export function userBindingsSnapshotsEqual(left, right) {
346
+ if (left === undefined || right === undefined) return left === right
347
+ try {
348
+ return JSON.stringify(normalizeUserBindingsSnapshot(left))
349
+ === JSON.stringify(normalizeUserBindingsSnapshot(right))
350
+ } catch {
351
+ return false
352
+ }
353
+ }
@@ -0,0 +1,28 @@
1
+ export const VALUE_CODEC = 'ptc-value-graph/v1'
2
+
3
+ export const DEFAULT_VALUE_LIMITS = Object.freeze({
4
+ maxNodes: 100_000,
5
+ maxEdges: 1_000_000,
6
+ maxArrayLength: 1_000_000,
7
+ maxBigIntDigits: 100_000,
8
+ maxStringBytes: 64 * 1024 * 1024,
9
+ })
10
+
11
+ export const VALUE_ENVELOPE_FIELDS = new Set(['codec', 'root', 'nodes'])
12
+ export const VALUE_OBJECT_FIELDS = new Set(['type', 'prototype', 'entries'])
13
+ export const VALUE_ARRAY_FIELDS = new Set(['type', 'length', 'entries'])
14
+ export const VALUE_UNDEFINED_FIELDS = new Set(['tag'])
15
+ export const VALUE_NUMBER_FIELDS = new Set(['tag', 'value'])
16
+ export const VALUE_BIGINT_FIELDS = new Set(['tag', 'value'])
17
+ export const VALUE_REFERENCE_FIELDS = new Set(['tag', 'index'])
18
+
19
+ /** Map one resolved runtime configuration to its value codec budgets. */
20
+ export function valueLimitsFromConfig(config) {
21
+ return {
22
+ maxNodes: config.maxValueNodes,
23
+ maxEdges: config.maxValueEdges,
24
+ maxArrayLength: config.maxValueArrayLength,
25
+ maxBigIntDigits: config.maxValueBigIntDigits,
26
+ maxStringBytes: config.maxOutputBytes,
27
+ }
28
+ }