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.
- package/README.en.md +171 -0
- package/README.md +94 -67
- package/assets/dsh-ptc-plus-banner-en.webp +0 -0
- package/assets/ptc-plus-repair-en.png +0 -0
- package/assets/ptc-plus-settings-en.png +0 -0
- package/assets/ptc-plus-settings-zh.png +0 -0
- package/client.js +1211 -128
- package/docs/adr/0019-plugin-settings-and-kill-switch.md +20 -4
- package/docs/adr/0020-optional-cordis-tools-in-ptc-mode.md +2 -2
- package/docs/adr/0022-stage-npm-releases-with-oidc.md +31 -0
- package/docs/architecture.md +10 -9
- package/docs/client-ui.md +30 -12
- package/docs/installation.md +13 -5
- package/docs/publishing.md +25 -1
- package/docs/runtime-reference.md +18 -12
- package/docs/value-wire.md +1 -1
- package/index.js +43 -12
- package/internal/cell-analysis.js +20 -3
- package/internal/cell-rewriter.js +36 -2
- package/internal/config-spec.js +192 -82
- package/internal/direct-surface-owner.js +77 -23
- package/internal/edit-transport-owner.js +10 -1
- package/internal/failure-reporting.js +15 -0
- package/internal/rejected-cell-editor.js +21 -12
- package/internal/repl-memory-projection.js +277 -0
- package/internal/run-code-description.js +34 -0
- package/internal/runtime-bridge-owner.js +63 -15
- package/internal/runtime-config.js +2 -0
- package/internal/session-cell-executor.js +14 -4
- package/internal/session-events.js +7 -0
- package/internal/session-journal.js +3 -2
- package/internal/session-log-view.js +2 -1
- package/internal/session-runtime.js +19 -2
- package/internal/session-state.js +83 -4
- package/internal/settings-compat.js +31 -0
- package/package.json +20 -13
- package/src/client-activity.js +472 -0
- package/src/client.js +536 -38
- package/README.zh.md +0 -144
- package/assets/ptc-plus-settings.png +0 -0
- /package/assets/{dsh-ptc-plus-banner.webp → dsh-ptc-plus-banner-zh.webp} +0 -0
- /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
|
+
}
|