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.
- package/README.en.md +66 -73
- package/README.md +66 -73
- package/assets/ptc-plus-bindings-en.png +0 -0
- package/assets/ptc-plus-bindings-zh.png +0 -0
- package/assets/ptc-plus-repair-en.png +0 -0
- package/assets/ptc-plus-repair-zh.png +0 -0
- package/assets/ptc-plus-repl-workspace-en.png +0 -0
- package/assets/ptc-plus-repl-workspace-zh.png +0 -0
- package/assets/ptc-plus-settings-en.png +0 -0
- package/assets/ptc-plus-settings-zh.png +0 -0
- package/client.js +30610 -722
- package/compiler-amaro.cjs +5 -0
- package/compiler-amaro.cjs.map +6 -0
- package/compiler-asset-124f80c1bba92ff5fe79cfec6e8ad518f476de4a42514cae3919538887f510e8.txt +1 -0
- package/compiler-assets.json +3 -0
- package/compiler-core.cjs +554 -0
- package/compiler-core.cjs.map +6 -0
- package/compiler-platform.cjs +2 -0
- package/compiler-platform.cjs.map +6 -0
- package/compiler-typescript.cjs +388 -0
- package/compiler-typescript.cjs.map +6 -0
- package/docs/README.md +6 -0
- package/docs/adr/0004-declare-program-bindings-at-the-host.md +1 -1
- package/docs/adr/0006-rewrite-module-syntax-with-an-ast.md +6 -4
- package/docs/adr/0007-separate-worker-transport-from-session-semantics.md +15 -3
- package/docs/adr/0009-bound-expensive-acceptance-default.md +2 -0
- package/docs/adr/0010-session-log-derived-recovery-tips.md +59 -5
- package/docs/adr/0011-isolate-the-local-recovery-tip-provider.md +1 -1
- package/docs/adr/0012-preserve-mixed-destructuring-initialization.md +3 -1
- package/docs/adr/0013-isolate-repl-redeclaration-convenience.md +9 -3
- package/docs/adr/0014-persist-cell-rewrite-policy.md +11 -5
- package/docs/adr/0015-preserve-source-positions-through-rewrites.md +1 -1
- package/docs/adr/0016-archive-resolved-review-findings.md +1 -1
- package/docs/adr/0017-track-the-latest-dsh-public-surface.md +49 -0
- package/docs/adr/0018-use-versioned-fixture-for-ab-benchmark.md +3 -3
- package/docs/adr/0019-plugin-settings-and-kill-switch.md +8 -6
- package/docs/adr/0020-optional-cordis-tools-in-ptc-mode.md +2 -2
- package/docs/adr/0021-separate-host-bootstrap-from-session-runtime.md +17 -3
- package/docs/adr/0023-global-user-bindings.md +104 -0
- package/docs/adr/0024-repl-console-observation.md +33 -0
- package/docs/adr/0025-use-versioned-logical-binding-identities.md +119 -0
- package/docs/adr/0026-persist-the-delivery-verdict-against-the-verified-candidate.md +21 -0
- package/docs/adr/0027-run-the-session-kernel-in-a-killable-helper-process.md +37 -0
- package/docs/adr/0028-attach-to-the-host-ptc-execution-seam.md +103 -0
- package/docs/adr/0029-keep-plugin-unavailability-out-of-the-host-agent-lifecycle.md +27 -0
- package/docs/adr/0030-serve-both-typert-codec-generations-from-one-codec.md +27 -0
- package/docs/architecture.md +82 -22
- package/docs/binding-review-placement-design.md +200 -0
- package/docs/capability-projection.md +1 -1
- package/docs/client-ui.md +96 -20
- package/docs/durability-design.md +61 -24
- package/docs/evaluation.md +78 -6
- package/docs/installation.md +41 -6
- package/docs/program-data-plane.md +6 -0
- package/docs/publishing.md +5 -3
- package/docs/runtime-reference.md +79 -25
- package/docs/semantic-validation.md +29 -0
- package/docs/stateful-binding-updates-proposal.md +755 -0
- package/docs/user-bindings.en.md +64 -0
- package/docs/user-bindings.md +64 -0
- package/docs/value-wire.md +3 -3
- package/docs/verification.md +57 -0
- package/index.js +374 -92
- package/internal/ast-traversal.js +5 -2
- package/internal/binding-descriptors.js +1 -0
- package/internal/binding-pattern-traversal.js +25 -0
- package/internal/binding-pattern.js +26 -29
- package/internal/binding-source-analysis.js +391 -0
- package/internal/binding-update-policy.js +66 -0
- package/internal/bounded-worker-round.js +56 -0
- package/internal/callable-definition-environment.js +42 -0
- package/internal/callable-source-catalog.js +129 -0
- package/internal/callable-source-encoding.js +47 -0
- package/internal/callable-source-facts.js +459 -0
- package/internal/callable-source-format.js +2 -0
- package/internal/callable-source-lowering.js +123 -0
- package/internal/cell-analysis-contract.js +25 -0
- package/internal/cell-analysis.js +252 -242
- package/internal/cell-completion.js +15 -0
- package/internal/cell-error.js +7 -0
- package/internal/cell-parser.js +66 -0
- package/internal/cell-rewriter.js +3 -553
- package/internal/commonjs-export-evidence.js +63 -0
- package/internal/commonjs-source-evidence.js +72 -0
- package/internal/compiler-amaro.cjs +2 -0
- package/internal/compiler-compression.js +12 -0
- package/internal/compiler-data.js +58 -0
- package/internal/compiler-descriptors.js +54 -0
- package/internal/compiler-dynamic-cache.js +48 -0
- package/internal/compiler-entry.js +35 -0
- package/internal/compiler-generated-names.js +17 -0
- package/internal/compiler-intrinsics.js +84 -0
- package/internal/compiler-module-links.js +34 -0
- package/internal/compiler-operations.js +61 -0
- package/internal/compiler-platform-factory.js +84 -0
- package/internal/compiler-platform.js +136 -0
- package/internal/compiler-record-roles.js +53 -0
- package/internal/compiler-region-output.js +259 -0
- package/internal/compiler-resource-lowering.js +103 -0
- package/internal/compiler-scope-facts.js +115 -0
- package/internal/compiler-service.js +109 -0
- package/internal/compiler-source-cache.js +32 -0
- package/internal/compiler-source-pieces.js +93 -0
- package/internal/compiler-source-regions.js +132 -0
- package/internal/compiler-storage-source.js +54 -0
- package/internal/compiler-text.js +15 -0
- package/internal/compiler-typescript.cjs +2 -0
- package/internal/config-spec.js +91 -25
- package/internal/cordis-tools-owner.js +130 -21
- package/internal/diagnostic.js +27 -1
- package/internal/direct-surface-owner.js +76 -23
- package/internal/dynamic-binding-evidence.js +16 -0
- package/internal/dynamic-environment-compiler.js +779 -0
- package/internal/dynamic-environment-integration.js +87 -0
- package/internal/dynamic-environment-runtime.js +362 -0
- package/internal/dynamic-native-calls.js +150 -0
- package/internal/dynamic-native-runtime.js +248 -0
- package/internal/dynamic-scope-analysis.js +90 -0
- package/internal/edit-transport-owner.js +28 -3
- package/internal/execution-seam-compat.js +193 -0
- package/internal/failure-reporting.js +118 -18
- package/internal/host-rpc.js +66 -0
- package/internal/isolated-worker.js +548 -0
- package/internal/kernel-child.js +139 -0
- package/internal/kernel-worker.js +911 -116
- package/internal/language-semantics.js +14 -0
- package/internal/legacy-binding-catalog.js +48 -0
- package/internal/legacy-cell-analysis.js +600 -0
- package/internal/legacy-cell-parser.js +50 -0
- package/internal/legacy-cell-rewriter.js +631 -0
- package/internal/legacy-repl-convenience.js +291 -0
- package/internal/managed-module-operations.js +170 -0
- package/internal/module-compilation.js +321 -0
- package/internal/module-runtime-platform.js +19 -0
- package/internal/module-transform-contract.js +12 -0
- package/internal/native-await-declarations.js +68 -0
- package/internal/native-root-compilation.js +54 -0
- package/internal/native-root-dynamic.js +67 -0
- package/internal/recovery-tips.js +27 -15
- package/internal/rejected-cell-editor.js +13 -8
- package/internal/repl-convenience.js +2 -201
- package/internal/repl-memory-projection.js +81 -12
- package/internal/repl-observation-interest.js +70 -0
- package/internal/repl-rewrite-contract.js +21 -0
- package/internal/repl-scope-normalizer.js +2091 -0
- package/internal/repl-value-observer.js +114 -0
- package/internal/rpc-contract.js +52 -0
- package/internal/runtime-bridge-owner.js +172 -28
- package/internal/runtime-config.js +48 -51
- package/internal/runtime-contexts.js +82 -10
- package/internal/runtime-messages.js +111 -0
- package/internal/session-cell-executor.js +455 -91
- package/internal/session-journal-recovery.js +684 -0
- package/internal/session-journal-schema.js +106 -0
- package/internal/session-journal.js +237 -440
- package/internal/session-log-view.js +42 -9
- package/internal/session-runtime.js +325 -80
- package/internal/session-state.js +249 -40
- package/internal/source-position-map.js +211 -71
- package/internal/stateful-module-compiler.js +154 -0
- package/internal/stateful-module-runtime.js +303 -0
- package/internal/stateful-root-compiler.js +598 -0
- package/internal/stateful-root-runtime.js +340 -0
- package/internal/typert-codec-compat.js +28 -0
- package/internal/typescript-transform.js +29 -0
- package/internal/user-binding-authoring.js +70 -0
- package/internal/user-binding-console-worker.js +184 -0
- package/internal/user-binding-console.js +148 -0
- package/internal/user-binding-draft-projection.js +335 -0
- package/internal/user-binding-model-context.js +31 -0
- package/internal/user-binding-runner.js +56 -0
- package/internal/user-bindings-owner.js +1065 -0
- package/internal/user-bindings-store.js +293 -0
- package/internal/user-bindings.js +353 -0
- package/internal/value-wire-schema.js +28 -0
- package/internal/value-wire.js +29 -20
- package/internal/worker-client.js +57 -67
- package/internal/worker-environment.js +45 -0
- package/internal/worker-output-relay.js +29 -0
- package/internal/worker-shutdown.js +14 -0
- package/package.json +81 -11
- package/src/client-activity.js +109 -45
- package/src/client-authoring-view.js +580 -0
- package/src/client-binding-review.js +230 -0
- package/src/client-bindings-data.js +29 -0
- package/src/client-catalog.js +127 -0
- package/src/client-code-editor.js +84 -0
- package/src/client-console.js +112 -0
- package/src/client-copy.js +370 -0
- package/src/client-feature-gates.js +62 -0
- package/src/client-host-compat.js +150 -0
- package/src/client-indicator-view.js +267 -0
- package/src/client-repl-view.js +292 -0
- package/src/client-rpc.js +20 -0
- package/src/client-settings-view.js +221 -0
- package/src/client-styles.js +106 -0
- package/src/client-tool-view.js +108 -0
- package/src/client-transport.js +159 -0
- package/src/client-workbench.js +521 -0
- 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
|
+
}
|