@sublang/playbook 15.1.0 → 17.0.0

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 (102) hide show
  1. package/README.md +2 -0
  2. package/docs/cli.md +36 -18
  3. package/docs/configuration.md +7 -5
  4. package/docs/embedding.md +28 -7
  5. package/package.json +4 -4
  6. package/reference/sdlc/branch.playbook/branch.fsm.d.ts +63 -33
  7. package/reference/sdlc/branch.playbook/branch.fsm.js +190 -108
  8. package/reference/sdlc/branch.playbook/branch.fsm.ts +269 -147
  9. package/reference/sdlc/branch.playbook/branch.gears.md +7 -17
  10. package/reference/sdlc/branch.playbook/branch.playbook.d.ts +12 -16
  11. package/reference/sdlc/branch.playbook/branch.playbook.js +112 -101
  12. package/reference/sdlc/branch.playbook/branch.playbook.ts +128 -171
  13. package/reference/sdlc/branch.playbook/branch.registry.d.ts +3 -2
  14. package/reference/sdlc/branch.playbook/branch.registry.ts +6 -2
  15. package/reference/sdlc/captain.md +19 -10
  16. package/reference/sdlc/captain.playbook/captain.fsm.d.ts +6 -2
  17. package/reference/sdlc/captain.playbook/captain.fsm.js +19 -7
  18. package/reference/sdlc/captain.playbook/captain.fsm.ts +25 -10
  19. package/reference/sdlc/captain.playbook/captain.gears.md +16 -8
  20. package/reference/sdlc/captain.playbook/captain.playbook.d.ts +1 -1
  21. package/reference/sdlc/captain.playbook/captain.playbook.js +3 -2
  22. package/reference/sdlc/captain.playbook/captain.playbook.ts +4 -3
  23. package/reference/sdlc/code.playbook/bin/interactive-session.js +14 -1
  24. package/reference/sdlc/code.playbook/bin/portable-codec.js +3 -0
  25. package/reference/sdlc/code.playbook/bin/repository-effects.js +1 -0
  26. package/reference/sdlc/code.playbook/bin/run.js +128 -85
  27. package/reference/sdlc/code.playbook/bin/session-host.js +40 -8
  28. package/reference/sdlc/code.playbook/bin/session-store.js +162 -34
  29. package/reference/sdlc/code.playbook/code.fsm.d.ts +146 -93
  30. package/reference/sdlc/code.playbook/code.fsm.introspect.d.ts +8 -8
  31. package/reference/sdlc/code.playbook/code.fsm.introspect.ts +13 -13
  32. package/reference/sdlc/code.playbook/code.fsm.js +609 -649
  33. package/reference/sdlc/code.playbook/code.fsm.ts +890 -821
  34. package/reference/sdlc/code.playbook/code.gears.md +39 -29
  35. package/reference/sdlc/code.playbook/code.playbook.d.ts +13 -16
  36. package/reference/sdlc/code.playbook/code.playbook.js +166 -147
  37. package/reference/sdlc/code.playbook/code.playbook.ts +180 -229
  38. package/reference/sdlc/code.playbook/code.registry.d.ts +3 -2
  39. package/reference/sdlc/code.playbook/code.registry.ts +6 -2
  40. package/reference/sdlc/code.playbook/host-capabilities.d.ts +3 -0
  41. package/reference/sdlc/code.playbook/playbook-captain.d.ts +74 -2
  42. package/reference/sdlc/code.playbook/playbook-captain.js +1548 -468
  43. package/reference/sdlc/code.playbook/playbook-captain.ts +1993 -573
  44. package/reference/sdlc/code.playbook/session-host.d.ts +4 -1
  45. package/reference/sdlc/code.playbook/session-store.d.ts +17 -0
  46. package/reference/sdlc/code.playbook/session-store.js +1 -0
  47. package/reference/sdlc/decide.md +1 -0
  48. package/reference/sdlc/decide.playbook/decide.fsm.d.ts +273 -157
  49. package/reference/sdlc/decide.playbook/decide.fsm.js +765 -732
  50. package/reference/sdlc/decide.playbook/decide.fsm.ts +1167 -945
  51. package/reference/sdlc/decide.playbook/decide.gears.md +28 -21
  52. package/reference/sdlc/decide.playbook/decide.playbook.d.ts +11 -59
  53. package/reference/sdlc/decide.playbook/decide.playbook.js +155 -3696
  54. package/reference/sdlc/decide.playbook/decide.playbook.ts +186 -5244
  55. package/reference/sdlc/decide.playbook/decide.registry.d.ts +8 -4
  56. package/reference/sdlc/decide.playbook/decide.registry.js +2 -2
  57. package/reference/sdlc/decide.playbook/decide.registry.ts +13 -6
  58. package/reference/sdlc/dev.playbook/dev.fsm.d.ts +116 -134
  59. package/reference/sdlc/dev.playbook/dev.fsm.js +517 -575
  60. package/reference/sdlc/dev.playbook/dev.fsm.ts +667 -787
  61. package/reference/sdlc/dev.playbook/dev.gears.md +52 -54
  62. package/reference/sdlc/dev.playbook/dev.playbook.d.ts +13 -10
  63. package/reference/sdlc/dev.playbook/dev.playbook.js +128 -120
  64. package/reference/sdlc/dev.playbook/dev.playbook.ts +143 -193
  65. package/reference/sdlc/dev.playbook/dev.registry.d.ts +3 -2
  66. package/reference/sdlc/dev.playbook/dev.registry.ts +6 -2
  67. package/reference/sdlc/pr.playbook/pr.fsm.d.ts +95 -88
  68. package/reference/sdlc/pr.playbook/pr.fsm.js +461 -518
  69. package/reference/sdlc/pr.playbook/pr.fsm.ts +533 -663
  70. package/reference/sdlc/pr.playbook/pr.gears.md +47 -63
  71. package/reference/sdlc/pr.playbook/pr.playbook.d.ts +13 -23
  72. package/reference/sdlc/pr.playbook/pr.playbook.js +118 -112
  73. package/reference/sdlc/pr.playbook/pr.playbook.ts +136 -190
  74. package/reference/sdlc/pr.playbook/pr.registry.d.ts +3 -2
  75. package/reference/sdlc/pr.playbook/pr.registry.ts +4 -2
  76. package/reference/sdlc/review.md +3 -3
  77. package/reference/sdlc/review.playbook/review.fsm.d.ts +150 -116
  78. package/reference/sdlc/review.playbook/review.fsm.js +479 -550
  79. package/reference/sdlc/review.playbook/review.fsm.ts +717 -662
  80. package/reference/sdlc/review.playbook/review.gears.md +32 -29
  81. package/reference/sdlc/review.playbook/review.playbook.d.ts +12 -12
  82. package/reference/sdlc/review.playbook/review.playbook.js +180 -155
  83. package/reference/sdlc/review.playbook/review.playbook.ts +190 -230
  84. package/reference/sdlc/review.playbook/review.registry.d.ts +6 -5
  85. package/reference/sdlc/review.playbook/review.registry.js +3 -3
  86. package/reference/sdlc/review.playbook/review.registry.ts +9 -5
  87. package/slc/gears2fsm.md +77 -8
  88. package/slc/link.md +127 -60
  89. package/slc/materialize-link.mjs +51 -13
  90. package/slc/optimize.md +4 -1
  91. package/slc/prefix-prompts.mjs +29 -28
  92. package/slc/prefix.md +27 -24
  93. package/slc/slc.pin-inputs.json +11 -2
  94. package/slc/text2gears.md +71 -11
  95. package/src/runtime.d.ts +46 -2
  96. package/src/runtime.ts +42 -2
  97. package/src/xstate-playbook-runtime.d.ts +63 -23
  98. package/src/xstate-playbook-runtime.js +1637 -475
  99. package/src/xstate-playbook-runtime.ts +2105 -532
  100. package/src/xstate-runtime.d.ts +3 -4
  101. package/src/xstate-runtime.js +52 -0
  102. package/src/xstate-runtime.ts +59 -4
