dsh-ptc-plus 0.2.3 → 0.3.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 (42) hide show
  1. package/README.en.md +171 -0
  2. package/README.md +94 -67
  3. package/assets/dsh-ptc-plus-banner-en.webp +0 -0
  4. package/assets/ptc-plus-repair-en.png +0 -0
  5. package/assets/ptc-plus-settings-en.png +0 -0
  6. package/assets/ptc-plus-settings-zh.png +0 -0
  7. package/client.js +1211 -128
  8. package/docs/adr/0019-plugin-settings-and-kill-switch.md +20 -4
  9. package/docs/adr/0020-optional-cordis-tools-in-ptc-mode.md +2 -2
  10. package/docs/adr/0022-stage-npm-releases-with-oidc.md +31 -0
  11. package/docs/architecture.md +10 -9
  12. package/docs/client-ui.md +30 -12
  13. package/docs/installation.md +13 -5
  14. package/docs/publishing.md +25 -1
  15. package/docs/runtime-reference.md +18 -12
  16. package/docs/value-wire.md +1 -1
  17. package/index.js +43 -12
  18. package/internal/cell-analysis.js +20 -3
  19. package/internal/cell-rewriter.js +36 -2
  20. package/internal/config-spec.js +192 -82
  21. package/internal/direct-surface-owner.js +77 -23
  22. package/internal/edit-transport-owner.js +10 -1
  23. package/internal/failure-reporting.js +15 -0
  24. package/internal/rejected-cell-editor.js +21 -12
  25. package/internal/repl-memory-projection.js +277 -0
  26. package/internal/run-code-description.js +34 -0
  27. package/internal/runtime-bridge-owner.js +63 -15
  28. package/internal/runtime-config.js +2 -0
  29. package/internal/session-cell-executor.js +14 -4
  30. package/internal/session-events.js +7 -0
  31. package/internal/session-journal.js +3 -2
  32. package/internal/session-log-view.js +2 -1
  33. package/internal/session-runtime.js +19 -2
  34. package/internal/session-state.js +83 -4
  35. package/internal/settings-compat.js +31 -0
  36. package/package.json +20 -13
  37. package/src/client-activity.js +472 -0
  38. package/src/client.js +536 -38
  39. package/README.zh.md +0 -144
  40. package/assets/ptc-plus-settings.png +0 -0
  41. /package/assets/{dsh-ptc-plus-banner.webp → dsh-ptc-plus-banner-zh.webp} +0 -0
  42. /package/assets/{ptc-plus-repair.png → ptc-plus-repair-zh.png} +0 -0
