dsh-ptc-plus 0.2.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/LICENSE +21 -0
- package/README.md +137 -0
- package/README.zh.md +137 -0
- package/assets/dsh-ptc-plus-banner.webp +0 -0
- package/assets/ptc-plus-repair.png +0 -0
- package/assets/ptc-plus-settings.png +0 -0
- package/client.js +366 -0
- package/cordis.patch.yml +5 -0
- package/docs/README.md +21 -0
- package/docs/adr/0001-delegate-governance-to-dsh.md +3 -0
- package/docs/adr/0002-limit-work-map-scope.md +3 -0
- package/docs/adr/0003-prefer-native-program-surfaces.md +3 -0
- package/docs/adr/0004-declare-program-bindings-at-the-host.md +5 -0
- package/docs/adr/0005-temporary-rejected-cell-edit-transport.md +129 -0
- package/docs/adr/0006-rewrite-module-syntax-with-an-ast.md +45 -0
- package/docs/adr/0007-separate-worker-transport-from-session-semantics.md +39 -0
- package/docs/adr/0008-define-agent-operating-contract.md +25 -0
- package/docs/adr/0009-bound-expensive-acceptance-default.md +39 -0
- package/docs/adr/0010-session-log-derived-recovery-tips.md +26 -0
- package/docs/adr/0011-isolate-the-local-recovery-tip-provider.md +20 -0
- package/docs/adr/0012-preserve-mixed-destructuring-initialization.md +23 -0
- package/docs/adr/0013-isolate-repl-redeclaration-convenience.md +21 -0
- package/docs/adr/0014-persist-cell-rewrite-policy.md +25 -0
- package/docs/adr/0015-preserve-source-positions-through-rewrites.md +25 -0
- package/docs/adr/0016-archive-resolved-review-findings.md +43 -0
- package/docs/adr/0017-track-the-latest-dsh-public-surface.md +23 -0
- package/docs/adr/0018-use-versioned-fixture-for-ab-benchmark.md +25 -0
- package/docs/adr/0019-plugin-settings-and-kill-switch.md +32 -0
- package/docs/adr/0020-optional-cordis-tools-in-ptc-mode.md +36 -0
- package/docs/adr/0021-separate-host-bootstrap-from-session-runtime.md +68 -0
- package/docs/adr/README.md +18 -0
- package/docs/architecture.md +134 -0
- package/docs/capability-projection.md +48 -0
- package/docs/client-ui.md +47 -0
- package/docs/durability-design.md +265 -0
- package/docs/evaluation.md +62 -0
- package/docs/installation.md +82 -0
- package/docs/program-data-plane.md +61 -0
- package/docs/publishing.md +37 -0
- package/docs/runtime-reference.md +119 -0
- package/docs/value-wire.md +61 -0
- package/index.js +532 -0
- package/internal/ast-traversal.js +20 -0
- package/internal/binding-descriptors.js +95 -0
- package/internal/binding-pattern.js +54 -0
- package/internal/builtin-esm-sync.js +9 -0
- package/internal/cell-analysis.js +467 -0
- package/internal/cell-rewriter.js +520 -0
- package/internal/config-spec.js +179 -0
- package/internal/cordis-tools-owner.js +363 -0
- package/internal/diagnostic.js +133 -0
- package/internal/direct-surface-owner.js +326 -0
- package/internal/edit-transport-owner.js +287 -0
- package/internal/failure-reporting.js +139 -0
- package/internal/kernel-worker.js +484 -0
- package/internal/module-policy.js +46 -0
- package/internal/program-bindings.js +187 -0
- package/internal/record-utils.js +53 -0
- package/internal/recovery-tips.js +81 -0
- package/internal/rejected-cell-editor.js +328 -0
- package/internal/repl-convenience.js +201 -0
- package/internal/runtime-bridge-owner.js +417 -0
- package/internal/runtime-config.js +77 -0
- package/internal/runtime-contexts.js +31 -0
- package/internal/sdk-projection.js +88 -0
- package/internal/session-cell-executor.js +612 -0
- package/internal/session-journal.js +889 -0
- package/internal/session-log-view.js +80 -0
- package/internal/session-runtime.js +536 -0
- package/internal/session-state.js +45 -0
- package/internal/source-position-map.js +224 -0
- package/internal/tool-call-canonicalizer.js +339 -0
- package/internal/value-wire.js +421 -0
- package/internal/worker-client.js +218 -0
- package/internal/worker-cwd-virtualization.js +155 -0
- package/package.json +100 -0
- package/src/client.js +165 -0
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
import * as CordisTools from '@deepseek-ai/dsh-tool-cordis'
|
|
2
|
+
import * as CordisSkillFilesystem from '@deepseek-ai/dsh-skill-filesystem'
|
|
3
|
+
import { dirname, isAbsolute, join } from 'node:path'
|
|
4
|
+
import { isDeepStrictEqual } from 'node:util'
|
|
5
|
+
import { RUN_CODE } from './runtime-bridge-owner.js'
|
|
6
|
+
|
|
7
|
+
const CORDIS_PRESET_ID = 'cordis'
|
|
8
|
+
const CORDIS_SKILL_NAME = 'cordis-plugin-development'
|
|
9
|
+
const CORDIS_SKILL_PROVIDER = 'ptc-plus-cordis'
|
|
10
|
+
|
|
11
|
+
function requireFiberServices(agent, fiber, owner) {
|
|
12
|
+
if (typeof agent?.ctx?.get !== 'function') {
|
|
13
|
+
throw new Error('ptc-plus: cordisToolsEnabled requires the DSH agent Context.get API')
|
|
14
|
+
}
|
|
15
|
+
if (fiber.inject === null || typeof fiber.inject !== 'object' || Array.isArray(fiber.inject)) {
|
|
16
|
+
throw new Error(`ptc-plus: DSH Context.plugin did not expose resolved ${owner} service dependencies`)
|
|
17
|
+
}
|
|
18
|
+
const missing = Object.keys(fiber.inject).filter(service => agent.ctx.get(service) === undefined)
|
|
19
|
+
if (missing.length > 0) {
|
|
20
|
+
throw new Error(
|
|
21
|
+
`ptc-plus: cordisToolsEnabled requires DSH services: ${missing.join(', ')}`,
|
|
22
|
+
)
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function requireCompanionServices(agent) {
|
|
27
|
+
if (typeof agent?.ctx?.get !== 'function') {
|
|
28
|
+
throw new Error('ptc-plus: cordisToolsEnabled requires the DSH agent Context.get API')
|
|
29
|
+
}
|
|
30
|
+
const agentPresets = agent.ctx.get('agentPresets')
|
|
31
|
+
if (typeof agentPresets?.resolve !== 'function') {
|
|
32
|
+
throw new Error('ptc-plus: cordisToolsEnabled requires the DSH agentPresets.resolve API')
|
|
33
|
+
}
|
|
34
|
+
const skills = agent.ctx.get('skills')
|
|
35
|
+
if (typeof skills?.list !== 'function' || typeof skills?.get !== 'function') {
|
|
36
|
+
throw new Error('ptc-plus: cordisToolsEnabled requires the DSH skills list/get APIs')
|
|
37
|
+
}
|
|
38
|
+
if (agent.ctx.tools?.get?.('skill', agent) === undefined) {
|
|
39
|
+
throw new Error('ptc-plus: cordisToolsEnabled requires the DSH skill tool in the PTC agent scope')
|
|
40
|
+
}
|
|
41
|
+
return { agentPresets, skills }
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function companionSkillDirectory(agentPresets) {
|
|
45
|
+
const preset = await agentPresets.resolve(CORDIS_PRESET_ID)
|
|
46
|
+
if (typeof preset?.path !== 'string' || !isAbsolute(preset.path)) {
|
|
47
|
+
throw new Error('ptc-plus: DSH cordis preset did not expose an absolute composition path')
|
|
48
|
+
}
|
|
49
|
+
if (typeof preset.broken === 'string' && preset.broken.length > 0) {
|
|
50
|
+
throw new Error(`ptc-plus: DSH cordis preset is unavailable: ${preset.broken}`)
|
|
51
|
+
}
|
|
52
|
+
return join(dirname(preset.path), 'skills')
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function requireCompanionSkill(agent, skills) {
|
|
56
|
+
const lookup = {
|
|
57
|
+
cwd: agent.session?.header?.cwd,
|
|
58
|
+
scope: agent,
|
|
59
|
+
}
|
|
60
|
+
const summary = (await skills.list(lookup)).find(skill => skill?.name === CORDIS_SKILL_NAME)
|
|
61
|
+
if (summary?.provider !== CORDIS_SKILL_PROVIDER
|
|
62
|
+
|| summary.invocation?.modelInvocable !== true) {
|
|
63
|
+
throw new Error(`ptc-plus: official Cordis companion Skill ${JSON.stringify(CORDIS_SKILL_NAME)} is not model-invocable`)
|
|
64
|
+
}
|
|
65
|
+
const skill = await skills.get(CORDIS_SKILL_NAME, lookup)
|
|
66
|
+
if (skill?.provider !== CORDIS_SKILL_PROVIDER
|
|
67
|
+
|| skill.invocation?.modelInvocable !== true
|
|
68
|
+
|| typeof skill.content !== 'string'
|
|
69
|
+
|| skill.content.length === 0) {
|
|
70
|
+
throw new Error(`ptc-plus: official Cordis companion Skill ${JSON.stringify(CORDIS_SKILL_NAME)} is not loadable`)
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function createCordisInspectLeases(ctx) {
|
|
75
|
+
let cordisInspect
|
|
76
|
+
const requireCordisInspect = () => {
|
|
77
|
+
if (typeof ctx?.get !== 'function') {
|
|
78
|
+
throw new Error('ptc-plus: cordisToolsEnabled requires the DSH Context.get API')
|
|
79
|
+
}
|
|
80
|
+
cordisInspect ??= ctx.get('cordisInspect')
|
|
81
|
+
if (typeof cordisInspect?.register !== 'function') {
|
|
82
|
+
throw new Error('ptc-plus: cordisToolsEnabled requires the DSH cordisInspect.register API')
|
|
83
|
+
}
|
|
84
|
+
return cordisInspect
|
|
85
|
+
}
|
|
86
|
+
const providers = new Map()
|
|
87
|
+
|
|
88
|
+
const register = (registration) => {
|
|
89
|
+
const id = registration?.manifest?.id
|
|
90
|
+
if (typeof id !== 'string' || id.length === 0) {
|
|
91
|
+
throw new Error('ptc-plus: Cordis inspect registration requires a non-empty manifest id')
|
|
92
|
+
}
|
|
93
|
+
let provider = providers.get(id)
|
|
94
|
+
if (provider === undefined) {
|
|
95
|
+
const registrations = new Set([registration])
|
|
96
|
+
const delegated = {
|
|
97
|
+
...registration,
|
|
98
|
+
query(...args) {
|
|
99
|
+
const current = registrations.values().next().value
|
|
100
|
+
return Reflect.apply(current.query, current, args)
|
|
101
|
+
},
|
|
102
|
+
}
|
|
103
|
+
provider = {
|
|
104
|
+
manifest: registration.manifest,
|
|
105
|
+
registrations,
|
|
106
|
+
dispose: requireCordisInspect().register(delegated),
|
|
107
|
+
}
|
|
108
|
+
if (typeof provider.dispose !== 'function') {
|
|
109
|
+
throw new Error(`ptc-plus: Cordis inspect provider "${id}" did not return a disposer`)
|
|
110
|
+
}
|
|
111
|
+
providers.set(id, provider)
|
|
112
|
+
} else {
|
|
113
|
+
if (!isDeepStrictEqual(provider.manifest, registration.manifest)) {
|
|
114
|
+
throw new Error(`ptc-plus: Cordis inspect provider "${id}" changed its manifest across agent scopes`)
|
|
115
|
+
}
|
|
116
|
+
provider.registrations.add(registration)
|
|
117
|
+
}
|
|
118
|
+
let active = true
|
|
119
|
+
return () => {
|
|
120
|
+
if (!active) return
|
|
121
|
+
active = false
|
|
122
|
+
provider.registrations.delete(registration)
|
|
123
|
+
if (provider.registrations.size > 0) return
|
|
124
|
+
providers.delete(id)
|
|
125
|
+
provider.dispose()
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const facade = new Proxy({}, {
|
|
130
|
+
get(_target, property) {
|
|
131
|
+
if (property === 'register') return register
|
|
132
|
+
const target = requireCordisInspect()
|
|
133
|
+
const value = Reflect.get(target, property, target)
|
|
134
|
+
return typeof value === 'function' ? value.bind(target) : value
|
|
135
|
+
},
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
return Object.freeze({
|
|
139
|
+
plugin(cordisPlugin) {
|
|
140
|
+
return {
|
|
141
|
+
...cordisPlugin,
|
|
142
|
+
apply(pluginCtx, config) {
|
|
143
|
+
if (typeof pluginCtx?.extend !== 'function') {
|
|
144
|
+
throw new Error('ptc-plus: cordisToolsEnabled requires the DSH Context.extend API')
|
|
145
|
+
}
|
|
146
|
+
return cordisPlugin.apply(pluginCtx.extend({ cordisInspect: facade }), config)
|
|
147
|
+
},
|
|
148
|
+
}
|
|
149
|
+
},
|
|
150
|
+
dispose() {
|
|
151
|
+
const failures = []
|
|
152
|
+
for (const provider of providers.values()) {
|
|
153
|
+
try {
|
|
154
|
+
provider.dispose()
|
|
155
|
+
} catch (error) {
|
|
156
|
+
failures.push(error)
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
providers.clear()
|
|
160
|
+
if (failures.length > 0) {
|
|
161
|
+
throw new AggregateError(failures, 'ptc-plus Cordis inspect lease disposal failed')
|
|
162
|
+
}
|
|
163
|
+
},
|
|
164
|
+
})
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** Own the official Cordis tool and companion Skill fibers in PTC agent scopes. */
|
|
168
|
+
export function createCordisToolsOwner(
|
|
169
|
+
ctx,
|
|
170
|
+
cordisPlugin = CordisTools,
|
|
171
|
+
skillFilesystemPlugin = CordisSkillFilesystem,
|
|
172
|
+
) {
|
|
173
|
+
if (typeof ctx?.agents?.list !== 'function') {
|
|
174
|
+
throw new Error('ptc-plus: cordisToolsEnabled requires the DSH agents.list API')
|
|
175
|
+
}
|
|
176
|
+
const mounts = new Map()
|
|
177
|
+
const pending = new Set()
|
|
178
|
+
const cordisInspectLeases = createCordisInspectLeases(ctx)
|
|
179
|
+
const scopedCordisPlugin = cordisInspectLeases.plugin(cordisPlugin)
|
|
180
|
+
let disposed = false
|
|
181
|
+
|
|
182
|
+
const leafErrors = error => error instanceof AggregateError
|
|
183
|
+
? error.errors.flatMap(leafErrors)
|
|
184
|
+
: [error]
|
|
185
|
+
const activationCleanupError = (activationError, cleanupError) => new AggregateError(
|
|
186
|
+
[...new Set([...leafErrors(activationError), ...leafErrors(cleanupError)])],
|
|
187
|
+
'ptc-plus: Cordis activation and cleanup failed',
|
|
188
|
+
{ cause: activationError },
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
const disposeMount = (agent, mount) => {
|
|
192
|
+
mount.disposed = true
|
|
193
|
+
if (mount.disposal === undefined) {
|
|
194
|
+
mount.disposal = (async () => {
|
|
195
|
+
const failures = []
|
|
196
|
+
for (const fiber of [...mount.fibers].reverse()) {
|
|
197
|
+
try {
|
|
198
|
+
await fiber.dispose()
|
|
199
|
+
} catch (error) {
|
|
200
|
+
failures.push(error)
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
if (failures.length > 0) {
|
|
204
|
+
throw new AggregateError(failures, 'ptc-plus Cordis agent mount disposal failed')
|
|
205
|
+
}
|
|
206
|
+
})()
|
|
207
|
+
}
|
|
208
|
+
return mount.disposal.then(() => {
|
|
209
|
+
if (mounts.get(agent) === mount) mounts.delete(agent)
|
|
210
|
+
})
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const disposeAgent = async (agent) => {
|
|
214
|
+
pending.delete(agent)
|
|
215
|
+
const mount = mounts.get(agent)
|
|
216
|
+
if (mount === undefined) return
|
|
217
|
+
await disposeMount(agent, mount)
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const installAgent = (agent) => {
|
|
221
|
+
if (disposed) return Promise.resolve()
|
|
222
|
+
const mounted = mounts.get(agent)
|
|
223
|
+
if (mounted !== undefined) return mounted.activation
|
|
224
|
+
if (agent?.ctx?.tools?.get?.(RUN_CODE, agent) === undefined) {
|
|
225
|
+
pending.add(agent)
|
|
226
|
+
return Promise.resolve()
|
|
227
|
+
}
|
|
228
|
+
pending.delete(agent)
|
|
229
|
+
if (typeof agent.ctx.plugin !== 'function') {
|
|
230
|
+
throw new Error('ptc-plus: cordisToolsEnabled requires the DSH agent Context.plugin API')
|
|
231
|
+
}
|
|
232
|
+
const mount = {
|
|
233
|
+
fibers: [],
|
|
234
|
+
activation: undefined,
|
|
235
|
+
disposal: undefined,
|
|
236
|
+
activated: false,
|
|
237
|
+
disposed: false,
|
|
238
|
+
}
|
|
239
|
+
mounts.set(agent, mount)
|
|
240
|
+
mount.activation = Promise.resolve().then(async () => {
|
|
241
|
+
const { agentPresets, skills } = requireCompanionServices(agent)
|
|
242
|
+
const skillDirectory = await companionSkillDirectory(agentPresets)
|
|
243
|
+
if (mount.disposed) return
|
|
244
|
+
|
|
245
|
+
const skillFiber = agent.ctx.plugin(skillFilesystemPlugin, {
|
|
246
|
+
providerName: CORDIS_SKILL_PROVIDER,
|
|
247
|
+
includeDefaultRoots: false,
|
|
248
|
+
customSkillDirs: [skillDirectory],
|
|
249
|
+
})
|
|
250
|
+
if (typeof skillFiber?.dispose !== 'function') {
|
|
251
|
+
throw new Error('ptc-plus: DSH Context.plugin did not return a disposable Cordis Skill fiber')
|
|
252
|
+
}
|
|
253
|
+
mount.fibers.push(skillFiber)
|
|
254
|
+
requireFiberServices(agent, skillFiber, 'Cordis Skill')
|
|
255
|
+
await Promise.resolve(skillFiber)
|
|
256
|
+
if (mount.disposed) return
|
|
257
|
+
|
|
258
|
+
const cordisFiber = agent.ctx.plugin(scopedCordisPlugin)
|
|
259
|
+
if (typeof cordisFiber?.dispose !== 'function') {
|
|
260
|
+
throw new Error('ptc-plus: DSH Context.plugin did not return a disposable Cordis tool fiber')
|
|
261
|
+
}
|
|
262
|
+
mount.fibers.push(cordisFiber)
|
|
263
|
+
requireFiberServices(agent, cordisFiber, 'Cordis tool')
|
|
264
|
+
await Promise.resolve(cordisFiber)
|
|
265
|
+
if (mount.disposed) return
|
|
266
|
+
|
|
267
|
+
await requireCompanionSkill(agent, skills)
|
|
268
|
+
if (mount.disposed) return
|
|
269
|
+
mount.activated = true
|
|
270
|
+
}).catch(async error => {
|
|
271
|
+
if (!mount.disposed) {
|
|
272
|
+
try {
|
|
273
|
+
await disposeMount(agent, mount)
|
|
274
|
+
} catch (cleanupError) {
|
|
275
|
+
throw activationCleanupError(error, cleanupError)
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
throw error
|
|
279
|
+
})
|
|
280
|
+
return mount.activation
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const promptAssembly = async (_assembly, context, next) => {
|
|
284
|
+
const agent = context?.scope
|
|
285
|
+
if (agent === undefined || mounts.get(agent)?.activated === true) return next()
|
|
286
|
+
await installAgent(agent)
|
|
287
|
+
if (disposed || mounts.get(agent)?.activated !== true) return next()
|
|
288
|
+
if (typeof ctx.systemPrompt?.assemble !== 'function') {
|
|
289
|
+
throw new Error('ptc-plus: cordisToolsEnabled requires the DSH systemPrompt.assemble API')
|
|
290
|
+
}
|
|
291
|
+
return ctx.systemPrompt.assemble(context)
|
|
292
|
+
}
|
|
293
|
+
const retryPending = () => {
|
|
294
|
+
if (disposed || pending.size === 0) return
|
|
295
|
+
queueMicrotask(() => {
|
|
296
|
+
if (disposed) return
|
|
297
|
+
for (const agent of [...pending]) {
|
|
298
|
+
void Promise.resolve().then(() => installAgent(agent)).catch(error => {
|
|
299
|
+
pending.delete(agent)
|
|
300
|
+
ctx.logger?.warn?.('ptc-plus: deferred Cordis activation failed', error)
|
|
301
|
+
})
|
|
302
|
+
}
|
|
303
|
+
})
|
|
304
|
+
}
|
|
305
|
+
let stopCreated
|
|
306
|
+
let stopDisposed
|
|
307
|
+
let stopPromptAssembly
|
|
308
|
+
let stopToolsChange
|
|
309
|
+
let initialActivations
|
|
310
|
+
try {
|
|
311
|
+
stopPromptAssembly = ctx.on('system-prompt/assemble', promptAssembly, { prepend: true })
|
|
312
|
+
stopCreated = ctx.on('agent/created', ({ agent }) => installAgent(agent))
|
|
313
|
+
stopDisposed = ctx.on('agent/disposed', ({ agent }) => disposeAgent(agent))
|
|
314
|
+
stopToolsChange = ctx.on('tools/change', retryPending)
|
|
315
|
+
initialActivations = ctx.agents.list().map(installAgent)
|
|
316
|
+
} catch (error) {
|
|
317
|
+
stopToolsChange?.()
|
|
318
|
+
stopDisposed?.()
|
|
319
|
+
stopCreated?.()
|
|
320
|
+
stopPromptAssembly?.()
|
|
321
|
+
for (const [agent, mount] of mounts) {
|
|
322
|
+
void mount.activation?.catch(() => {})
|
|
323
|
+
void disposeMount(agent, mount)
|
|
324
|
+
}
|
|
325
|
+
cordisInspectLeases.dispose()
|
|
326
|
+
throw error
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
let disposal
|
|
330
|
+
const dispose = () => {
|
|
331
|
+
if (disposal !== undefined) return disposal
|
|
332
|
+
disposed = true
|
|
333
|
+
stopToolsChange()
|
|
334
|
+
stopDisposed()
|
|
335
|
+
stopCreated()
|
|
336
|
+
stopPromptAssembly()
|
|
337
|
+
pending.clear()
|
|
338
|
+
const owned = [...mounts.entries()]
|
|
339
|
+
const attempts = owned.map(([agent, mount]) => disposeMount(agent, mount))
|
|
340
|
+
disposal = Promise.allSettled(attempts).then(results => {
|
|
341
|
+
const failures = results.filter(result => result.status === 'rejected').map(result => result.reason)
|
|
342
|
+
try {
|
|
343
|
+
cordisInspectLeases.dispose()
|
|
344
|
+
} catch (error) {
|
|
345
|
+
failures.push(error)
|
|
346
|
+
}
|
|
347
|
+
if (failures.length > 0) throw new AggregateError(failures, 'ptc-plus Cordis disposal failed')
|
|
348
|
+
})
|
|
349
|
+
return disposal
|
|
350
|
+
}
|
|
351
|
+
const ready = Promise.all(initialActivations).then(() => undefined).catch(async error => {
|
|
352
|
+
try {
|
|
353
|
+
await dispose()
|
|
354
|
+
} catch (cleanupError) {
|
|
355
|
+
throw activationCleanupError(error, cleanupError)
|
|
356
|
+
}
|
|
357
|
+
throw error
|
|
358
|
+
})
|
|
359
|
+
return Object.freeze({
|
|
360
|
+
ready,
|
|
361
|
+
dispose,
|
|
362
|
+
})
|
|
363
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { codeFrameColumns } from '@babel/code-frame'
|
|
2
|
+
import { assertOwnFields, isRecord } from './record-utils.js'
|
|
3
|
+
|
|
4
|
+
const DIAGNOSTIC_FIELDS = new Set([
|
|
5
|
+
'code', 'severity', 'phase', 'message', 'stateEffect', 'dispatchState',
|
|
6
|
+
'source', 'cause', 'help',
|
|
7
|
+
])
|
|
8
|
+
const SOURCE_FIELDS = new Set(['cell', 'start', 'end'])
|
|
9
|
+
const POSITION_FIELDS = new Set(['line', 'column'])
|
|
10
|
+
const CAUSE_FIELDS = new Set(['code', 'message'])
|
|
11
|
+
const SEVERITIES = new Set(['error', 'warning', 'note'])
|
|
12
|
+
const PHASES = new Set(['parse', 'preflight', 'execute', 'tool-dispatch', 'replay', 'recover'])
|
|
13
|
+
const STATE_EFFECTS = new Set(['unchanged', 'partially-applied', 'rolled-back', 'unknown'])
|
|
14
|
+
const DISPATCH_STATES = new Set(['not-dispatched', 'dispatched', 'completed', 'unknown'])
|
|
15
|
+
|
|
16
|
+
function isLine(value) {
|
|
17
|
+
return typeof value === 'string' && value.length > 0 && !/[\r\n]/.test(value)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function normalizePosition(value, label) {
|
|
21
|
+
if (!isRecord(value) || !Number.isSafeInteger(value.line) || value.line < 1
|
|
22
|
+
|| !Number.isSafeInteger(value.column) || value.column < 1) {
|
|
23
|
+
throw new Error(`invalid diagnostic ${label}`)
|
|
24
|
+
}
|
|
25
|
+
assertOwnFields(value, POSITION_FIELDS, `diagnostic ${label}`)
|
|
26
|
+
return Object.freeze({ line: value.line, column: value.column })
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function normalizeCause(value, depth) {
|
|
30
|
+
if (depth > 16) throw new Error('diagnostic cause chain is too deep')
|
|
31
|
+
if (!isRecord(value)) throw new Error('invalid diagnostic cause')
|
|
32
|
+
if (!Object.hasOwn(value, 'severity')) {
|
|
33
|
+
assertOwnFields(value, CAUSE_FIELDS, 'diagnostic cause')
|
|
34
|
+
if (value.code !== undefined && !isLine(value.code)) {
|
|
35
|
+
throw new Error('invalid diagnostic cause code')
|
|
36
|
+
}
|
|
37
|
+
if (!isLine(value.message)) {
|
|
38
|
+
throw new Error('invalid diagnostic cause message')
|
|
39
|
+
}
|
|
40
|
+
return Object.freeze({
|
|
41
|
+
...(value.code === undefined ? {} : { code: value.code }),
|
|
42
|
+
message: value.message,
|
|
43
|
+
})
|
|
44
|
+
}
|
|
45
|
+
return normalizeDiagnostic(value, depth + 1)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function normalizeDiagnostic(value, depth = 0) {
|
|
49
|
+
if (!isRecord(value)) throw new Error('invalid dsh-ptc-plus diagnostic')
|
|
50
|
+
assertOwnFields(value, DIAGNOSTIC_FIELDS, 'dsh-ptc-plus diagnostic')
|
|
51
|
+
if (typeof value.code !== 'string' || !/^[A-Z][A-Z0-9-]{2,31}$/.test(value.code)) {
|
|
52
|
+
throw new Error('invalid dsh-ptc-plus diagnostic code')
|
|
53
|
+
}
|
|
54
|
+
if (!SEVERITIES.has(value.severity) || !PHASES.has(value.phase)
|
|
55
|
+
|| !STATE_EFFECTS.has(value.stateEffect) || !isLine(value.message)) {
|
|
56
|
+
throw new Error('invalid dsh-ptc-plus diagnostic')
|
|
57
|
+
}
|
|
58
|
+
if (value.dispatchState !== undefined && !DISPATCH_STATES.has(value.dispatchState)) {
|
|
59
|
+
throw new Error('invalid dsh-ptc-plus diagnostic dispatch state')
|
|
60
|
+
}
|
|
61
|
+
let source
|
|
62
|
+
if (value.source !== undefined) {
|
|
63
|
+
if (!isRecord(value.source) || !isLine(value.source.cell)) {
|
|
64
|
+
throw new Error('invalid dsh-ptc-plus diagnostic source')
|
|
65
|
+
}
|
|
66
|
+
assertOwnFields(value.source, SOURCE_FIELDS, 'diagnostic source')
|
|
67
|
+
source = {
|
|
68
|
+
cell: value.source.cell,
|
|
69
|
+
start: normalizePosition(value.source.start, 'source start'),
|
|
70
|
+
...(value.source.end === undefined ? {} : { end: normalizePosition(value.source.end, 'source end') }),
|
|
71
|
+
}
|
|
72
|
+
if (source.end !== undefined && (source.end.line < source.start.line
|
|
73
|
+
|| (source.end.line === source.start.line && source.end.column < source.start.column))) {
|
|
74
|
+
throw new Error('diagnostic source end precedes its start')
|
|
75
|
+
}
|
|
76
|
+
Object.freeze(source)
|
|
77
|
+
}
|
|
78
|
+
let cause
|
|
79
|
+
if (value.cause !== undefined) cause = normalizeCause(value.cause, depth)
|
|
80
|
+
let help
|
|
81
|
+
if (value.help !== undefined) {
|
|
82
|
+
if (!Array.isArray(value.help) || value.help.length > 3
|
|
83
|
+
|| value.help.some(item => !isLine(item))) {
|
|
84
|
+
throw new Error('invalid dsh-ptc-plus diagnostic help')
|
|
85
|
+
}
|
|
86
|
+
help = Object.freeze([...value.help])
|
|
87
|
+
}
|
|
88
|
+
return Object.freeze({
|
|
89
|
+
code: value.code,
|
|
90
|
+
severity: value.severity,
|
|
91
|
+
phase: value.phase,
|
|
92
|
+
message: value.message,
|
|
93
|
+
stateEffect: value.stateEffect,
|
|
94
|
+
...(value.dispatchState === undefined ? {} : { dispatchState: value.dispatchState }),
|
|
95
|
+
...(source === undefined ? {} : { source }),
|
|
96
|
+
...(cause === undefined ? {} : { cause }),
|
|
97
|
+
...(help === undefined ? {} : { help }),
|
|
98
|
+
})
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function diagnostic(value) {
|
|
102
|
+
return normalizeDiagnostic(value)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function causeText(cause) {
|
|
106
|
+
if (cause === undefined) return undefined
|
|
107
|
+
if (cause.code === undefined) return `cause: ${cause.message}`
|
|
108
|
+
return `cause: ${cause.code}: ${cause.message}`
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Render a diagnostic without ANSI escapes or model-side parsing conventions. */
|
|
112
|
+
export function renderDiagnostic(value, sourceText = undefined) {
|
|
113
|
+
const normalized = normalizeDiagnostic(value)
|
|
114
|
+
const lines = [`${normalized.severity}[${normalized.code}]: ${normalized.message}`]
|
|
115
|
+
if (normalized.source !== undefined) {
|
|
116
|
+
const { source } = normalized
|
|
117
|
+
lines.push(` --> ${source.cell}:${source.start.line}:${source.start.column}`)
|
|
118
|
+
if (typeof sourceText === 'string') {
|
|
119
|
+
const frame = codeFrameColumns(sourceText, {
|
|
120
|
+
start: source.start,
|
|
121
|
+
...(source.end === undefined ? {} : { end: source.end }),
|
|
122
|
+
}, { highlightCode: false, linesAbove: 0, linesBelow: 0 })
|
|
123
|
+
if (frame.length > 0) lines.push(frame)
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
lines.push(`phase: ${normalized.phase}`)
|
|
127
|
+
if (normalized.dispatchState !== undefined) lines.push(`dispatch: ${normalized.dispatchState}`)
|
|
128
|
+
lines.push(`state: ${normalized.stateEffect}`)
|
|
129
|
+
const cause = causeText(normalized.cause)
|
|
130
|
+
if (cause !== undefined) lines.push(cause)
|
|
131
|
+
for (const help of normalized.help ?? []) lines.push(`help: ${help}`)
|
|
132
|
+
return lines.join('\n')
|
|
133
|
+
}
|