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.
Files changed (77) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +137 -0
  3. package/README.zh.md +137 -0
  4. package/assets/dsh-ptc-plus-banner.webp +0 -0
  5. package/assets/ptc-plus-repair.png +0 -0
  6. package/assets/ptc-plus-settings.png +0 -0
  7. package/client.js +366 -0
  8. package/cordis.patch.yml +5 -0
  9. package/docs/README.md +21 -0
  10. package/docs/adr/0001-delegate-governance-to-dsh.md +3 -0
  11. package/docs/adr/0002-limit-work-map-scope.md +3 -0
  12. package/docs/adr/0003-prefer-native-program-surfaces.md +3 -0
  13. package/docs/adr/0004-declare-program-bindings-at-the-host.md +5 -0
  14. package/docs/adr/0005-temporary-rejected-cell-edit-transport.md +129 -0
  15. package/docs/adr/0006-rewrite-module-syntax-with-an-ast.md +45 -0
  16. package/docs/adr/0007-separate-worker-transport-from-session-semantics.md +39 -0
  17. package/docs/adr/0008-define-agent-operating-contract.md +25 -0
  18. package/docs/adr/0009-bound-expensive-acceptance-default.md +39 -0
  19. package/docs/adr/0010-session-log-derived-recovery-tips.md +26 -0
  20. package/docs/adr/0011-isolate-the-local-recovery-tip-provider.md +20 -0
  21. package/docs/adr/0012-preserve-mixed-destructuring-initialization.md +23 -0
  22. package/docs/adr/0013-isolate-repl-redeclaration-convenience.md +21 -0
  23. package/docs/adr/0014-persist-cell-rewrite-policy.md +25 -0
  24. package/docs/adr/0015-preserve-source-positions-through-rewrites.md +25 -0
  25. package/docs/adr/0016-archive-resolved-review-findings.md +43 -0
  26. package/docs/adr/0017-track-the-latest-dsh-public-surface.md +23 -0
  27. package/docs/adr/0018-use-versioned-fixture-for-ab-benchmark.md +25 -0
  28. package/docs/adr/0019-plugin-settings-and-kill-switch.md +32 -0
  29. package/docs/adr/0020-optional-cordis-tools-in-ptc-mode.md +36 -0
  30. package/docs/adr/0021-separate-host-bootstrap-from-session-runtime.md +68 -0
  31. package/docs/adr/README.md +18 -0
  32. package/docs/architecture.md +134 -0
  33. package/docs/capability-projection.md +48 -0
  34. package/docs/client-ui.md +47 -0
  35. package/docs/durability-design.md +265 -0
  36. package/docs/evaluation.md +62 -0
  37. package/docs/installation.md +82 -0
  38. package/docs/program-data-plane.md +61 -0
  39. package/docs/publishing.md +37 -0
  40. package/docs/runtime-reference.md +119 -0
  41. package/docs/value-wire.md +61 -0
  42. package/index.js +532 -0
  43. package/internal/ast-traversal.js +20 -0
  44. package/internal/binding-descriptors.js +95 -0
  45. package/internal/binding-pattern.js +54 -0
  46. package/internal/builtin-esm-sync.js +9 -0
  47. package/internal/cell-analysis.js +467 -0
  48. package/internal/cell-rewriter.js +520 -0
  49. package/internal/config-spec.js +179 -0
  50. package/internal/cordis-tools-owner.js +363 -0
  51. package/internal/diagnostic.js +133 -0
  52. package/internal/direct-surface-owner.js +326 -0
  53. package/internal/edit-transport-owner.js +287 -0
  54. package/internal/failure-reporting.js +139 -0
  55. package/internal/kernel-worker.js +484 -0
  56. package/internal/module-policy.js +46 -0
  57. package/internal/program-bindings.js +187 -0
  58. package/internal/record-utils.js +53 -0
  59. package/internal/recovery-tips.js +81 -0
  60. package/internal/rejected-cell-editor.js +328 -0
  61. package/internal/repl-convenience.js +201 -0
  62. package/internal/runtime-bridge-owner.js +417 -0
  63. package/internal/runtime-config.js +77 -0
  64. package/internal/runtime-contexts.js +31 -0
  65. package/internal/sdk-projection.js +88 -0
  66. package/internal/session-cell-executor.js +612 -0
  67. package/internal/session-journal.js +889 -0
  68. package/internal/session-log-view.js +80 -0
  69. package/internal/session-runtime.js +536 -0
  70. package/internal/session-state.js +45 -0
  71. package/internal/source-position-map.js +224 -0
  72. package/internal/tool-call-canonicalizer.js +339 -0
  73. package/internal/value-wire.js +421 -0
  74. package/internal/worker-client.js +218 -0
  75. package/internal/worker-cwd-virtualization.js +155 -0
  76. package/package.json +100 -0
  77. 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
+ }