dsh-ptc-plus 0.5.2 → 0.5.3
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 +3 -1
- package/README.md +3 -1
- package/client.cjs +165 -29
- package/docs/adr/0017-track-the-latest-dsh-public-surface.md +4 -2
- package/docs/adr/0019-plugin-settings-and-kill-switch.md +2 -2
- package/docs/adr/0023-global-user-bindings.md +12 -0
- package/docs/adr/0028-attach-to-the-host-ptc-execution-seam.md +1 -1
- package/docs/adr/0030-serve-both-typert-codec-generations-from-one-codec.md +2 -2
- package/docs/adr/0044-coexist-with-the-official-persistent-repl.md +4 -4
- package/docs/adr/0046-gate-the-commit-with-the-semantic-evidence-tier.md +1 -1
- package/docs/adr/0048-supersede-a-rewritten-cell.md +48 -0
- package/docs/adr/0049-prove-a-settlement-against-its-published-record.md +53 -0
- package/docs/adr/0050-read-host-surfaces-through-their-published-contract.md +48 -0
- package/docs/adr/README.md +7 -1
- package/docs/architecture.md +1 -1
- package/docs/durability-design.md +9 -2
- package/docs/installation.md +9 -9
- package/docs/releases/v0.5.2.md +26 -14
- package/docs/releases/v0.5.3.md +77 -0
- package/docs/runtime-reference.md +7 -1
- package/docs/user-bindings.en.md +15 -0
- package/docs/user-bindings.md +15 -0
- package/docs/v0.5.3-goals.md +117 -0
- package/docs/verification.md +4 -4
- package/index.js +67 -19
- package/internal/binding-adoption.js +136 -0
- package/internal/capability-command-receipt.js +18 -4
- package/internal/capability-delivery-proof.js +70 -0
- package/internal/capability-environment.js +38 -1
- package/internal/config-spec.js +14 -5
- package/internal/cordis-tools-owner.js +2 -1
- package/internal/direct-surface-owner.js +27 -13
- package/internal/edit-transport-owner.js +6 -5
- package/internal/execution-provider-owner.js +6 -0
- package/internal/execution-seam-compat.js +25 -0
- package/internal/host-tool-sdk.js +308 -0
- package/internal/official-repl-priority.js +31 -10
- package/internal/program-bindings.js +12 -1
- package/internal/repl-memory-projection.js +21 -2
- package/internal/runtime-bridge-owner.js +113 -8
- package/internal/runtime-messages.js +5 -0
- package/internal/session-journal-recovery.js +210 -32
- package/internal/session-journal-schema.js +3 -0
- package/internal/session-journal.js +46 -9
- package/internal/session-log-view.js +22 -2
- package/internal/session-runtime.js +106 -39
- package/internal/user-bindings-owner.js +109 -6
- package/package.json +15 -7
- package/src/client-capabilities-view.js +45 -3
- package/src/client-copy.js +24 -0
- package/src/client-indicator-view.js +2 -1
- package/src/client-repl-view.js +35 -16
- package/src/client.js +3 -1
|
@@ -9,6 +9,9 @@ import {
|
|
|
9
9
|
RECOVERY_BOUNDARY_EVENT,
|
|
10
10
|
DIAGNOSTIC_WORDING_KEY,
|
|
11
11
|
DIAGNOSTIC_WORDING_FIELDS,
|
|
12
|
+
SETTLEMENT_PROOF_KEY,
|
|
13
|
+
SETTLEMENT_PROOF_FIELDS,
|
|
14
|
+
SETTLEMENT_PROOF_VERSION,
|
|
12
15
|
LEGACY_JOURNAL_VERSION,
|
|
13
16
|
REPL_TOOL_NAMES,
|
|
14
17
|
REWRITES_KEY,
|
|
@@ -23,6 +26,7 @@ import {
|
|
|
23
26
|
normalizeJournal,
|
|
24
27
|
normalizeRecoveryBoundaries,
|
|
25
28
|
reduceStateOperations,
|
|
29
|
+
settlementProofDigest,
|
|
26
30
|
userBindingsForJournal,
|
|
27
31
|
validatedRewrites,
|
|
28
32
|
} from './session-journal.js'
|
|
@@ -104,14 +108,13 @@ function reachableFrom(nodes, head) {
|
|
|
104
108
|
}
|
|
105
109
|
|
|
106
110
|
/**
|
|
107
|
-
* Binding names a
|
|
111
|
+
* Binding names a settlement journal recorded.
|
|
108
112
|
*
|
|
109
113
|
* The recorded values are the only evidence that names a durable binding today;
|
|
110
114
|
* `unrecordedBindings` names the ones the codec could not transfer, and a
|
|
111
115
|
* `kind: "binding"` save names one explicitly. A checkpoint save never counts.
|
|
112
116
|
*/
|
|
113
|
-
function
|
|
114
|
-
const journal = node?.journal
|
|
117
|
+
function journalBindingNames(journal) {
|
|
115
118
|
if (journal === undefined) return []
|
|
116
119
|
return [...new Set([
|
|
117
120
|
...(journal.values ?? []).map(value => value.name),
|
|
@@ -120,6 +123,11 @@ function recordedBindingNames(node) {
|
|
|
120
123
|
].filter(name => typeof name === 'string'))]
|
|
121
124
|
}
|
|
122
125
|
|
|
126
|
+
/** Binding names an installed node recorded. */
|
|
127
|
+
function recordedBindingNames(node) {
|
|
128
|
+
return journalBindingNames(node?.journal)
|
|
129
|
+
}
|
|
130
|
+
|
|
123
131
|
function applyOperations(state, operations, nodeIndex) {
|
|
124
132
|
const previousHead = state.head
|
|
125
133
|
const transition = reduceStateOperations(state, operations, nodeIndex)
|
|
@@ -145,6 +153,117 @@ function applyOperations(state, operations, nodeIndex) {
|
|
|
145
153
|
}
|
|
146
154
|
}
|
|
147
155
|
|
|
156
|
+
/**
|
|
157
|
+
* Adopt the relation a record states about the cell it revised.
|
|
158
|
+
*
|
|
159
|
+
* The relation belongs to the fold, because only the fold knows the path that
|
|
160
|
+
* builds the environment. An accepted edit claims the revision *replaces* the cell
|
|
161
|
+
* it edited, so the revision takes that cell's place in the recorded order: it
|
|
162
|
+
* continues from the target's parent and every cell that followed the target
|
|
163
|
+
* continues from the revision. Appending the revision after those cells instead
|
|
164
|
+
* leaves them replaying an environment the edit already rewrote, with the
|
|
165
|
+
* declarations they read gone and the revision not yet in place.
|
|
166
|
+
*
|
|
167
|
+
* `installed` says whether this settlement puts a cell on the path. A contracted
|
|
168
|
+
* revision installs nothing, so the cells that followed the target bridge to the
|
|
169
|
+
* target's parent, and only a head that was the target itself has to move.
|
|
170
|
+
*
|
|
171
|
+
* The returned record of what changed is what lets a rejected settlement put the
|
|
172
|
+
* path back: nothing this adoption did survives a settlement the fold refuses.
|
|
173
|
+
*/
|
|
174
|
+
function adoptSupersession(state, record, { installed }) {
|
|
175
|
+
const superseded = state.editRewritesHistory && record.supersedes !== undefined
|
|
176
|
+
? state.nodes.findIndex(node => node.callSeq === record.supersedes) : -1
|
|
177
|
+
const adoption = {
|
|
178
|
+
superseded,
|
|
179
|
+
previousHead: state.head,
|
|
180
|
+
supersededBindings: new Set(state.supersededBindings),
|
|
181
|
+
supersessions: state.supersessions.length,
|
|
182
|
+
children: [],
|
|
183
|
+
parent: state.head,
|
|
184
|
+
head: undefined,
|
|
185
|
+
}
|
|
186
|
+
if (superseded < 0) {
|
|
187
|
+
// No relation: the settlement appends to the head, as it always has.
|
|
188
|
+
adoption.head = installed ? undefined : state.head
|
|
189
|
+
return adoption
|
|
190
|
+
}
|
|
191
|
+
const parent = state.nodes[superseded].parent
|
|
192
|
+
adoption.parent = parent
|
|
193
|
+
for (let child = 0; child < state.nodes.length; child += 1) {
|
|
194
|
+
if (state.nodes[child].parent === superseded) adoption.children.push(child)
|
|
195
|
+
}
|
|
196
|
+
// The head is the last recorded cell. It keeps its place when the target is on
|
|
197
|
+
// its path behind other cells, because those cells now reach the same head
|
|
198
|
+
// through the revision; otherwise the revision is the newest cell and the head.
|
|
199
|
+
const targetOnPath = reachableFrom(state.nodes, state.head).has(superseded)
|
|
200
|
+
if (installed) adoption.head = targetOnPath && state.head !== superseded ? state.head : undefined
|
|
201
|
+
else adoption.head = state.head === superseded ? parent : state.head
|
|
202
|
+
return adoption
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Record the relation as the fact the model is owed.
|
|
207
|
+
*
|
|
208
|
+
* Only names the record proves count. A durable declaration that the replay
|
|
209
|
+
* re-establishes is not evidence about the superseded cell, and claiming it
|
|
210
|
+
* would report a binding this fold cannot see. What the revision supplies is the
|
|
211
|
+
* caller's answer, because a settlement the fold refused to install supplies
|
|
212
|
+
* nothing; a cell the fold installs supplies the names its journal recorded.
|
|
213
|
+
*/
|
|
214
|
+
function recordSupersession(state, record, adoption, suppliedNames) {
|
|
215
|
+
if (adoption.superseded < 0) return
|
|
216
|
+
const supplied = new Set(suppliedNames)
|
|
217
|
+
for (const name of recordedBindingNames(state.nodes[adoption.superseded])) {
|
|
218
|
+
if (!supplied.has(name)) state.supersededBindings.add(name)
|
|
219
|
+
}
|
|
220
|
+
state.supersessions.push(Object.freeze({
|
|
221
|
+
supersededCallSeq: record.supersedes,
|
|
222
|
+
supersedingCallSeq: record.call.seq,
|
|
223
|
+
}))
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Fold one accepted record as a settlement of the recorded path.
|
|
228
|
+
*
|
|
229
|
+
* `installed` is false for a settlement the fold contracts: it puts no cell on the
|
|
230
|
+
* path and supplies no name, while the relation it states is still the log's fact.
|
|
231
|
+
* Either way the relation is adopted first and the state operations are applied
|
|
232
|
+
* last, so a settlement that throws leaves the path, the children it re-parented
|
|
233
|
+
* and the relation it recorded exactly as it found them.
|
|
234
|
+
*/
|
|
235
|
+
function applyAdoptedRecord(state, record, { journal, installed = true, restoredFromRecord = false }) {
|
|
236
|
+
const adoption = adoptSupersession(state, record, { installed })
|
|
237
|
+
const index = installed
|
|
238
|
+
? state.nodes.push(Object.freeze({
|
|
239
|
+
code: record.code,
|
|
240
|
+
journal,
|
|
241
|
+
callSeq: record.call.seq,
|
|
242
|
+
parent: adoption.parent,
|
|
243
|
+
...(restoredFromRecord ? { restoredFromRecord: true } : {}),
|
|
244
|
+
...(record.result.userBindings === undefined ? {} : { userBindings: record.result.userBindings }),
|
|
245
|
+
})) - 1
|
|
246
|
+
: undefined
|
|
247
|
+
const inherited = installed ? index : adoption.parent
|
|
248
|
+
for (const child of adoption.children) {
|
|
249
|
+
state.nodes[child] = Object.freeze({ ...state.nodes[child], parent: inherited })
|
|
250
|
+
}
|
|
251
|
+
state.head = adoption.head === undefined ? index : adoption.head
|
|
252
|
+
try {
|
|
253
|
+
recordSupersession(state, record, adoption, installed ? journalBindingNames(journal) : [])
|
|
254
|
+
applyOperations(state, journal.operations, index)
|
|
255
|
+
} catch (error) {
|
|
256
|
+
if (index !== undefined) state.nodes.pop()
|
|
257
|
+
state.head = adoption.previousHead
|
|
258
|
+
for (const child of adoption.children) {
|
|
259
|
+
state.nodes[child] = Object.freeze({ ...state.nodes[child], parent: adoption.superseded })
|
|
260
|
+
}
|
|
261
|
+
state.supersededBindings = adoption.supersededBindings
|
|
262
|
+
state.supersessions.length = adoption.supersessions
|
|
263
|
+
throw error
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
148
267
|
/**
|
|
149
268
|
* Binding names a recorded journal operation list saves.
|
|
150
269
|
*
|
|
@@ -212,17 +331,10 @@ function applyRecord(state, record, invalidCallSeqs) {
|
|
|
212
331
|
// the owner that read `Date.now()` or `process.pid` is gone, and the next
|
|
213
332
|
// environment inherits the durable computation, not the live cache.
|
|
214
333
|
if (!state.liveOnlyLost && journal.values !== undefined && journal.values.length > 0) {
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
parent: state.head,
|
|
220
|
-
restoredFromRecord: true,
|
|
221
|
-
...(result.userBindings === undefined ? {} : { userBindings: result.userBindings }),
|
|
222
|
-
})
|
|
223
|
-
const index = state.nodes.push(node) - 1
|
|
224
|
-
state.head = index
|
|
225
|
-
applyOperations(state, journal.operations, index)
|
|
334
|
+
// The relation is adopted for this settlement too: an accepted revision that
|
|
335
|
+
// settled volatile is still the revision the record names, so it replaces its
|
|
336
|
+
// target on the path and installs its own recorded values.
|
|
337
|
+
applyAdoptedRecord(state, record, { journal, restoredFromRecord: true })
|
|
226
338
|
return
|
|
227
339
|
}
|
|
228
340
|
// A released live environment is reported as a contracted cell: the model
|
|
@@ -236,29 +348,22 @@ function applyRecord(state, record, invalidCallSeqs) {
|
|
|
236
348
|
: journal.volatileReason ?? 'volatile cell',
|
|
237
349
|
...(volatileBindings.length === 0 || state.liveOnlyLost === true ? {} : { bindings: volatileBindings }),
|
|
238
350
|
})
|
|
239
|
-
|
|
351
|
+
// A contracted revision still replaces the cell it edited. Leaving the target
|
|
352
|
+
// on the path would keep building an environment the accepted edit already
|
|
353
|
+
// ended, and the relation is what tells the model those names are withdrawn
|
|
354
|
+
// rather than lost. The settlement installs nothing, so it supplies no name:
|
|
355
|
+
// every name the target recorded is withdrawn by this record.
|
|
356
|
+
applyAdoptedRecord(state, record, { journal, installed: false })
|
|
240
357
|
return
|
|
241
358
|
}
|
|
242
359
|
if (!state.trusted && !state.surfaceContracted) {
|
|
243
360
|
state.trusted = true
|
|
244
361
|
state.volatileSuffix.length = 0
|
|
245
362
|
}
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
parent: state.head,
|
|
251
|
-
...(result.userBindings === undefined ? {} : { userBindings: result.userBindings }),
|
|
252
|
-
})
|
|
253
|
-
const index = state.nodes.push(node) - 1
|
|
254
|
-
state.head = index
|
|
255
|
-
try {
|
|
256
|
-
applyOperations(state, journal.operations, index)
|
|
257
|
-
} catch (error) {
|
|
258
|
-
state.nodes.pop()
|
|
259
|
-
state.head = node.parent
|
|
260
|
-
throw error
|
|
261
|
-
}
|
|
363
|
+
// A record may declare that it is the revision of an earlier cell. Adoption of
|
|
364
|
+
// that relation, its re-parented children and the node this record puts on the
|
|
365
|
+
// path are one settlement: a record the fold refuses leaves none of them behind.
|
|
366
|
+
applyAdoptedRecord(state, record, { journal })
|
|
262
367
|
}
|
|
263
368
|
|
|
264
369
|
function recordEventSeq(record) {
|
|
@@ -277,9 +382,15 @@ function foldRecords(records, invalidCallSeqs, options = {}) {
|
|
|
277
382
|
volatileSuffix: [],
|
|
278
383
|
provenBindings: new Set(),
|
|
279
384
|
deletedBindings: new Set(),
|
|
385
|
+
// One recorded revision replaced the cell it edited. The relation, not a
|
|
386
|
+
// deletion, is what the log proves: the superseded cell stays in the record
|
|
387
|
+
// and off the path to the head.
|
|
388
|
+
supersessions: [],
|
|
389
|
+
supersededBindings: new Set(),
|
|
280
390
|
trusted: true,
|
|
281
391
|
surfaceContracted: options.surfaceContracted === true,
|
|
282
392
|
liveOnlyLost: options.liveOnlyLost === true,
|
|
393
|
+
editRewritesHistory: options.editRewritesHistory === true,
|
|
283
394
|
}
|
|
284
395
|
for (const record of records) {
|
|
285
396
|
applyRecord(state, record, invalidCallSeqs)
|
|
@@ -1459,6 +1570,12 @@ function normalizePostEditFact(facts, call, resultEvent, result, replayPostEdit,
|
|
|
1459
1570
|
let derived
|
|
1460
1571
|
try {
|
|
1461
1572
|
derived = normalizeDeferredEditSettlement(post.meta, target?.callSeq)
|
|
1573
|
+
// The derived metadata is a published settlement of its own, so it carries the
|
|
1574
|
+
// same proof the root journal does. Comparing the two recorded journal copies
|
|
1575
|
+
// proves only that they agree with each other; an edit that changed both and
|
|
1576
|
+
// left this proof behind no longer reproduces the settlement it published, and
|
|
1577
|
+
// its journal is then not evidence for the bindings the revision declares.
|
|
1578
|
+
requireRecordedSettlementProof(post.meta, derived.journal)
|
|
1462
1579
|
} catch (error) {
|
|
1463
1580
|
return fail(error.message)
|
|
1464
1581
|
}
|
|
@@ -1517,6 +1634,40 @@ function requireRecordedDiagnosticWording(meta, journal) {
|
|
|
1517
1634
|
}
|
|
1518
1635
|
}
|
|
1519
1636
|
|
|
1637
|
+
/**
|
|
1638
|
+
* A published settlement proves its own facts, and the Host's copy of its outcome
|
|
1639
|
+
* must still agree with them.
|
|
1640
|
+
*
|
|
1641
|
+
* Recovery reads a record as evidence for the bindings its cell declared, and the
|
|
1642
|
+
* record is the only account of its cell: editing the status, the completion or a
|
|
1643
|
+
* diagnostic leaves the record self-consistent, because the completion is rendered
|
|
1644
|
+
* from the same diagnostic that was edited. The proof recorded at publication is
|
|
1645
|
+
* one digest over exactly those facts, so an edited record no longer reproduces it.
|
|
1646
|
+
* A record that carries no proof predates it and keeps the older evidence.
|
|
1647
|
+
*
|
|
1648
|
+
* The retained `isError` is the Host's copy of the same outcome and not a fact this
|
|
1649
|
+
* plugin can recompute, so an edit that also rewrites the proof still has to agree
|
|
1650
|
+
* with it. That relation is the one a deferred edit already proves before it is
|
|
1651
|
+
* published.
|
|
1652
|
+
*/
|
|
1653
|
+
function requireRecordedSettlementProof(meta, journal, isError = undefined) {
|
|
1654
|
+
if (isError !== undefined && isError !== (journal.status === 'discarded'
|
|
1655
|
+
|| journal.completion?.kind === 'throw')) {
|
|
1656
|
+
throw new Error('journal settlement does not agree with the tool result it settled')
|
|
1657
|
+
}
|
|
1658
|
+
if (!isRecord(meta) || !Object.hasOwn(meta, SETTLEMENT_PROOF_KEY)) return
|
|
1659
|
+
const recorded = meta[SETTLEMENT_PROOF_KEY]
|
|
1660
|
+
if (!isRecord(recorded) || Reflect.ownKeys(recorded).length !== SETTLEMENT_PROOF_FIELDS.size
|
|
1661
|
+
|| Reflect.ownKeys(recorded).some(key => !SETTLEMENT_PROOF_FIELDS.has(key))
|
|
1662
|
+
|| recorded.version !== SETTLEMENT_PROOF_VERSION
|
|
1663
|
+
|| typeof recorded.recordedMessage !== 'string') {
|
|
1664
|
+
throw new Error('invalid recorded settlement proof')
|
|
1665
|
+
}
|
|
1666
|
+
if (recorded.recordedMessage !== settlementProofDigest(journal)) {
|
|
1667
|
+
throw new Error('journal facts do not match their recorded settlement proof')
|
|
1668
|
+
}
|
|
1669
|
+
}
|
|
1670
|
+
|
|
1520
1671
|
function advanceTimelineEvent(previous, inputEvent, eventIndex, replayFact, retainedCalls) {
|
|
1521
1672
|
const nested = inputEvent?.type === 'tool/ptc-dispatch-start'
|
|
1522
1673
|
|| inputEvent?.type === 'tool/ptc-dispatch'
|
|
@@ -1767,6 +1918,7 @@ function advanceTimelineEvent(previous, inputEvent, eventIndex, replayFact, reta
|
|
|
1767
1918
|
...(derived.userBindings === undefined ? {} : { userBindings: derived.userBindings }),
|
|
1768
1919
|
}
|
|
1769
1920
|
requireRecordedDiagnosticWording(meta, derived.journal)
|
|
1921
|
+
requireRecordedSettlementProof(meta, derived.journal)
|
|
1770
1922
|
} else if (call.data.name === 'run_code') {
|
|
1771
1923
|
const rawJournal = meta[JOURNAL_KEY]
|
|
1772
1924
|
const resolveLegacyConfirm = rawJournal?.version === LEGACY_JOURNAL_VERSION
|
|
@@ -1800,6 +1952,15 @@ function advanceTimelineEvent(previous, inputEvent, eventIndex, replayFact, reta
|
|
|
1800
1952
|
...(userBindings === undefined ? {} : { userBindings }),
|
|
1801
1953
|
}
|
|
1802
1954
|
requireRecordedDiagnosticWording(meta, journal)
|
|
1955
|
+
// The Host states the outcome of the cell it settled; a record whose
|
|
1956
|
+
// completion or status contradicts it was edited after publication. The
|
|
1957
|
+
// Host records that outcome on the result message it appends, so the
|
|
1958
|
+
// message is read first; a log that carries it on the event instead is
|
|
1959
|
+
// read the same way.
|
|
1960
|
+
const hostIsError = typeof event.data?.message?.isError === 'boolean'
|
|
1961
|
+
? event.data.message.isError
|
|
1962
|
+
: typeof event.data?.isError === 'boolean' ? event.data.isError : undefined
|
|
1963
|
+
requireRecordedSettlementProof(meta, journal, hostIsError)
|
|
1803
1964
|
let postEdit
|
|
1804
1965
|
try {
|
|
1805
1966
|
postEdit = normalizePostEditFact(state.eventFacts, call, event, normalized, undefined, state.executableCalls)
|
|
@@ -2299,7 +2460,8 @@ export function recoverJournal(session, currentCallSeq, options = {}) {
|
|
|
2299
2460
|
// A live environment that was released cannot lend its recorded live-only
|
|
2300
2461
|
// values to the environment that replaces it. The flag belongs to the fold, so
|
|
2301
2462
|
// every re-fold below carries it.
|
|
2302
|
-
const foldOptions = { liveOnlyLost: options?.liveOnlyLost === true
|
|
2463
|
+
const foldOptions = { liveOnlyLost: options?.liveOnlyLost === true,
|
|
2464
|
+
editRewritesHistory: options?.editRewritesHistory === true }
|
|
2303
2465
|
let state = foldRecords(records, invalidCallSeqs, foldOptions)
|
|
2304
2466
|
let boundaryIndex = 0
|
|
2305
2467
|
const carrierCallSeqForBoundary = boundary => boundary.carrierCallSeq
|
|
@@ -2415,6 +2577,9 @@ export function recoverJournal(session, currentCallSeq, options = {}) {
|
|
|
2415
2577
|
code: postEdit.derived.code,
|
|
2416
2578
|
result: { eventSeq: result.eventSeq, journal: postEdit.derived.journal,
|
|
2417
2579
|
derived: postEdit.derived, userBindings: postEdit.derived.userBindings },
|
|
2580
|
+
// The revision names the cell it edits. Whether that relation replaces
|
|
2581
|
+
// the target or appends to it is the fold's decision, not the log's.
|
|
2582
|
+
supersedes: postEdit.targetCallSeq,
|
|
2418
2583
|
hidden: visibleCalls instanceof Set && (!visibleCalls.has(call.seq)
|
|
2419
2584
|
|| !visibleCalls.has(postEdit.targetCallSeq)),
|
|
2420
2585
|
})
|
|
@@ -2482,6 +2647,19 @@ export function recoverJournal(session, currentCallSeq, options = {}) {
|
|
|
2482
2647
|
enumerable: false,
|
|
2483
2648
|
configurable: true,
|
|
2484
2649
|
})
|
|
2650
|
+
// The supersession relation is recovery evidence of the same kind: which cell
|
|
2651
|
+
// each revision replaced, and which proved names the path no longer supplies
|
|
2652
|
+
// because of it. Both stay off the enumerable shape for the same reason.
|
|
2653
|
+
Object.defineProperty(history, 'supersessions', {
|
|
2654
|
+
value: [...state.supersessions],
|
|
2655
|
+
enumerable: false,
|
|
2656
|
+
configurable: true,
|
|
2657
|
+
})
|
|
2658
|
+
Object.defineProperty(history, 'supersededBindings', {
|
|
2659
|
+
value: [...state.supersededBindings],
|
|
2660
|
+
enumerable: false,
|
|
2661
|
+
configurable: true,
|
|
2662
|
+
})
|
|
2485
2663
|
return history
|
|
2486
2664
|
}
|
|
2487
2665
|
|
|
@@ -12,6 +12,7 @@ export const POST_EDIT_KEY = 'dshPtcPlusPostEdit'
|
|
|
12
12
|
export const REWRITES_KEY = 'dshPtcPlusRewrites'
|
|
13
13
|
export const RECOVERY_BOUNDARY_KEY = 'dshPtcPlusRecoveryBoundaries'
|
|
14
14
|
export const DIAGNOSTIC_WORDING_KEY = 'dshPtcPlusDiagnosticWording'
|
|
15
|
+
export const SETTLEMENT_PROOF_KEY = 'dshPtcPlusSettlementProof'
|
|
15
16
|
export const IMPORT_BOUNDARY_JOURNAL_VERSION = 7
|
|
16
17
|
export const PER_NAME_USER_BINDINGS_JOURNAL_VERSION = 8
|
|
17
18
|
export const LANGUAGE_SEMANTICS_JOURNAL_VERSION = 9
|
|
@@ -62,6 +63,8 @@ export const ERROR_FIELDS = new Set(['kind', 'message'])
|
|
|
62
63
|
export const EDIT_TARGET_FIELDS = new Set(['targetCallSeq'])
|
|
63
64
|
export const DERIVED_RUN_FIELDS = new Set(['code', 'codeDigest', 'codeBytes', 'description'])
|
|
64
65
|
export const DIAGNOSTIC_WORDING_FIELDS = new Set(['digest'])
|
|
66
|
+
export const SETTLEMENT_PROOF_VERSION = 1
|
|
67
|
+
export const SETTLEMENT_PROOF_FIELDS = new Set(['version', 'recordedMessage'])
|
|
65
68
|
|
|
66
69
|
export const JOURNAL_VERSIONS = new Set([LEGACY_JOURNAL_VERSION, INTERMEDIATE_JOURNAL_VERSION, PREVIOUS_JOURNAL_VERSION, USER_BINDING_RELATIONLESS_JOURNAL_VERSION, FINGERPRINT_REUSE_JOURNAL_VERSION, VERSIONED_BINDING_REUSE_JOURNAL_VERSION, IMPORT_BOUNDARY_JOURNAL_VERSION, PER_NAME_USER_BINDINGS_JOURNAL_VERSION, LANGUAGE_SEMANTICS_JOURNAL_VERSION, MODULE_TRANSFORM_JOURNAL_VERSION, JOURNAL_VERSION])
|
|
67
70
|
export const USER_BINDINGS_REUSE_POLICIES = new Set([LEGACY_USER_BINDINGS_REUSE_POLICY, LIVE_USER_BINDINGS_REUSE_POLICY])
|
|
@@ -64,6 +64,9 @@ import {
|
|
|
64
64
|
USER_BINDINGS_REUSE_POLICIES,
|
|
65
65
|
RECOVERY_BOUNDARY_FIELDS,
|
|
66
66
|
DIAGNOSTIC_WORDING_KEY,
|
|
67
|
+
SETTLEMENT_PROOF_KEY,
|
|
68
|
+
SETTLEMENT_PROOF_FIELDS,
|
|
69
|
+
SETTLEMENT_PROOF_VERSION,
|
|
67
70
|
REPL_TOOL_NAMES,
|
|
68
71
|
REWRITE_FIELDS,
|
|
69
72
|
REWRITE_KINDS,
|
|
@@ -78,6 +81,9 @@ export {
|
|
|
78
81
|
RECOVERY_BOUNDARY_KEY,
|
|
79
82
|
DIAGNOSTIC_WORDING_KEY,
|
|
80
83
|
DIAGNOSTIC_WORDING_FIELDS,
|
|
84
|
+
SETTLEMENT_PROOF_KEY,
|
|
85
|
+
SETTLEMENT_PROOF_FIELDS,
|
|
86
|
+
SETTLEMENT_PROOF_VERSION,
|
|
81
87
|
JOURNAL_VERSION,
|
|
82
88
|
LIVE_USER_BINDINGS_REUSE_POLICY,
|
|
83
89
|
RECOVERY_BOUNDARY_EVENT,
|
|
@@ -129,15 +135,46 @@ export function diagnosticWordingDigest(journal) {
|
|
|
129
135
|
return digestOf(diagnostics.map(diagnosticWordingFacts))
|
|
130
136
|
}
|
|
131
137
|
|
|
132
|
-
/**
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
138
|
+
/**
|
|
139
|
+
* Digest of the one settlement a journal states: the status of its cell, its
|
|
140
|
+
* completion, and the wording of its diagnostics.
|
|
141
|
+
*
|
|
142
|
+
* A published record is the only account of its own cell, so a record edited
|
|
143
|
+
* afterwards stays self-consistent: the completion is checkable against a
|
|
144
|
+
* diagnostic, and the diagnostic against the wording digest, but all three are
|
|
145
|
+
* the same settlement. The proof is taken at publication, from the facts the
|
|
146
|
+
* settlement was published with, and every fact is represented here exactly once,
|
|
147
|
+
* so a record whose status, completion or diagnostic wording changed no longer
|
|
148
|
+
* reproduces it.
|
|
149
|
+
*/
|
|
150
|
+
export function settlementProofDigest(journal) {
|
|
151
|
+
const completion = journal?.completion
|
|
152
|
+
const completionFacts = completion === undefined ? null
|
|
153
|
+
: completion.kind === 'return'
|
|
154
|
+
? [completion.kind, completion.hasValue,
|
|
155
|
+
completion.hasValue ? digestOf(completion.value) : null]
|
|
156
|
+
: [completion.kind, completion.error.kind, completion.error.message]
|
|
157
|
+
return digestOf([
|
|
158
|
+
journal?.status ?? null,
|
|
159
|
+
completionFacts,
|
|
160
|
+
diagnosticWordingDigest(journal) ?? null,
|
|
161
|
+
])
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Attach the published settlement's proof beside a journal.
|
|
166
|
+
*
|
|
167
|
+
* Every settlement states a status, so every publication carries one; a record
|
|
168
|
+
* that carries none predates this proof and keeps the older evidence.
|
|
169
|
+
*/
|
|
170
|
+
export function withSettlementProof(meta, journal) {
|
|
171
|
+
return {
|
|
172
|
+
...meta,
|
|
173
|
+
[SETTLEMENT_PROOF_KEY]: {
|
|
174
|
+
version: SETTLEMENT_PROOF_VERSION,
|
|
175
|
+
recordedMessage: settlementProofDigest(journal),
|
|
176
|
+
},
|
|
139
177
|
}
|
|
140
|
-
return { ...meta, [DIAGNOSTIC_WORDING_KEY]: { digest } }
|
|
141
178
|
}
|
|
142
179
|
|
|
143
180
|
function cloneJson(value) {
|
|
@@ -1272,7 +1309,7 @@ export function withJournal(meta, journal) {
|
|
|
1272
1309
|
const base = isRecord(meta) ? { ...meta } : meta === undefined ? {} : { value: cloneJson(meta) }
|
|
1273
1310
|
const normalized = normalizeJournal(journal)
|
|
1274
1311
|
base[JOURNAL_KEY] = normalized
|
|
1275
|
-
return
|
|
1312
|
+
return withSettlementProof(base, normalized)
|
|
1276
1313
|
}
|
|
1277
1314
|
|
|
1278
1315
|
/** Validate and detach one rewrite record emitted by the runtime. */
|
|
@@ -249,6 +249,24 @@ export function createSessionLogProjection(options = {}) {
|
|
|
249
249
|
})
|
|
250
250
|
}
|
|
251
251
|
|
|
252
|
+
/**
|
|
253
|
+
* The disposer a session projection registration handed back.
|
|
254
|
+
*
|
|
255
|
+
* Two registration contracts are published: an earlier host returns a disposer and a
|
|
256
|
+
* newer one returns a `ProjectionHandle` (`stateOf`/`readerFor`/`dispose`, possibly
|
|
257
|
+
* async). Every projection this plugin registers goes through this reader, because a
|
|
258
|
+
* registration that only understands the earlier shape drops the projection — and
|
|
259
|
+
* with it the fact that the host accepted it — on the newer contract.
|
|
260
|
+
* @param {unknown} registration - what `sessionProjections.register` returned.
|
|
261
|
+
* @returns {(() => unknown)|undefined} the release call, or undefined when the answer
|
|
262
|
+
* names neither contract.
|
|
263
|
+
*/
|
|
264
|
+
export function projectionRegistrationDisposer(registration) {
|
|
265
|
+
if (typeof registration === 'function') return () => registration()
|
|
266
|
+
if (typeof registration?.dispose === 'function') return () => registration.dispose()
|
|
267
|
+
return undefined
|
|
268
|
+
}
|
|
269
|
+
|
|
252
270
|
export function createSessionLogOwner(ctx) {
|
|
253
271
|
const projection = createSessionLogProjection()
|
|
254
272
|
let registeredService
|
|
@@ -256,8 +274,10 @@ export function createSessionLogOwner(ctx) {
|
|
|
256
274
|
try {
|
|
257
275
|
scope.effect(() => {
|
|
258
276
|
const registry = scope.sessionProjections
|
|
259
|
-
const unregister = registry.register(projection)
|
|
260
|
-
if (
|
|
277
|
+
const unregister = projectionRegistrationDisposer(registry.register(projection))
|
|
278
|
+
if (unregister === undefined) {
|
|
279
|
+
throw new Error('ptc-plus: sessionProjections.register returned neither a disposer nor a handle')
|
|
280
|
+
}
|
|
261
281
|
registeredService = registry
|
|
262
282
|
return async () => {
|
|
263
283
|
await unregister()
|