dsh-ptc-plus 0.2.4 → 0.3.2

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