@@ -0,0 +1,472 @@
1
+ const JOURNAL_VERSIONS = new Set([1, 2, 3])
2
+ const JOURNAL_STATUSES = new Set(['durable', 'volatile', 'discarded', 'noop'])
3
+ const BINDING_MODES = new Set(['loose', 'strict'])
4
+ const JOURNAL_FIELDS = new Set([
5
+ 'version', 'bindingMode', 'rewritePolicy', 'status', 'calls', 'operations',
6
+ 'confirms', 'diagnostics', 'completion', 'volatileReason',
7
+ ])
8
+ const LEGACY_JOURNAL_FIELDS = new Set([...JOURNAL_FIELDS].filter(key => key !== 'rewritePolicy'))
9
+ const REWRITE_POLICY_FIELDS = new Set([
10
+ 'autoRewriteImports', 'autoStripExports', 'autoSplitRedeclarations',
11
+ ])
12
+ const CALL_SUCCESS_FIELDS = new Set(['global', 'member', 'args', 'ok', 'value', 'settle'])
13
+ const CALL_ERROR_FIELDS = new Set(['global', 'member', 'args', 'ok', 'error', 'settle'])
14
+ const OPERATION_FIELDS = new Set(['action', 'name'])
15
+ const RETURN_FIELDS = new Set(['kind', 'hasValue', 'value'])
16
+ const THROW_FIELDS = new Set(['kind', 'error'])
17
+ const ERROR_FIELDS = new Set(['kind', 'message'])
18
+ const DIAGNOSTIC_FIELDS = new Set([
19
+ 'code', 'severity', 'phase', 'message', 'stateEffect', 'dispatchState',
20
+ 'source', 'cause', 'help',
21
+ ])
22
+ const SOURCE_FIELDS = new Set(['cell', 'start', 'end'])
23
+ const POSITION_FIELDS = new Set(['line', 'column'])
24
+ const CAUSE_FIELDS = new Set(['code', 'message'])
25
+ const SEVERITIES = new Set(['error', 'warning', 'note'])
26
+ const PHASES = new Set(['parse', 'preflight', 'execute', 'tool-dispatch', 'replay', 'recover'])
27
+ const STATE_EFFECTS = new Set(['unchanged', 'partially-applied', 'rolled-back', 'unknown'])
28
+ const DISPATCH_STATES = new Set(['not-dispatched', 'dispatched', 'completed', 'unknown'])
29
+ const REWRITE_FIELDS = new Set(['kind', 'description', 'source'])
30
+ const REWRITE_KINDS = new Set(['import', 'redeclaration', 'export'])
31
+ const REWRITE_POLICY_BY_KIND = Object.freeze({
32
+ import: 'autoRewriteImports',
33
+ redeclaration: 'autoSplitRedeclarations',
34
+ export: 'autoStripExports',
35
+ })
36
+ const EDIT_TARGET_FIELDS = new Set(['targetCallSeq'])
37
+ const DERIVED_RUN_FIELDS = new Set(['code', 'description'])
38
+ const RECOVERY_BOUNDARY_FIELDS = new Set(['failedCallSeq', 'frontierCallSeq'])
39
+ const VALUE_ENVELOPE_FIELDS = new Set(['codec', 'root', 'nodes'])
40
+ const VALUE_OBJECT_FIELDS = new Set(['type', 'prototype', 'entries'])
41
+ const VALUE_ARRAY_FIELDS = new Set(['type', 'length', 'entries'])
42
+ const VALUE_UNDEFINED_FIELDS = new Set(['tag'])
43
+ const VALUE_NUMBER_FIELDS = new Set(['tag', 'value'])
44
+ const VALUE_BIGINT_FIELDS = new Set(['tag', 'value'])
45
+ const VALUE_REFERENCE_FIELDS = new Set(['tag', 'index'])
46
+ const MAX_VALUE_NODES = 100_000
47
+ const MAX_VALUE_EDGES = 1_000_000
48
+ const MAX_ARRAY_LENGTH = 1_000_000
49
+ const MAX_BIGINT_DIGITS = 100_000
50
+ const MAX_STRING_BYTES = 64 * 1024 * 1024
51
+ const textEncoder = new TextEncoder()
52
+
53
+ function isRecord(value) {
54
+ return value !== null && typeof value === 'object' && !Array.isArray(value)
55
+ }
56
+
57
+ function hasClosedFields(value, allowed, required = []) {
58
+ if (!isRecord(value)) return false
59
+ const keys = Reflect.ownKeys(value)
60
+ if (keys.some(key => typeof key !== 'string' || !allowed.has(key)
61
+ || !Object.prototype.propertyIsEnumerable.call(value, key))) return false
62
+ return [...required].every(key => Object.hasOwn(value, key))
63
+ }
64
+
65
+ function hasExactFields(value, fields) {
66
+ return hasClosedFields(value, fields, fields) && Reflect.ownKeys(value).length === fields.size
67
+ }
68
+
69
+ function hasExactOrderedFields(value, fields) {
70
+ if (!isRecord(value)) return false
71
+ const keys = Reflect.ownKeys(value)
72
+ const ordered = [...fields]
73
+ return keys.length === ordered.length && keys.every((key, index) => (
74
+ key === ordered[index] && Object.prototype.propertyIsEnumerable.call(value, key)
75
+ ))
76
+ }
77
+
78
+ function isLine(value) {
79
+ return typeof value === 'string' && value.length > 0 && !/[\r\n]/.test(value)
80
+ }
81
+
82
+ function isSafeSequence(value) {
83
+ return Number.isSafeInteger(value) && value >= 0
84
+ }
85
+
86
+ function isValidPosition(value) {
87
+ return hasExactFields(value, POSITION_FIELDS)
88
+ && Number.isSafeInteger(value.line) && value.line >= 1
89
+ && Number.isSafeInteger(value.column) && value.column >= 1
90
+ }
91
+
92
+ function isValidDiagnosticCause(value, depth) {
93
+ if (depth > 16 || !isRecord(value)) return false
94
+ if (Object.hasOwn(value, 'severity')) return isValidDiagnostic(value, depth + 1)
95
+ return hasClosedFields(value, CAUSE_FIELDS, ['message'])
96
+ && (value.code === undefined || isLine(value.code))
97
+ && isLine(value.message)
98
+ }
99
+
100
+ function isValidDiagnostic(value, depth = 0) {
101
+ if (!hasClosedFields(value, DIAGNOSTIC_FIELDS, [
102
+ 'code', 'severity', 'phase', 'message', 'stateEffect',
103
+ ])) return false
104
+ if (typeof value.code !== 'string' || !/^[A-Z][A-Z0-9-]{2,31}$/.test(value.code)
105
+ || !SEVERITIES.has(value.severity) || !PHASES.has(value.phase)
106
+ || !STATE_EFFECTS.has(value.stateEffect) || !isLine(value.message)) return false
107
+ if (value.dispatchState !== undefined && !DISPATCH_STATES.has(value.dispatchState)) return false
108
+ if (value.source !== undefined) {
109
+ if (!hasClosedFields(value.source, SOURCE_FIELDS, ['cell', 'start'])
110
+ || !isLine(value.source.cell) || !isValidPosition(value.source.start)
111
+ || (value.source.end !== undefined && !isValidPosition(value.source.end))) return false
112
+ if (value.source.end !== undefined && (value.source.end.line < value.source.start.line
113
+ || (value.source.end.line === value.source.start.line
114
+ && value.source.end.column < value.source.start.column))) return false
115
+ }
116
+ if (value.cause !== undefined && !isValidDiagnosticCause(value.cause, depth)) return false
117
+ return value.help === undefined || (Array.isArray(value.help) && value.help.length <= 3
118
+ && value.help.every(isLine))
119
+ }
120
+
121
+ function isArrayIndexKey(value) {
122
+ if (!/^(0|[1-9][0-9]*)$/.test(value)) return false
123
+ const parsed = Number(value)
124
+ return Number.isInteger(parsed) && parsed >= 0 && parsed < 4_294_967_295
125
+ && String(parsed) === value
126
+ }
127
+
128
+ function accountText(value, state) {
129
+ state.textBytes += textEncoder.encode(value).byteLength
130
+ return state.textBytes <= MAX_STRING_BYTES
131
+ }
132
+
133
+ function isValidValueAtom(value, state) {
134
+ if (value === null || typeof value === 'boolean') return true
135
+ if (typeof value === 'string') return accountText(value, state)
136
+ if (typeof value === 'number') return Number.isFinite(value) && !Object.is(value, -0)
137
+ if (!isRecord(value) || typeof value.tag !== 'string') return false
138
+ if (value.tag === 'undefined') return hasExactOrderedFields(value, VALUE_UNDEFINED_FIELDS)
139
+ if (value.tag === 'number') {
140
+ return hasExactOrderedFields(value, VALUE_NUMBER_FIELDS)
141
+ && ['nan', 'infinity', '-infinity', '-0'].includes(value.value)
142
+ }
143
+ if (value.tag === 'bigint') {
144
+ if (!hasExactOrderedFields(value, VALUE_BIGINT_FIELDS) || typeof value.value !== 'string'
145
+ || !/^(0|-[1-9][0-9]*|[1-9][0-9]*)$/.test(value.value)) return false
146
+ const digits = value.value[0] === '-' ? value.value.length - 1 : value.value.length
147
+ return digits <= MAX_BIGINT_DIGITS && accountText(value.value, state)
148
+ }
149
+ if (value.tag !== 'reference' || !hasExactOrderedFields(value, VALUE_REFERENCE_FIELDS)
150
+ || !isSafeSequence(value.index) || value.index >= state.nodeCount) return false
151
+ if (!state.discovered.has(value.index)) {
152
+ if (value.index !== state.discovered.size) return false
153
+ state.discovered.add(value.index)
154
+ }
155
+ return true
156
+ }
157
+
158
+ function isValidValueWire(value) {
159
+ if (!hasExactOrderedFields(value, VALUE_ENVELOPE_FIELDS) || value.codec !== 'ptc-value-graph/v1'
160
+ || !Array.isArray(value.nodes) || value.nodes.length > MAX_VALUE_NODES) return false
161
+ const state = {
162
+ nodeCount: value.nodes.length,
163
+ discovered: new Set(),
164
+ edges: 0,
165
+ textBytes: 0,
166
+ }
167
+ if (!isValidValueAtom(value.root, state)) return false
168
+ for (const [nodeIndex, node] of value.nodes.entries()) {
169
+ if (!state.discovered.has(nodeIndex) || !isRecord(node)) return false
170
+ if (node.type === 'array') {
171
+ if (!hasExactOrderedFields(node, VALUE_ARRAY_FIELDS)
172
+ || !Number.isSafeInteger(node.length) || node.length < 0 || node.length > MAX_ARRAY_LENGTH
173
+ || !Array.isArray(node.entries)) return false
174
+ let previous = -1
175
+ for (const entry of node.entries) {
176
+ if (!Array.isArray(entry) || entry.length !== 2 || !Number.isSafeInteger(entry[0])
177
+ || entry[0] <= previous || entry[0] < 0 || entry[0] >= node.length) return false
178
+ previous = entry[0]
179
+ state.edges += 1
180
+ if (state.edges > MAX_VALUE_EDGES || !isValidValueAtom(entry[1], state)) return false
181
+ }
182
+ continue
183
+ }
184
+ if (node.type !== 'object' || !hasExactOrderedFields(node, VALUE_OBJECT_FIELDS)
185
+ || !['object', 'null'].includes(node.prototype) || !Array.isArray(node.entries)) return false
186
+ const keys = new Set()
187
+ let previousArrayIndex = -1
188
+ let sawOtherKey = false
189
+ for (const entry of node.entries) {
190
+ if (!Array.isArray(entry) || entry.length !== 2 || typeof entry[0] !== 'string'
191
+ || keys.has(entry[0]) || !accountText(entry[0], state)) return false
192
+ keys.add(entry[0])
193
+ if (isArrayIndexKey(entry[0])) {
194
+ const current = Number(entry[0])
195
+ if (sawOtherKey || current <= previousArrayIndex) return false
196
+ previousArrayIndex = current
197
+ } else {
198
+ sawOtherKey = true
199
+ }
200
+ state.edges += 1
201
+ if (state.edges > MAX_VALUE_EDGES || !isValidValueAtom(entry[1], state)) return false
202
+ }
203
+ }
204
+ return state.discovered.size === value.nodes.length
205
+ }
206
+
207
+ function isValidRewritePolicy(value) {
208
+ return hasExactFields(value, REWRITE_POLICY_FIELDS)
209
+ && [...REWRITE_POLICY_FIELDS].every(key => typeof value[key] === 'boolean')
210
+ }
211
+
212
+ function isValidCall(value) {
213
+ if (!isRecord(value) || (value.ok !== true && value.ok !== false)) return false
214
+ const fields = value.ok ? CALL_SUCCESS_FIELDS : CALL_ERROR_FIELDS
215
+ if (!hasExactFields(value, fields) || typeof value.global !== 'string'
216
+ || typeof value.member !== 'string' || !isValidValueWire(value.args)
217
+ || !isSafeSequence(value.settle)) return false
218
+ return value.ok ? isValidValueWire(value.value) : typeof value.error === 'string'
219
+ }
220
+
221
+ function isValidOperation(value) {
222
+ if (!hasClosedFields(value, OPERATION_FIELDS, ['action'])
223
+ || !['save', 'restore', 'delete'].includes(value.action)) return false
224
+ if (value.action === 'restore' && value.name === undefined) return true
225
+ return typeof value.name === 'string' && /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/.test(value.name)
226
+ }
227
+
228
+ function isValidCompletion(value) {
229
+ if (!isRecord(value) || !['return', 'throw'].includes(value.kind)) return false
230
+ if (value.kind === 'return') {
231
+ if (!hasClosedFields(value, RETURN_FIELDS, ['kind', 'hasValue'])
232
+ || typeof value.hasValue !== 'boolean') return false
233
+ return value.hasValue
234
+ ? Object.hasOwn(value, 'value') && isValidValueWire(value.value)
235
+ : !Object.hasOwn(value, 'value')
236
+ }
237
+ return hasExactFields(value, THROW_FIELDS) && hasExactFields(value.error, ERROR_FIELDS)
238
+ && typeof value.error.kind === 'string' && typeof value.error.message === 'string'
239
+ }
240
+
241
+ function isValidConfirms(value, version) {
242
+ if (value === undefined) return true
243
+ if (!Array.isArray(value) || new Set(value).size !== value.length) return false
244
+ if (version === 1) return value.length === 0
245
+ return value.every(isSafeSequence)
246
+ }
247
+
248
+ function isReadableJournalUnchecked(value) {
249
+ if (!isRecord(value) || !JOURNAL_VERSIONS.has(value.version) || !JOURNAL_STATUSES.has(value.status)) {
250
+ return false
251
+ }
252
+ const fields = value.version === 1 ? LEGACY_JOURNAL_FIELDS : JOURNAL_FIELDS
253
+ const required = ['version', 'bindingMode', 'status', 'calls', 'operations', 'diagnostics']
254
+ if (value.version !== 1) required.push('rewritePolicy')
255
+ if (!hasClosedFields(value, fields, required) || !BINDING_MODES.has(value.bindingMode)
256
+ || (value.version !== 1 && !isValidRewritePolicy(value.rewritePolicy))
257
+ || !Array.isArray(value.calls) || !value.calls.every(isValidCall)
258
+ || !Array.isArray(value.operations) || !value.operations.every(isValidOperation)
259
+ || !isValidConfirms(value.confirms, value.version)
260
+ || !Array.isArray(value.diagnostics) || !value.diagnostics.every(isValidDiagnostic)) return false
261
+ const settlementOrder = value.calls.map(call => call.settle).sort((left, right) => left - right)
262
+ if (settlementOrder.some((settle, index) => settle !== index)) return false
263
+ const requiresCompletion = value.status === 'durable' || value.status === 'volatile'
264
+ if ((requiresCompletion && !isValidCompletion(value.completion))
265
+ || (!requiresCompletion && value.completion !== undefined && !isValidCompletion(value.completion))) return false
266
+ if ((value.status === 'discarded' || value.status === 'noop')
267
+ && (value.calls.length !== 0 || value.operations.length !== 0)) return false
268
+ if (value.volatileReason !== undefined && (typeof value.volatileReason !== 'string'
269
+ || (value.status !== 'volatile' && value.status !== 'discarded'))) return false
270
+ return true
271
+ }
272
+
273
+ function isReadableJournal(value) {
274
+ try {
275
+ return isReadableJournalUnchecked(value)
276
+ } catch {
277
+ return false
278
+ }
279
+ }
280
+
281
+ // Keep the presentation fold closed over the journal contract. This guard is
282
+ // intentionally separate from the schema reader so future counters cannot
283
+ // accidentally treat an unknown status or partial arrays as activity.
284
+ function isCountableJournal(value) {
285
+ return isReadableJournal(value)
286
+ && JOURNAL_STATUSES.has(value.status)
287
+ && Array.isArray(value.calls)
288
+ && Array.isArray(value.operations)
289
+ && Array.isArray(value.diagnostics)
290
+ }
291
+
292
+ function isValidRewrites(value, journal) {
293
+ try {
294
+ return journal.status !== 'noop' && isValidRewritePolicy(journal.rewritePolicy)
295
+ && Array.isArray(value) && value.every(rewrite => (
296
+ hasClosedFields(rewrite, REWRITE_FIELDS, ['kind', 'description'])
297
+ && REWRITE_KINDS.has(rewrite.kind)
298
+ && journal.rewritePolicy[REWRITE_POLICY_BY_KIND[rewrite.kind]] === true
299
+ && typeof rewrite.description === 'string' && rewrite.description.length > 0
300
+ && (rewrite.source === undefined || typeof rewrite.source === 'string')
301
+ ))
302
+ } catch {
303
+ return false
304
+ }
305
+ }
306
+
307
+ function isValidEditTarget(value) {
308
+ try {
309
+ return hasExactFields(value, EDIT_TARGET_FIELDS) && isSafeSequence(value.targetCallSeq)
310
+ } catch {
311
+ return false
312
+ }
313
+ }
314
+
315
+ function isValidDerivedRun(value) {
316
+ try {
317
+ return hasExactFields(value, DERIVED_RUN_FIELDS)
318
+ && typeof value.code === 'string' && typeof value.description === 'string'
319
+ } catch {
320
+ return false
321
+ }
322
+ }
323
+
324
+ function isValidRecoveryBoundaries(value) {
325
+ try {
326
+ return Array.isArray(value) && value.every(boundary => (
327
+ hasExactFields(boundary, RECOVERY_BOUNDARY_FIELDS)
328
+ && isSafeSequence(boundary.failedCallSeq)
329
+ && (boundary.frontierCallSeq === null || isSafeSequence(boundary.frontierCallSeq))
330
+ ))
331
+ } catch {
332
+ return false
333
+ }
334
+ }
335
+
336
+ function isValidEditRelation(meta, args, journal) {
337
+ try {
338
+ if (!isRecord(meta) || !isRecord(args) || !isValidEditTarget(meta.dshPtcPlusEdit)
339
+ || !isValidDerivedRun(meta.dshPtcPlusDerivedRun) || journal.status === 'noop') return false
340
+ if (Object.hasOwn(meta, 'dshPtcPlusRecoveryBoundaries')
341
+ && !isValidRecoveryBoundaries(meta.dshPtcPlusRecoveryBoundaries)) return false
342
+ return !Object.hasOwn(args, 'expected_target_call_seq')
343
+ || args.expected_target_call_seq === meta.dshPtcPlusEdit.targetCallSeq
344
+ } catch {
345
+ return false
346
+ }
347
+ }
348
+
349
+ function rawArguments(block, settled) {
350
+ const call = settled ? block.call : block
351
+ return typeof call?.argsRaw === 'string' ? call.argsRaw
352
+ : typeof call?.arguments === 'string' ? call.arguments
353
+ : ''
354
+ }
355
+
356
+ function parseArguments(raw) {
357
+ try {
358
+ const value = JSON.parse(raw)
359
+ return isRecord(value) ? value : undefined
360
+ } catch {
361
+ return undefined
362
+ }
363
+ }
364
+
365
+ function resultText(block) {
366
+ if (!Array.isArray(block.content)) return ''
367
+ try {
368
+ const parts = block.content.map(item => item?.type === 'text' && typeof item.text === 'string'
369
+ ? item.text
370
+ : JSON.stringify(item, null, 2))
371
+ if (parts.length === 0 && isRecord(block.error)) {
372
+ return [block.error.name, block.error.code].filter(value => typeof value === 'string').join(': ')
373
+ }
374
+ return parts.filter(value => typeof value === 'string').join('\n')
375
+ } catch {
376
+ return ''
377
+ }
378
+ }
379
+
380
+ const MIXED_REDECLARATION = 'split a mixed top-level declaration while preserving native pattern initialization'
381
+ const GENERATED_RUN_CODE_DESCRIPTION_KEY = 'dshPtcPlusRunCodeDescription'
382
+
383
+ function rewriteFeature(rewrites, predicate, key) {
384
+ const matching = rewrites.filter(predicate)
385
+ if (matching.length === 0) return undefined
386
+ const sources = [...new Set(matching.map(rewrite => rewrite.source).filter(source => source !== undefined))]
387
+ return { key, detail: sources.join(', ') }
388
+ }
389
+
390
+ function operationFeatures(operations) {
391
+ const keys = {
392
+ save: 'feature.stateSaved',
393
+ restore: 'feature.stateRestored',
394
+ delete: 'feature.stateDeleted',
395
+ }
396
+ return operations.map(operation => ({
397
+ key: keys[operation.action],
398
+ detail: operation.name ?? '',
399
+ }))
400
+ }
401
+
402
+ function uniqueFeatures(features) {
403
+ const seen = new Set()
404
+ return features.filter((feature) => {
405
+ if (feature === undefined) return false
406
+ const identity = `${feature.key}\u0000${feature.detail}`
407
+ if (seen.has(identity)) return false
408
+ seen.add(identity)
409
+ return true
410
+ })
411
+ }
412
+
413
+ function isDurableRecoveryDiagnostic(diagnostic) {
414
+ return diagnostic.code === 'PTC-R002'
415
+ && diagnostic.severity === 'warning'
416
+ && diagnostic.phase === 'recover'
417
+ && diagnostic.stateEffect === 'rolled-back'
418
+ }
419
+
420
+ /** Derive one read-only body view from a DSH running or settled tool block. */
421
+ export function derivePtcToolView(block, toolName = undefined) {
422
+ const settled = isRecord(block) && block.kind === 'tool-result'
423
+ const argsRaw = isRecord(block) ? rawArguments(block, settled) : ''
424
+ const args = parseArguments(argsRaw)
425
+ const code = typeof args?.code === 'string' ? args.code : argsRaw
426
+ const explicitDescription = typeof args?.description === 'string' && args.description.length > 0
427
+ ? args.description
428
+ : undefined
429
+ const generatedDescription = settled && isRecord(block.meta)
430
+ && typeof block.meta[GENERATED_RUN_CODE_DESCRIPTION_KEY] === 'string'
431
+ && block.meta[GENERATED_RUN_CODE_DESCRIPTION_KEY].length > 0
432
+ ? block.meta[GENERATED_RUN_CODE_DESCRIPTION_KEY]
433
+ : undefined
434
+ const description = (explicitDescription ?? generatedDescription)?.split(/\r?\n/, 1)[0] ?? ''
435
+ const output = settled ? resultText(block) : ''
436
+ const state = !settled ? 'running'
437
+ : block.error?.code === 'interrupted' ? 'stopped'
438
+ : block.isError === true ? 'error' : 'ok'
439
+ const journal = settled && isRecord(block.meta) && isCountableJournal(block.meta.dshPtcPlus)
440
+ ? block.meta.dshPtcPlus
441
+ : undefined
442
+ if (journal === undefined) {
443
+ return Object.freeze({ state, description, code, output, ptc: false, features: Object.freeze([]) })
444
+ }
445
+ const rewrites = isValidRewrites(block.meta.dshPtcPlusRewrites, journal)
446
+ ? block.meta.dshPtcPlusRewrites
447
+ : []
448
+ const resolvedToolName = typeof toolName === 'string'
449
+ ? toolName
450
+ : typeof block.call?.name === 'string' ? block.call.name : ''
451
+ const recordedToolName = typeof block.call?.name === 'string' ? block.call.name : undefined
452
+ const safeEdit = resolvedToolName === 'edit_run_code'
453
+ && (recordedToolName === undefined || recordedToolName === 'edit_run_code')
454
+ && isValidEditRelation(block.meta, args, journal)
455
+ const features = uniqueFeatures([
456
+ safeEdit
457
+ ? { key: 'feature.safeEdit', detail: '' }
458
+ : undefined,
459
+ rewriteFeature(rewrites, rewrite => rewrite.kind === 'import', 'autoRewriteImports.label'),
460
+ rewriteFeature(rewrites, rewrite => rewrite.kind === 'export', 'autoStripExports.label'),
461
+ rewriteFeature(rewrites, rewrite => rewrite.kind === 'redeclaration'
462
+ && rewrite.description === MIXED_REDECLARATION, 'autoSplitRedeclarations.label'),
463
+ journal.calls.some(call => call.global === 'code' && call.member === 'run' && call.ok === true)
464
+ ? { key: 'feature.codeRun', detail: '' }
465
+ : undefined,
466
+ journal.diagnostics.some(isDurableRecoveryDiagnostic)
467
+ ? { key: 'durableReplay.label', detail: '' }
468
+ : undefined,
469
+ ...operationFeatures(journal.operations),
470
+ ])
471
+ return Object.freeze({ state, description, code, output, ptc: true, features: Object.freeze(features) })
472
+ }