@@ -2551,24 +2551,21 @@ async function createLease({
2551
2551
  now,
2552
2552
  }) {
2553
2553
  let released = false;
2554
- let operationActive = false;
2554
+ let operationTail = Promise.resolve();
2555
2555
  let indeterminateEffectLedgerWrite;
2556
-
2557
- const requireActive = () => {
2558
- if (released) throw new Error('Captain session lease was already released');
2559
- if (operationActive) {
2560
- throw new Error('Captain session lease operation is already in progress');
2561
- }
2562
- };
2563
-
2564
- const runExclusive = async (operation) => {
2565
- requireActive();
2566
- operationActive = true;
2567
- try {
2568
- return await operation();
2569
- } finally {
2570
- operationActive = false;
2571
- }
2556
+ let releasing = false;
2557
+ const ownerChecks = new Set();
2558
+
2559
+ // All lifecycle, step and repository writes share one order, including
2560
+ // calls from parallel players. A failed write does not poison the queue.
2561
+ const runExclusive = (operation) => {
2562
+ if (releasing) return Promise.reject(new Error('Captain session lease is releasing'));
2563
+ const run = operationTail.then(() => {
2564
+ if (released) throw new Error('Captain session lease was already released');
2565
+ return operation();
2566
+ });
2567
+ operationTail = run.then(() => undefined, () => undefined);
2568
+ return run;
2572
2569
  };
2573
2570
 
2574
2571
  const assertOwnerUnchecked = async () => {
@@ -2580,7 +2577,14 @@ async function createLease({
2580
2577
  return current;
2581
2578
  };
2582
2579
 
2583
- const assertOwner = () => runExclusive(assertOwnerUnchecked);
2580
+ const assertOwner = () => {
2581
+ if (releasing) return Promise.reject(new Error('Captain session lease is releasing'));
2582
+ // This reads only the lease token. Parallel players must not compete
2583
+ // for the mutation guard, but release must still join admitted reads.
2584
+ const check = assertOwnerUnchecked();
2585
+ ownerChecks.add(check);
2586
+ return check.finally(() => ownerChecks.delete(check));
2587
+ };
2584
2588
 
2585
2589
  const replayWriter = await createLeaseReplayWriter({
2586
2590
  sessionsDir,
@@ -3113,6 +3117,7 @@ async function createLease({
3113
3117
  ...settledAbandonmentMember(prior),
3114
3118
  uncertain: {
3115
3119
  baseUpdatedAt: prior.uncertain.baseUpdatedAt,
3120
+ ...(prior.uncertain.progress === undefined ? {} : { progress: prior.uncertain.progress }),
3116
3121
  input: prior.uncertain.input,
3117
3122
  attemptId: nextAttemptId,
3118
3123
  attemptNumber: prior.uncertain.attemptNumber + 1,
@@ -3607,6 +3612,49 @@ async function createLease({
3607
3612
  return record.effectLedger;
3608
3613
  });
3609
3614
 
3615
+ const recordProgress = (value) => runExclusive(async () => {
3616
+ const change = requireRecord(snapshotJsonValue(value, 'Captain progress change'), 'Captain progress change');
3617
+ exactOptionalKeys(change, [], ['snapshot', 'step'], 'Captain progress change');
3618
+ await assertOwnerUnchecked();
3619
+ const prior = await readRecord(sessionId, { missing: 'undefined' });
3620
+ if (prior?.state !== 'uncertain' || prior.uncertain.abandonment) {
3621
+ throw new Error('Saving progress requires an active uncertain turn');
3622
+ }
3623
+ const progress = structuredClone(prior.uncertain.progress ?? { snapshot: null, steps: [], positionStepId: null });
3624
+ const newStepStart = change.step && !Object.hasOwn(change.step, 'result') && !progress.steps.some(({ id }) => id === change.step.id);
3625
+ if (Object.hasOwn(change, 'snapshot')) {
3626
+ if (change.snapshot !== null && !isDeepStrictEqual(change.snapshot.effectLedger, prior.effectLedger)) {
3627
+ throw new Error('Saved progress must contain the current effect ledger');
3628
+ }
3629
+ progress.snapshot = change.snapshot;
3630
+ progress.positionStepId = change.snapshot !== null && newStepStart ? change.step.id : null;
3631
+ }
3632
+ if (change.step !== undefined) {
3633
+ const step = snapshotJsonValue(change.step, 'Captain step');
3634
+ const index = progress.steps.findIndex(({ id }) => id === step.id);
3635
+ if (index < 0) {
3636
+ if (!Object.hasOwn(change, 'snapshot')) throw new Error('A step start requires its position or explicit null');
3637
+ if (Object.hasOwn(step, 'result') && !['completion', 'answer'].includes(step.kind)) throw new Error('A step result requires its saved start');
3638
+ progress.steps.push(step);
3639
+ } else {
3640
+ const { result, ...start } = step;
3641
+ const saved = progress.steps[index];
3642
+ const { result: priorResult, ...priorStart } = saved;
3643
+ if (!isDeepStrictEqual(start, priorStart) ||
3644
+ (priorResult !== undefined && !isDeepStrictEqual(priorResult, result))) {
3645
+ throw new Error('A saved step cannot be replaced');
3646
+ }
3647
+ progress.steps[index] = step;
3648
+ }
3649
+ }
3650
+ const record = validateCaptainSessionRecord(projectRecovery({
3651
+ ...prior, uncertain: { ...prior.uncertain, progress },
3652
+ }));
3653
+ await assertOwnerUnchecked();
3654
+ await writeRecord(record, { noReplace: false });
3655
+ await assertOwnerUnchecked();
3656
+ });
3657
+
3610
3658
  const discard = ({ attemptId } = {}) =>
3611
3659
  runExclusive(async () => {
3612
3660
  assertUuid(attemptId, 'Captain session attempt id');
@@ -3620,10 +3668,8 @@ async function createLease({
3620
3668
  'Captain session unresolved-effect abandonment must recover before discard',
3621
3669
  );
3622
3670
  }
3623
- if (!isDeepStrictEqual(prior.effectLedger, prior.snapshot.effectLedger)) {
3624
- throw new Error(
3625
- 'Captain session uncertain effect ledger differs from its pre-turn checkpoint and cannot be discarded',
3626
- );
3671
+ if (!isUncertainTurnDiscardable(prior)) {
3672
+ throw new Error('Recorded work or changed repository evidence must be restored and reported; the turn cannot be discarded');
3627
3673
  }
3628
3674
  await assertOwnerUnchecked();
3629
3675
  if (prior.uncertain.baseUpdatedAt === null) {
@@ -3655,19 +3701,27 @@ async function createLease({
3655
3701
  });
3656
3702
 
3657
3703
  const release = () => {
3704
+ if (released || releasing) return Promise.reject(new Error(`Captain session lease is ${released ? 'released' : 'releasing'}`));
3658
3705
  replayWriter.closeAppendAdmission();
3659
- return runExclusive(async () => {
3660
- await replayWriter.prepareRelease();
3661
- if (replayWriter.status().incomplete && previousManifest?.replay?.incomplete !== true) {
3662
- const record = await readRecord(sessionId, { missing: 'undefined' });
3663
- if (record !== undefined) await writeRecord(record, { noReplace: false });
3706
+ const pending = runExclusive(async () => {
3707
+ try {
3708
+ await Promise.allSettled([...ownerChecks]);
3709
+ await replayWriter.prepareRelease();
3710
+ if (replayWriter.status().incomplete && previousManifest?.replay?.incomplete !== true) {
3711
+ const record = await readRecord(sessionId, { missing: 'undefined' });
3712
+ if (record !== undefined) await writeRecord(record, { noReplace: false });
3713
+ }
3714
+ const current = await assertOwnerUnchecked();
3715
+ await retireObservedLease(sessionId, current);
3716
+ released = true;
3717
+ portableWriters.delete(sessionId);
3718
+ return replayWriter.status();
3719
+ } finally {
3720
+ releasing = false;
3664
3721
  }
3665
- const current = await assertOwnerUnchecked();
3666
- await retireObservedLease(sessionId, current);
3667
- released = true;
3668
- portableWriters.delete(sessionId);
3669
- return replayWriter.status();
3670
3722
  });
3723
+ releasing = true;
3724
+ return pending;
3671
3725
  };
3672
3726
 
3673
3727
  return Object.freeze({
@@ -3691,6 +3745,7 @@ async function createLease({
3691
3745
  completeUnresolvedEffectAbandonment,
3692
3746
  recoverUnresolvedEffectAbandonment,
3693
3747
  writeEffectLedger,
3748
+ recordProgress,
3694
3749
  settle,
3695
3750
  discard,
3696
3751
  assertOwner,
@@ -4034,7 +4089,7 @@ function validateCanonicalCaptainSessionRecord(
4034
4089
  exactOptionalKeys(
4035
4090
  uncertain,
4036
4091
  UNCERTAIN_KEYS,
4037
- ['abandonment'],
4092
+ ['abandonment', 'progress'],
4038
4093
  'Captain session record uncertain',
4039
4094
  );
4040
4095
  if (uncertain.baseUpdatedAt !== null) {
@@ -4112,6 +4167,65 @@ function validateCanonicalCaptainSessionRecord(
4112
4167
  uncertain.attemptedExecutionProjection,
4113
4168
  artifactSchemas,
4114
4169
  );
4170
+ if (uncertain.progress !== undefined) {
4171
+ const progress = requireRecord(uncertain.progress, 'Captain progress');
4172
+ exactOptionalKeys(progress, ['snapshot', 'steps'], ['positionStepId'], 'Captain progress');
4173
+ if (!Array.isArray(progress.steps)) throw new Error('Captain steps must be an array');
4174
+ const ids = new Set();
4175
+ for (const step of progress.steps) {
4176
+ exactOptionalKeys(requireRecord(step, 'Captain step'), ['id', 'kind', 'stateId', 'runtimeSessionId', 'playbookId'], ['result'], 'Captain step');
4177
+ assertUuid(step.id, 'Captain step id');
4178
+ assertUuid(step.runtimeSessionId, 'Captain step runtime');
4179
+ if (ids.has(step.id)) throw new Error('Captain step ids must be unique');
4180
+ ids.add(step.id);
4181
+ if (!['player', 'captain', 'script', 'preparation', 'completion', 'answer'].includes(step.kind)) throw new Error('Unknown Captain step kind');
4182
+ if (['completion', 'answer'].includes(step.kind) && (!step.result || typeof step.result !== 'object' || Array.isArray(step.result))) throw new Error('A recorded completion or answer requires its result');
4183
+ if (step.kind === 'completion') {
4184
+ exactOptionalKeys(step.result, ['state'], ['description', 'terminalOutcome', 'retention'], 'Completed root result');
4185
+ if (step.result.retention !== undefined && !['clear', 'keep'].includes(step.result.retention)) throw new Error('Completed root retention must be clear or keep');
4186
+ if (step.result.state?.status !== 'done' || (step.result.state.stateId !== undefined && step.result.state.stateId !== step.stateId)) throw new Error('Completed root result must name its final state');
4187
+ if (step.result.description !== undefined) requireNonblank(step.result.description, 'Completed root description');
4188
+ if (step.result.terminalOutcome !== undefined) {
4189
+ const terminal = step.result.terminalOutcome;
4190
+ exactOptionalKeys(terminal, ['stateId', 'kind'], ['description'], 'Completed root outcome');
4191
+ if (terminal.stateId !== step.stateId || !['success', 'failure'].includes(terminal.kind)) throw new Error('Completed root outcome must name its final state and kind');
4192
+ if (terminal.description !== undefined) requireNonblank(terminal.description, 'Completed root outcome description');
4193
+ }
4194
+ }
4195
+ if (step.kind === 'answer') {
4196
+ exactOptionalKeys(step.result, ['questions', 'instruction'], [], 'Selected answer');
4197
+ requireNonblank(step.result.instruction, 'Selected answer instruction');
4198
+ if (!Array.isArray(step.result.questions) || !step.result.questions.length) throw new Error('Selected answer requires questions');
4199
+ for (const question of step.result.questions) {
4200
+ exactOptionalKeys(question, ['asker', 'question'], [], 'Selected answer question');
4201
+ if (question.asker?.kind === 'captain') exactOptionalKeys(question.asker, ['kind'], [], 'Selected answer asker');
4202
+ else {
4203
+ exactOptionalKeys(question.asker, ['kind', 'roleId'], [], 'Selected answer asker');
4204
+ if (question.asker.kind !== 'role') throw new Error('Unknown selected answer asker');
4205
+ requireCanonicalNonblank(question.asker.roleId, 'Selected answer role');
4206
+ }
4207
+ requireNonblank(question.question, 'Selected answer question');
4208
+ }
4209
+ }
4210
+ requireCanonicalNonblank(step.stateId, 'Captain step state');
4211
+ if (!Object.hasOwn(structural.catalog, step.playbookId)) throw new Error('Captain step names an unknown playbook');
4212
+ }
4213
+ if (progress.positionStepId !== undefined && progress.positionStepId !== null && !ids.has(progress.positionStepId)) throw new Error('Captain progress positionStepId must name a recorded step');
4214
+ if (progress.snapshot === null && progress.positionStepId != null) throw new Error('Captain progress positionStepId requires a saved position');
4215
+ if (progress.snapshot !== null) {
4216
+ const prepared = assertPlaybookCaptainShellSnapshot(progress.snapshot);
4217
+ assertSnapshotMatchesStructure(prepared, structural, artifactSchemas);
4218
+ if ((Object.hasOwn(progress.snapshot, 'presentedEffectPrefix') && prepared.presentedEffectPrefix !== snapshot.presentedEffectPrefix) ||
4219
+ !isDeepStrictEqual(prepared.captain, snapshot.captain) ||
4220
+ !isDeepStrictEqual(prepared.journal, snapshot.journal) ||
4221
+ !isDeepStrictEqual(prepared.sequences, snapshot.sequences) ||
4222
+ !isPlaybookEffectLedgerMonotonicExtension(snapshot.effectLedger, prepared.effectLedger) ||
4223
+ !isPlaybookEffectLedgerMonotonicExtension(prepared.effectLedger, effectLedger)) {
4224
+ throw new Error('Saved progress must retain the settled controller and current work');
4225
+ }
4226
+
4227
+ }
4228
+ }
4115
4229
  if (uncertain.baseUpdatedAt === null) {
4116
4230
  if (!isDeepStrictEqual(lastApplied, attempted)) {
4117
4231
  throw new Error(
@@ -4436,6 +4550,7 @@ const EFFECT_BOUNDARY_IDENTITY_KEYS = [
4436
4550
  const EFFECT_BOUNDARY_OPTIONAL_KEYS = [
4437
4551
  'after',
4438
4552
  'physicalReceipt',
4553
+ 'restored',
4439
4554
  'finalText',
4440
4555
  'semanticCandidate',
4441
4556
  'initialSemanticCandidate',
@@ -5018,6 +5133,7 @@ function assertMonotonicBoundaryReplacement(expected, next, path) {
5018
5133
  for (const key of [
5019
5134
  'after',
5020
5135
  'physicalReceipt',
5136
+ 'restored',
5021
5137
  'finalText',
5022
5138
  'initialSemanticCandidate',
5023
5139
  ]) {
@@ -6001,9 +6117,13 @@ function validateRetainedGenerationFrame(
6001
6117
  'roleBindings',
6002
6118
  'runtime',
6003
6119
  ],
6004
- ['parentSessionId', 'parentCallId'],
6120
+ ['parentSessionId', 'parentCallId', 'request', 'inputs'],
6005
6121
  path,
6006
6122
  );
6123
+ if (frame.request !== undefined) {
6124
+ if (index !== 0) throw new Error(`${path}.request belongs only to the root`);
6125
+ requireNonblank(frame.request, `${path}.request`);
6126
+ }
6007
6127
  const playbookId = requireCanonicalNonblank(
6008
6128
  frame.playbookId,
6009
6129
  `${path}.playbookId`,
@@ -6014,6 +6134,7 @@ function validateRetainedGenerationFrame(
6014
6134
  `${path}.playbookId names unknown stored playbook ${JSON.stringify(playbookId)}`,
6015
6135
  );
6016
6136
  }
6137
+ if (frame.inputs !== undefined && (index !== 0 || !Array.isArray(frame.inputs) || frame.inputs.some((input) => typeof input !== 'string' || !input.trim()))) throw new Error(`${path}.inputs belongs only to the root and must contain nonempty strings`);
6017
6138
  assertUuid(frame.sessionId, `${path}.sessionId`);
6018
6139
  assertUuid(frame.rootSessionId, `${path}.rootSessionId`);
6019
6140
  if (sessionIds.has(frame.sessionId)) {
@@ -6574,3 +6695,10 @@ async function writePrivateBytes(path, bytes, fs) {
6574
6695
  try { await fs.unlink(temporary); } catch (cause) { if (cause?.code !== 'ENOENT') throw cause; }
6575
6696
  }
6576
6697
  }
6698
+
6699
+ /** One shared decision for discard controls and durable rollback. */
6700
+ export function isUncertainTurnDiscardable(record) {
6701
+ return record?.state === 'uncertain' && !record.uncertain.abandonment &&
6702
+ (record.uncertain.progress?.steps.length ?? 0) === 0 &&
6703
+ isDeepStrictEqual(record.effectLedger, record.snapshot.effectLedger);
6704
+ }