@sublang/slc 0.9.0 → 0.10.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 (90) hide show
  1. package/README.md +43 -14
  2. package/dist/app.d.ts +2 -2
  3. package/dist/app.d.ts.map +1 -1
  4. package/dist/app.js +6 -4
  5. package/dist/app.js.map +1 -1
  6. package/dist/artifacts.d.ts +5 -1
  7. package/dist/artifacts.d.ts.map +1 -1
  8. package/dist/artifacts.js +19 -3
  9. package/dist/artifacts.js.map +1 -1
  10. package/dist/clarification.d.ts +32 -0
  11. package/dist/clarification.d.ts.map +1 -0
  12. package/dist/clarification.js +93 -0
  13. package/dist/clarification.js.map +1 -0
  14. package/dist/compiled-execution.js +1 -1
  15. package/dist/compiled-execution.js.map +1 -1
  16. package/dist/compiled-executor.d.ts.map +1 -1
  17. package/dist/compiled-executor.js +58 -22
  18. package/dist/compiled-executor.js.map +1 -1
  19. package/dist/config-file.d.ts.map +1 -1
  20. package/dist/config-file.js.map +1 -1
  21. package/dist/config.d.ts +1 -1
  22. package/dist/config.d.ts.map +1 -1
  23. package/dist/config.js +1 -1
  24. package/dist/config.js.map +1 -1
  25. package/dist/entry-module.d.ts.map +1 -1
  26. package/dist/entry-module.js +16 -7
  27. package/dist/entry-module.js.map +1 -1
  28. package/dist/entry-options.d.ts +13 -0
  29. package/dist/entry-options.d.ts.map +1 -0
  30. package/dist/entry-options.js +116 -0
  31. package/dist/entry-options.js.map +1 -0
  32. package/dist/execution.d.ts +21 -4
  33. package/dist/execution.d.ts.map +1 -1
  34. package/dist/execution.js +58 -19
  35. package/dist/execution.js.map +1 -1
  36. package/dist/index.d.ts +1 -0
  37. package/dist/index.d.ts.map +1 -1
  38. package/dist/index.js +1 -0
  39. package/dist/index.js.map +1 -1
  40. package/dist/interpreter.d.ts.map +1 -1
  41. package/dist/interpreter.js +20 -1
  42. package/dist/interpreter.js.map +1 -1
  43. package/dist/invocation.d.ts.map +1 -1
  44. package/dist/link.d.ts.map +1 -1
  45. package/dist/phase-runner.d.ts +1 -4
  46. package/dist/phase-runner.d.ts.map +1 -1
  47. package/dist/phase-runner.js +4 -1
  48. package/dist/phase-runner.js.map +1 -1
  49. package/dist/phase.d.ts.map +1 -1
  50. package/dist/phase.js.map +1 -1
  51. package/dist/pin-currency.js.map +1 -1
  52. package/dist/pin-generate.js.map +1 -1
  53. package/dist/pin-inputs.js.map +1 -1
  54. package/dist/pins.d.ts.map +1 -1
  55. package/dist/pins.js.map +1 -1
  56. package/dist/pipeline.d.ts.map +1 -1
  57. package/dist/playbook-contract.d.ts.map +1 -1
  58. package/dist/playbook-contract.js.map +1 -1
  59. package/dist/playbook-ports.d.ts +9 -1
  60. package/dist/playbook-ports.d.ts.map +1 -1
  61. package/dist/playbook-ports.js +24 -6
  62. package/dist/playbook-ports.js.map +1 -1
  63. package/dist/progress.js.map +1 -1
  64. package/dist/resolver.js.map +1 -1
  65. package/dist/reviewing-agent.d.ts +2 -2
  66. package/dist/reviewing-agent.d.ts.map +1 -1
  67. package/dist/reviewing-agent.js +34 -12
  68. package/dist/reviewing-agent.js.map +1 -1
  69. package/dist/runner.d.ts +4 -2
  70. package/dist/runner.d.ts.map +1 -1
  71. package/dist/runner.js +361 -45
  72. package/dist/runner.js.map +1 -1
  73. package/dist/slc.config.template.yaml +1 -1
  74. package/dist/verify-coverage.d.ts +7 -0
  75. package/dist/verify-coverage.d.ts.map +1 -1
  76. package/dist/verify-coverage.js +805 -254
  77. package/dist/verify-coverage.js.map +1 -1
  78. package/dist/verify-source.d.ts.map +1 -1
  79. package/dist/verify-source.js +61 -20
  80. package/dist/verify-source.js.map +1 -1
  81. package/dist/verify-support.js.map +1 -1
  82. package/dist/verify-typescript.d.ts +3 -0
  83. package/dist/verify-typescript.d.ts.map +1 -0
  84. package/dist/verify-typescript.js +87 -0
  85. package/dist/verify-typescript.js.map +1 -0
  86. package/dist/verify.d.ts +28 -10
  87. package/dist/verify.d.ts.map +1 -1
  88. package/dist/verify.js +580 -126
  89. package/dist/verify.js.map +1 -1
  90. package/package.json +17 -16
@@ -19,12 +19,77 @@
19
19
  * The checker needs only the artifact and `xstate`; the emitted per-artifact
20
20
  * test runs it beside the artifacts. See specs/packages/verification.md.
21
21
  */
22
+ import { AsyncLocalStorage } from 'node:async_hooks';
22
23
  import { mkdir, readFile, writeFile } from 'node:fs/promises';
23
24
  import { join } from 'node:path';
25
+ import { setTimeout as delay } from 'node:timers/promises';
24
26
  import { createActor, fromPromise } from 'xstate';
25
- import { AWAIT_BOSS_REPLY_STATE, BOSS_REPLY_EVENT, CONTROLLER_ACTION_GUARDS, INTERRUPT_EVENT, NEEDS_BOSS_REPLY, VERIFY_MODULE, controllerDecisionNearMiss, enumerateCaptainStates, enumerateScriptStates, isControllerDecisionResult, isControllerMachine, loadFsmModule, normalizeArms, } from './verify.js';
27
+ import { AWAIT_BOSS_REPLY_STATE, BOSS_REPLY_EVENT, CONTROLLER_ACTION_GUARDS, INTERRUPT_EVENT, NEEDS_BOSS_REPLY, VERIFY_MODULE, controllerDecisionNearMiss, enumerateCaptainStates, enumerateScriptStates, initialStateTarget, isControllerDecisionResult, isControllerMachine, loadFsmModule, normalizeArms, } from './verify.js';
26
28
  /** The `gears2fsm`-mandated captain actor name a machine declares. */
27
29
  export const CAPTAIN_ACTOR = 'captain';
30
+ // Each check owns its resources, including concurrent checks and async actor
31
+ // callbacks. No current-run variable is shared between invocations.
32
+ const coverageRuns = new AsyncLocalStorage();
33
+ /** Incomplete coverage is not an unsatisfiable transition or Source question. */
34
+ export class FsmCoverageDeadlineError extends Error {
35
+ constructor() {
36
+ super('FSM transition coverage incomplete: cooperative deadline exceeded');
37
+ this.name = 'FsmCoverageDeadlineError';
38
+ }
39
+ }
40
+ function coverageCheckpoint() {
41
+ const run = coverageRuns.getStore();
42
+ run?.signal?.throwIfAborted();
43
+ if (run !== undefined && performance.now() >= run.deadline)
44
+ throw new FsmCoverageDeadlineError();
45
+ }
46
+ /** Deliver pending cancellation between bounded blocks of synchronous work. */
47
+ async function coverageYield(force = false) {
48
+ coverageCheckpoint();
49
+ const run = coverageRuns.getStore();
50
+ if (run !== undefined && (force || performance.now() - run.lastYield >= 8)) {
51
+ try {
52
+ await delay(0, undefined, { signal: run.signal });
53
+ }
54
+ finally {
55
+ coverageCheckpoint();
56
+ }
57
+ run.lastYield = performance.now();
58
+ }
59
+ }
60
+ function trackCoverageActor(actor) {
61
+ const run = coverageRuns.getStore();
62
+ if (run !== undefined) {
63
+ run.actors.add(actor);
64
+ const stop = actor.stop.bind(actor);
65
+ actor.stop = () => {
66
+ try {
67
+ return stop();
68
+ }
69
+ finally {
70
+ run.actors.delete(actor);
71
+ }
72
+ };
73
+ }
74
+ return actor;
75
+ }
76
+ /** Release every actor without replacing an already propagating failure. */
77
+ function stopCoverageActors(actors, preserveFailure) {
78
+ let failed = false;
79
+ let firstError;
80
+ for (const actor of actors) {
81
+ try {
82
+ actor.stop();
83
+ }
84
+ catch (error) {
85
+ if (!failed)
86
+ firstError = error;
87
+ failed = true;
88
+ }
89
+ }
90
+ if (failed && !preserveFailure)
91
+ throw firstError;
92
+ }
28
93
  function stateRefKey(ref) {
29
94
  return ref.path.join('\u0000');
30
95
  }
@@ -49,6 +114,15 @@ function interruptTargetForRef(ref, fallback) {
49
114
  function captainInterruptTarget(captain) {
50
115
  return interruptTargetForRef(captain.ref, captainPublicStateId(captain));
51
116
  }
117
+ function invokeGuard(guard, args, ...params) {
118
+ coverageCheckpoint();
119
+ try {
120
+ return guard(args, ...params);
121
+ }
122
+ finally {
123
+ coverageCheckpoint();
124
+ }
125
+ }
52
126
  /** Scalar values carried by a parameterized XState guard descriptor. */
53
127
  function descriptorValues(value, seen = new Set()) {
54
128
  if (typeof value === 'string' ||
@@ -66,7 +140,7 @@ function resolveGuard(machine, guard) {
66
140
  if (typeof guard === 'function') {
67
141
  const implementation = guard;
68
142
  return {
69
- run: (args) => implementation(args),
143
+ run: (args) => invokeGuard(implementation, args),
70
144
  probeValues: minedLiterals(implementation),
71
145
  };
72
146
  }
@@ -83,7 +157,7 @@ function resolveGuard(machine, guard) {
83
157
  const implementation = candidate;
84
158
  const params = descriptor.params;
85
159
  return {
86
- run: (args) => implementation(args, typeof params === 'function'
160
+ run: (args) => invokeGuard(implementation, args, typeof params === 'function'
87
161
  ? params(args)
88
162
  : params),
89
163
  probeValues: [
@@ -100,6 +174,7 @@ function guardLabel(guard) {
100
174
  return JSON.stringify(guard) ?? String(guard);
101
175
  }
102
176
  catch {
177
+ coverageCheckpoint();
103
178
  return String(guard);
104
179
  }
105
180
  }
@@ -135,6 +210,8 @@ const SETTLE_MS = 1_000;
135
210
  const PARALLEL_SETTLE_MS = 250;
136
211
  const PARALLEL_QUESTION_SETTLE_MS = 500;
137
212
  const MAX_PARALLEL_COMBINATIONS = 64;
213
+ const MAX_COVERAGE_PATH_STEPS = 8;
214
+ const MAX_COVERAGE_PATH_ATTEMPTS = 64;
138
215
  /** Stable values shared by machine input, result output, and guard probing. */
139
216
  const COVERAGE_PLAYBOOK_ID = 'coverage-child-playbook';
140
217
  const COVERAGE_PLAYBOOK_INPUT = 'coverage: complete the child request';
@@ -185,15 +262,20 @@ function coverageGuardContext(dynamic) {
185
262
  }
186
263
  /** Context produced by the machine's real initializer for the coverage input. */
187
264
  function initializedMachineContext(machine) {
265
+ let actor;
266
+ let failed = false;
188
267
  try {
189
- const actor = createActor(machine, { input: COVERAGE_MACHINE_INPUT });
190
- const context = actor.getSnapshot().context;
191
- actor.stop();
192
- return context;
268
+ actor = trackCoverageActor(createActor(machine, { input: COVERAGE_MACHINE_INPUT }));
269
+ return actor.getSnapshot().context;
193
270
  }
194
271
  catch {
272
+ failed = true;
273
+ coverageCheckpoint();
195
274
  return {};
196
275
  }
276
+ finally {
277
+ stopCoverageActors(actor === undefined ? [] : [actor], failed);
278
+ }
197
279
  }
198
280
  /** Real initialized fields plus deterministic values needed by guard probes. */
199
281
  function initializedCoverageContext(machine, dynamic) {
@@ -243,12 +325,28 @@ function synthesizedFieldValue(field) {
243
325
  }
244
326
  /** Synthesizes a captain output that selects `key` under the state's contract. */
245
327
  function synthOutput(state, key) {
328
+ if (state.actor === 'script') {
329
+ return {
330
+ guard: key,
331
+ exitStatus: Object.keys(state.result)[0] === key ? 0 : 1,
332
+ };
333
+ }
246
334
  const output = { guard: key };
247
335
  for (const field of requiredFields(state.result[key] ?? '')) {
248
336
  output[field] = synthesizedFieldValue(field);
249
337
  }
250
338
  return output;
251
339
  }
340
+ /** One bounded runtime-valid candidate set for acceptance and arm auditing. */
341
+ function resultOutputCandidates(state, key, candidates) {
342
+ const output = synthOutput(state, key);
343
+ if (state.actor !== 'script' || output.exitStatus === 0)
344
+ return [output];
345
+ return [...new Set([1, ...candidates])]
346
+ .filter((value) => typeof value === 'number' && Number.isInteger(value) && value > 0)
347
+ .slice(0, MAX_SCRIPT_EXIT_STATUSES)
348
+ .map((exitStatus) => ({ guard: key, exitStatus }));
349
+ }
252
350
  function invocations(state) {
253
351
  if (Array.isArray(state.invoke))
254
352
  return state.invoke;
@@ -291,21 +389,33 @@ function stateRefs(config) {
291
389
  visit((config.states ?? {}));
292
390
  return out;
293
391
  }
294
- /** The root initial state followed through every compound initial child. */
295
- function initialActiveRefs(config, refs) {
392
+ /** States entered by default, including every active parallel region. */
393
+ function enteredStateRefs(state, refs, parent) {
296
394
  const active = [];
297
- let path = [];
298
- let initial = config.initial;
299
- while (typeof initial === 'string') {
300
- path = [...path, initial];
395
+ const initial = initialStateTarget(state.initial);
396
+ const keys = state.type === 'parallel'
397
+ ? Object.keys(state.states ?? {})
398
+ : initial !== undefined
399
+ ? [initial]
400
+ : [];
401
+ for (const key of keys) {
402
+ const path = [...(parent?.path ?? []), key];
301
403
  const ref = refs.find((candidate) => candidate.path.join('\u0000') === path.join('\u0000'));
302
404
  if (ref === undefined)
303
- break;
304
- active.push(ref);
305
- initial = ref.state.initial;
405
+ continue;
406
+ active.push(ref, ...enteredStateRefs(ref.state, refs, ref));
306
407
  }
307
408
  return active;
308
409
  }
410
+ function initialActiveRefs(config, refs) {
411
+ return enteredStateRefs(config, refs);
412
+ }
413
+ /** A target enters itself and only its default active descendants. */
414
+ function targetEntersRef(refs, target, invocation) {
415
+ return (target !== undefined &&
416
+ (sameStateRef(target, invocation) ||
417
+ enteredStateRefs(target.state, refs, target).some((ref) => sameStateRef(ref, invocation))));
418
+ }
309
419
  /** Captain bindings paired with the nested state node that owns the invoke. */
310
420
  function captainRefs(config) {
311
421
  const out = [];
@@ -361,6 +471,7 @@ function captainRefs(config) {
361
471
  return matchingActor;
362
472
  }
363
473
  catch {
474
+ coverageCheckpoint();
364
475
  return matchingActor && binding.sourceItem === '';
365
476
  }
366
477
  });
@@ -396,21 +507,10 @@ function coverageErrorMessage(error) {
396
507
  return error instanceof Error ? error.message : String(error);
397
508
  }
398
509
  catch {
510
+ coverageCheckpoint();
399
511
  return 'unknown error';
400
512
  }
401
513
  }
402
- /** Replays a nested input against the context observed after a failed start. */
403
- function playbookInputFailure(playbook, context) {
404
- if (typeof playbook.invocation.input !== 'function')
405
- return undefined;
406
- try {
407
- playbook.invocation.input({ context });
408
- return undefined;
409
- }
410
- catch (error) {
411
- return coverageErrorMessage(error);
412
- }
413
- }
414
514
  /** Dynamic target/text context names declared by a nested playbook input. */
415
515
  function dynamicPlaybookFields(playbook) {
416
516
  if (typeof playbook.invocation.input !== 'function')
@@ -420,6 +520,7 @@ function dynamicPlaybookFields(playbook) {
420
520
  input = playbook.invocation.input({ context: coverageGuardContext() });
421
521
  }
422
522
  catch {
523
+ coverageCheckpoint();
423
524
  return undefined;
424
525
  }
425
526
  if (typeof input !== 'object' || input === null || Array.isArray(input)) {
@@ -470,22 +571,26 @@ function resolvedStateNode(machine, ref) {
470
571
  return node;
471
572
  }
472
573
  function persistedSnapshotWithContext(provided, context) {
473
- const seed = createActor(provided, { input: COVERAGE_MACHINE_INPUT });
574
+ const seed = trackCoverageActor(createActor(provided, { input: COVERAGE_MACHINE_INPUT }));
575
+ let completed = false;
474
576
  try {
475
- return {
577
+ const snapshot = {
476
578
  ...seed.getPersistedSnapshot(),
477
579
  context: { ...context },
478
580
  children: {},
479
581
  };
582
+ completed = true;
583
+ return snapshot;
480
584
  }
481
585
  finally {
482
- seed.stop();
586
+ stopCoverageActors([seed], !completed);
483
587
  }
484
588
  }
485
589
  function makeActor(machine, script, playbookScript = () => null, restoredContext, observeTransition) {
590
+ coverageCheckpoint();
486
591
  const coverageErrors = [];
487
- const workActor = fromPromise(async ({ input }) => {
488
- const output = script(input ?? {});
592
+ const workActor = fromPromise(async ({ input, self, }) => {
593
+ const output = await script(input ?? {}, self.id);
489
594
  if (output === null)
490
595
  return new Promise(() => { });
491
596
  if (output instanceof Error)
@@ -500,7 +605,7 @@ function makeActor(machine, script, playbookScript = () => null, restoredContext
500
605
  // A child script is opt-in. All unrelated child invocations hang until
501
606
  // the driven actor is stopped, preserving Captain and parallel probes.
502
607
  playbook: fromPromise(async ({ input, self, }) => {
503
- const output = playbookScript(input ?? {}, self.id);
608
+ const output = await playbookScript(input ?? {}, self.id);
504
609
  if (output === null)
505
610
  return new Promise(() => { });
506
611
  if (output instanceof Error)
@@ -512,17 +617,18 @@ function makeActor(machine, script, playbookScript = () => null, restoredContext
512
617
  const restoredSnapshot = restoredContext === undefined
513
618
  ? undefined
514
619
  : persistedSnapshotWithContext(provided, restoredContext);
515
- const actor = createActor(provided, {
620
+ const actor = trackCoverageActor(createActor(provided, {
516
621
  input: COVERAGE_MACHINE_INPUT,
517
- ...(restoredSnapshot === undefined ? {} : { snapshot: restoredSnapshot }),
622
+ ...(restoredSnapshot === undefined
623
+ ? {}
624
+ : { snapshot: restoredSnapshot }),
518
625
  inspect: (inspection) => {
519
626
  if (typeof inspection !== 'object' ||
520
627
  inspection === null ||
521
628
  !('event' in inspection)) {
522
629
  return;
523
630
  }
524
- const inspectedTransitions = inspection
525
- ._transitions;
631
+ const inspectedTransitions = inspection._transitions;
526
632
  if (inspection.type === '@xstate.microstep' &&
527
633
  Array.isArray(inspectedTransitions)) {
528
634
  for (const transition of inspectedTransitions) {
@@ -559,12 +665,13 @@ function makeActor(machine, script, playbookScript = () => null, restoredContext
559
665
  });
560
666
  }
561
667
  },
562
- });
668
+ }));
563
669
  Object.defineProperty(actor, 'coverageErrors', {
564
670
  value: coverageErrors,
565
671
  });
566
672
  actor.subscribe({ error: () => { } });
567
673
  actor.start();
674
+ coverageCheckpoint();
568
675
  return actor;
569
676
  }
570
677
  /** A script that rejects the captain invocation for the given source item. */
@@ -587,14 +694,30 @@ function onceScript(sourceItem, output, gate) {
587
694
  }
588
695
  /** Waits until the actor's snapshot satisfies `predicate`, or times out. */
589
696
  function settle(actor, predicate, ms = SETTLE_MS) {
590
- return new Promise((resolveSettled) => {
697
+ coverageCheckpoint();
698
+ const run = coverageRuns.getStore();
699
+ return new Promise((resolveSettled, reject) => {
591
700
  let subscription = undefined;
701
+ let settled = false;
592
702
  const finish = (outcome) => {
703
+ if (settled)
704
+ return;
705
+ settled = true;
593
706
  clearTimeout(timer);
594
707
  subscription?.unsubscribe();
595
- resolveSettled(outcome);
708
+ run?.signal?.removeEventListener('abort', aborted);
709
+ try {
710
+ coverageCheckpoint();
711
+ resolveSettled(outcome);
712
+ }
713
+ catch (error) {
714
+ reject(error);
715
+ }
596
716
  };
597
- const timer = setTimeout(() => finish(predicate(actor.getSnapshot())), ms);
717
+ const aborted = () => finish(false);
718
+ const remaining = run === undefined ? ms : Math.max(0, run.deadline - performance.now());
719
+ const timer = setTimeout(() => finish(predicate(actor.getSnapshot())), Math.min(ms, Math.ceil(remaining)));
720
+ run?.signal?.addEventListener('abort', aborted, { once: true });
598
721
  subscription = actor.subscribe({
599
722
  next: (snapshot) => {
600
723
  if (predicate(snapshot))
@@ -605,13 +728,26 @@ function settle(actor, predicate, ms = SETTLE_MS) {
605
728
  // owning probe records the actionable coverage finding.
606
729
  error: () => finish(false),
607
730
  });
731
+ if (settled)
732
+ subscription.unsubscribe();
608
733
  if (predicate(actor.getSnapshot()))
609
734
  finish(true);
735
+ if (run?.signal?.aborted)
736
+ aborted();
610
737
  });
611
738
  }
612
739
  /** Lets XState process an event whose correct outcome is no transition. */
613
740
  async function settleNoTransition() {
614
- await new Promise((resolveSettled) => setTimeout(resolveSettled, 10));
741
+ coverageCheckpoint();
742
+ const run = coverageRuns.getStore();
743
+ try {
744
+ await delay(Math.min(10, run === undefined
745
+ ? 10
746
+ : Math.max(0, Math.ceil(run.deadline - performance.now()))), undefined, { signal: run?.signal });
747
+ }
748
+ finally {
749
+ coverageCheckpoint();
750
+ }
615
751
  }
616
752
  function activeStateIds(snapshot) {
617
753
  const ids = new Set();
@@ -641,6 +777,7 @@ function activeStateIds(snapshot) {
641
777
  return;
642
778
  }
643
779
  for (const [key, nested] of Object.entries(value)) {
780
+ ids.add([...prefix, key].join('.'));
644
781
  walkValue(nested, [...prefix, key]);
645
782
  }
646
783
  };
@@ -701,8 +838,12 @@ function directlySelectedEventArm(machine, arms, event, context = coverageGuardC
701
838
  return index;
702
839
  }
703
840
  catch {
841
+ coverageCheckpoint();
704
842
  return undefined;
705
843
  }
844
+ finally {
845
+ coverageCheckpoint();
846
+ }
706
847
  }
707
848
  return undefined;
708
849
  }
@@ -742,7 +883,6 @@ function orderedArmPredicate(machine, arms, selected) {
742
883
  }
743
884
  /** A bounded satisfying context/event pair for one authored interrupt arm. */
744
885
  function interruptDriveForRef(machine, refs, target, targetId, extraValues = [], selectedArmIndex) {
745
- const base = { type: INTERRUPT_EVENT, targetId };
746
886
  const arms = transitionArms((machine.config.on ?? {})[INTERRUPT_EVENT]);
747
887
  const armIndex = selectedArmIndex ??
748
888
  arms.findIndex((arm) => {
@@ -750,9 +890,17 @@ function interruptDriveForRef(machine, refs, target, targetId, extraValues = [],
750
890
  if (rawTarget === undefined)
751
891
  return false;
752
892
  const resolvedTarget = stateRefForTarget(refs, rawTarget);
753
- return (sameStateRef(resolvedTarget, target) ||
893
+ return (targetEntersRef(refs, resolvedTarget, target) ||
754
894
  resolvedTarget?.stableId === targetId);
755
895
  });
896
+ const rawTarget = rawArmTarget(arms[armIndex]);
897
+ const entryTarget = rawTarget === undefined ? undefined : stateRefForTarget(refs, rawTarget);
898
+ const base = {
899
+ type: INTERRUPT_EVENT,
900
+ targetId: entryTarget !== undefined && !sameStateRef(entryTarget, target)
901
+ ? entryTarget.stableId
902
+ : targetId,
903
+ };
756
904
  const targetPlaybook = playbookRefs(machine.config).find((playbook) => sameStateRef(playbook.ref, target));
757
905
  const initialContext = initializedCoverageContext(machine, targetPlaybook === undefined
758
906
  ? undefined
@@ -802,7 +950,7 @@ function entryDriveForRef(machine, refs, target, extraValues = [], initialContex
802
950
  for (const [armIndex, arm] of arms.entries()) {
803
951
  const rawTarget = rawArmTarget(arm);
804
952
  if (rawTarget === undefined ||
805
- !sameStateRef(stateRefForTarget(refs, rawTarget, source), target)) {
953
+ !targetEntersRef(refs, stateRefForTarget(refs, rawTarget, source), target)) {
806
954
  continue;
807
955
  }
808
956
  const guard = orderedArmPredicate(machine, arms, armIndex);
@@ -867,15 +1015,18 @@ const GENERIC_VALUES = [
867
1015
  COVERAGE_ENABLED_PLAYBOOKS,
868
1016
  ];
869
1017
  const MAX_PROBES = 30_000;
1018
+ const MAX_SCRIPT_EXIT_STATUSES = 8;
870
1019
  const PROBES_PER_TIMEOUT_MILLISECOND = 50;
871
1020
  const COVERAGE_TIMEOUT_MARGIN_MS = 5_000;
872
1021
  const MIN_COVERAGE_TEST_TIMEOUT_MS = 10_000;
1022
+ const MAX_COVERAGE_TEST_TIMEOUT_MS = 300_000;
873
1023
  function minedLiterals(fn) {
874
1024
  let source;
875
1025
  try {
876
1026
  source = String(fn);
877
1027
  }
878
1028
  catch {
1029
+ coverageCheckpoint();
879
1030
  return [];
880
1031
  }
881
1032
  const literals = source.match(/'(?:[^'\\]|\\.)*'|"(?:[^"\\]|\\.)*"/g) ?? [];
@@ -923,6 +1074,22 @@ export function guardSatisfiable(guard, baseOutput, extraValues = []) {
923
1074
  }
924
1075
  function probeGuardAssignment(guard, fixedEvent, payloads, extraValues, options = {}) {
925
1076
  const baseContext = { ...(options.initialContext ?? {}) };
1077
+ // Guards are predicates over fixed seed objects. Snapshot their descriptors
1078
+ // once within this probe, while keeping every candidate independently mutable
1079
+ // and retaining accessors, prototypes and non-enumerable Error properties.
1080
+ const bases = new WeakMap();
1081
+ const overlay = (base, assigned) => {
1082
+ let shape = bases.get(base);
1083
+ if (shape === undefined) {
1084
+ shape = {
1085
+ prototype: Object.getPrototypeOf(base),
1086
+ descriptors: Object.getOwnPropertyDescriptors(base),
1087
+ };
1088
+ bases.set(base, shape);
1089
+ }
1090
+ const copy = Object.create(shape.prototype, shape.descriptors);
1091
+ return Object.assign(copy, assigned);
1092
+ };
926
1093
  // Guard-source literals first: the likeliest matches are tried earliest.
927
1094
  const values = [
928
1095
  ...new Set([...minedLiterals(guard), ...extraValues, ...GENERIC_VALUES]),
@@ -932,27 +1099,33 @@ function probeGuardAssignment(guard, fixedEvent, payloads, extraValues, options
932
1099
  let event = { ...fixedEvent };
933
1100
  for (const payload of payloads) {
934
1101
  if (payload.eventField === undefined) {
935
- event = overlaidObject(event, assignment.payloads[payload.tag] ?? {});
1102
+ event = overlay(overlay(payload.base, event), assignment.payloads[payload.tag] ?? {});
936
1103
  }
937
1104
  else {
938
- event[payload.eventField] = overlaidObject(payload.base, assignment.payloads[payload.tag] ?? {});
1105
+ event[payload.eventField] = overlay(payload.base, assignment.payloads[payload.tag] ?? {});
939
1106
  }
940
1107
  }
941
1108
  return event;
942
1109
  };
943
1110
  const passes = (assignment) => {
1111
+ coverageCheckpoint();
944
1112
  probes++;
945
1113
  try {
946
1114
  return Boolean(guard({ context: assignment.context, event: eventFor(assignment) }));
947
1115
  }
948
1116
  catch {
1117
+ coverageCheckpoint();
949
1118
  return false;
950
1119
  }
1120
+ finally {
1121
+ coverageCheckpoint();
1122
+ }
951
1123
  };
952
1124
  // Records the unassigned fields the guard reads under the given assignment.
953
1125
  const readsUnder = (assignment) => {
1126
+ coverageCheckpoint();
954
1127
  const reads = new Set();
955
- const recording = (base, assigned, tag, varyExisting = false) => new Proxy(overlaidObject(base, assigned), {
1128
+ const recording = (base, assigned, tag, varyExisting = false) => new Proxy(overlay(base, assigned), {
956
1129
  get(target, prop) {
957
1130
  if (typeof prop !== 'string')
958
1131
  return undefined;
@@ -978,7 +1151,7 @@ function probeGuardAssignment(guard, fixedEvent, payloads, extraValues, options
978
1151
  let event = { ...fixedEvent };
979
1152
  for (const payload of payloads) {
980
1153
  if (payload.eventField === undefined) {
981
- event = recording(event, assignment.payloads[payload.tag] ?? {}, payload.tag);
1154
+ event = recording(overlay(payload.base, event), assignment.payloads[payload.tag] ?? {}, payload.tag);
982
1155
  }
983
1156
  else {
984
1157
  event[payload.eventField] = recording(payload.base, assignment.payloads[payload.tag] ?? {}, payload.tag);
@@ -990,11 +1163,16 @@ function probeGuardAssignment(guard, fixedEvent, payloads, extraValues, options
990
1163
  });
991
1164
  }
992
1165
  catch {
1166
+ coverageCheckpoint();
993
1167
  // Reads observed before the throw still guide the search.
994
1168
  }
1169
+ finally {
1170
+ coverageCheckpoint();
1171
+ }
995
1172
  return [...reads];
996
1173
  };
997
1174
  const search = (assignment, depth) => {
1175
+ coverageCheckpoint();
998
1176
  if (probes > MAX_PROBES)
999
1177
  return undefined;
1000
1178
  if (passes(assignment))
@@ -1020,6 +1198,7 @@ function probeGuardAssignment(guard, fixedEvent, payloads, extraValues, options
1020
1198
  ...values.filter((value) => !hasBaseline || value !== baseline),
1021
1199
  ];
1022
1200
  for (const value of candidates) {
1201
+ coverageCheckpoint();
1023
1202
  if (probes > MAX_PROBES)
1024
1203
  return undefined;
1025
1204
  const nextDepth = depth + (hasBaseline && value === baseline ? 0 : 1);
@@ -1066,8 +1245,8 @@ function overlaidObject(base, assigned) {
1066
1245
  * named nested payloads are assignable; event-level fields such as `type` and
1067
1246
  * `actorId` remain the values XState actually supplies.
1068
1247
  */
1069
- function doneGuardSatisfiable(guard, event, output, extraValues) {
1070
- return probeGuardSatisfiable(guard.run, event, [{ eventField: 'output', tag: 'o:', base: output }], [...guard.probeValues, ...extraValues]);
1248
+ function doneGuardSatisfiable(guard, event, output, extraValues, fixedOutput = false) {
1249
+ return probeGuardSatisfiable(guard.run, fixedOutput ? { ...event, output } : event, fixedOutput ? [] : [{ eventField: 'output', tag: 'o:', base: output }], [...guard.probeValues, ...extraValues]);
1071
1250
  }
1072
1251
  /** A concrete initialized-context/output assignment selecting one done arm. */
1073
1252
  function controllerDoneGuardAssignment(guard, event, output, extraValues, initialContext) {
@@ -1085,11 +1264,7 @@ function parallelBranchCaptains(parallel, refs, captains) {
1085
1264
  const regions = refs.filter((ref) => ref.parent === parallel);
1086
1265
  const selected = [];
1087
1266
  for (const region of regions) {
1088
- const candidates = captains.filter((captain) => isDescendantOf(captain.ref, region));
1089
- const initial = region.state.initial;
1090
- const captain = (initial === undefined
1091
- ? undefined
1092
- : candidates.find((candidate) => candidate.ref.path[region.path.length] === initial)) ?? candidates[0];
1267
+ const captain = captains.find((candidate) => targetEntersRef(refs, region, candidate.ref));
1093
1268
  if (captain !== undefined)
1094
1269
  selected.push(captain);
1095
1270
  }
@@ -1153,20 +1328,294 @@ function callCount(calls, captain) {
1153
1328
  calls.get(captain.binding.sourceItem) ??
1154
1329
  0);
1155
1330
  }
1156
- function playbookCoverageInput(machine, playbook, dynamic) {
1157
- if (typeof playbook.invocation.input !== 'function')
1158
- return undefined;
1159
- try {
1160
- const input = playbook.invocation.input({
1161
- context: initializedCoverageContext(machine, dynamic),
1331
+ /** One public drive or the actual initialized state, never a private jump. */
1332
+ function childCoverageDrive(machine, refs, ref, targetId, candidates) {
1333
+ const interrupt = interruptDriveForRef(machine, refs, ref, targetId, candidates);
1334
+ if (interrupt.satisfiable)
1335
+ return { drive: interrupt };
1336
+ const entry = entryDriveForRef(machine, refs, ref, candidates);
1337
+ if (entry !== undefined)
1338
+ return { drive: entry };
1339
+ return initialActiveRefs(machine.config, refs).some((active) => sameStateRef(active, ref))
1340
+ ? {}
1341
+ : undefined;
1342
+ }
1343
+ function coverageNodeKey(node) {
1344
+ return `${stateRefKey(node.value.ref)}:${node.value.invocationIndex}`;
1345
+ }
1346
+ function coverageNodes(captains, playbooks) {
1347
+ return [
1348
+ ...captains.map((value) => ({ kind: 'acting', value })),
1349
+ ...playbooks.map((value) => ({ kind: 'child', value })),
1350
+ ];
1351
+ }
1352
+ /** The exact ordered output candidates used both for entry and target arms. */
1353
+ function coverageArmResults(machine, node, outcome, armIndex, context, input, candidates, dynamicTarget, requiredResult, includeBare = false, firstOnly = false) {
1354
+ const arms = transitionArms(node.value.invocation[outcome]);
1355
+ const guard = orderedArmPredicate(machine, arms, armIndex);
1356
+ if (guard === undefined)
1357
+ return [];
1358
+ const actorId = invocationActorId(machine, node.value);
1359
+ const event = {
1360
+ type: `xstate.${outcome === 'onDone' ? 'done' : 'error'}.actor.${actorId}`,
1361
+ actorId,
1362
+ };
1363
+ const bases = outcome === 'onError'
1364
+ ? node.kind === 'child'
1365
+ ? [
1366
+ nestedFailure(typeof input.playbookId === 'string'
1367
+ ? input.playbookId
1368
+ : COVERAGE_PLAYBOOK_ID),
1369
+ nestedFailureTerminal(typeof input.playbookId === 'string'
1370
+ ? input.playbookId
1371
+ : COVERAGE_PLAYBOOK_ID),
1372
+ new Error('coverage: generic nested playbook failure'),
1373
+ ]
1374
+ : [new Error('coverage: forced captain failure')]
1375
+ : node.kind === 'child'
1376
+ ? [{}, nestedSuccessOutput(), { invalidCoverageOutput: undefined }]
1377
+ : (requiredResult === undefined
1378
+ ? Object.keys(node.value.binding.result)
1379
+ : [requiredResult]).flatMap((key) => resultOutputCandidates(node.value.binding, key, [
1380
+ ...guard.probeValues,
1381
+ ...candidates,
1382
+ ]).map((output) => ({
1383
+ ...output,
1384
+ ...(dynamicTarget === undefined
1385
+ ? {}
1386
+ : {
1387
+ [dynamicTarget.playbookIdContext]: COVERAGE_PLAYBOOK_ID,
1388
+ [dynamicTarget.textContext]: COVERAGE_PLAYBOOK_INPUT,
1389
+ }),
1390
+ })));
1391
+ if (includeBare &&
1392
+ node.kind === 'acting' &&
1393
+ node.value.binding.actor !== 'script' &&
1394
+ outcome === 'onDone') {
1395
+ bases.push(...Object.keys(node.value.binding.result).map((guard) => ({ guard })));
1396
+ }
1397
+ const results = [];
1398
+ for (const base of bases) {
1399
+ coverageCheckpoint();
1400
+ if (node.kind === 'acting' &&
1401
+ node.value.binding.actor === 'script' &&
1402
+ outcome === 'onDone') {
1403
+ if (directlySelectedEventArm(machine, arms, { ...event, output: base }, context) === armIndex) {
1404
+ results.push(base);
1405
+ if (firstOnly)
1406
+ return results;
1407
+ }
1408
+ continue;
1409
+ }
1410
+ const assignment = probeGuardAssignment(guard.run, event, [
1411
+ {
1412
+ eventField: outcome === 'onDone' ? 'output' : 'error',
1413
+ tag: outcome === 'onDone' ? 'o:' : 'r:',
1414
+ base,
1415
+ ...(outcome === 'onError' ? { varyExisting: true } : {}),
1416
+ },
1417
+ ], [...guard.probeValues, ...GENERIC_VALUES, ...candidates], { initialContext: context, assignContext: false });
1418
+ if (assignment !== undefined) {
1419
+ results.push(assignedPayload(base, assignment, outcome === 'onDone' ? 'o:' : 'r:'));
1420
+ if (firstOnly)
1421
+ return results;
1422
+ }
1423
+ }
1424
+ return results;
1425
+ }
1426
+ /** Replay every prefix transition; no persisted private-state restoration. */
1427
+ async function replayCoveragePath(machine, nodes, path) {
1428
+ const gate = { armed: path.drive === undefined };
1429
+ let expected = path.root;
1430
+ let pending;
1431
+ const script = (input, actorId) => {
1432
+ const node = nodes.find((candidate) => invocationActorId(machine, candidate.value) === actorId);
1433
+ if (!gate.armed ||
1434
+ node === undefined ||
1435
+ coverageNodeKey(node) !== coverageNodeKey(expected))
1436
+ return null;
1437
+ const stateId = node.kind === 'acting'
1438
+ ? captainPublicStateId(node.value)
1439
+ : node.value.ref.stableId;
1440
+ if (input.stateId !== stateId)
1441
+ return null;
1442
+ return new Promise((complete) => {
1443
+ pending = { node, input, complete };
1162
1444
  });
1163
- return typeof input === 'object' && input !== null && !Array.isArray(input)
1164
- ? input
1165
- : undefined;
1445
+ };
1446
+ const actor = makeActor(machine, script, script, path.drive?.context);
1447
+ gate.armed = true;
1448
+ if (path.drive !== undefined)
1449
+ actor.send(path.drive.event);
1450
+ const wait = () => settle(actor, (snapshot) => (pending !== undefined && atState(expected.value.ref)(snapshot)) ||
1451
+ snapshot.status === 'done' ||
1452
+ snapshot.status === 'error', PARALLEL_SETTLE_MS);
1453
+ let completedPrefix = true;
1454
+ for (const step of path.steps) {
1455
+ await coverageYield();
1456
+ await wait();
1457
+ const current = pending;
1458
+ if (current === undefined ||
1459
+ coverageNodeKey(current.node) !== coverageNodeKey(step.node)) {
1460
+ completedPrefix = false;
1461
+ break;
1462
+ }
1463
+ pending = undefined;
1464
+ expected = step.next;
1465
+ current.complete(step.result);
1466
+ if (step.questionWait !== undefined) {
1467
+ if (!(await settle(actor, atState(step.questionWait), PARALLEL_SETTLE_MS))) {
1468
+ completedPrefix = false;
1469
+ break;
1470
+ }
1471
+ actor.send({
1472
+ type: BOSS_REPLY_EVENT,
1473
+ questionId: captainPublicStateId(step.node.value),
1474
+ answer: 'Proceed as planned.',
1475
+ });
1476
+ }
1166
1477
  }
1167
- catch {
1168
- return undefined;
1478
+ await wait();
1479
+ const snapshot = actor.getSnapshot();
1480
+ const failure = actor.coverageErrors.find(({ eventType }) => eventType ===
1481
+ `xstate.error.actor.${invocationActorId(machine, expected.value)}`);
1482
+ return {
1483
+ actor,
1484
+ ...(!completedPrefix ||
1485
+ pending === undefined ||
1486
+ !atState(expected.value.ref)(snapshot)
1487
+ ? {}
1488
+ : { pending }),
1489
+ ...(failure !== undefined
1490
+ ? { inputFailure: coverageErrorMessage(failure.error) }
1491
+ : snapshot.status === 'error'
1492
+ ? { inputFailure: coverageErrorMessage(snapshot.error) }
1493
+ : {}),
1494
+ };
1495
+ }
1496
+ /** Finite path replay, with one canonical question/reply revisit at most. */
1497
+ async function searchCoverageEntry(machine, target, refs, captains, playbooks, candidates, check) {
1498
+ const nodes = coverageNodes(captains, playbooks);
1499
+ const nodesForTarget = (raw, source) => {
1500
+ const targetName = rawArmTarget(raw);
1501
+ const targetRef = targetName === undefined
1502
+ ? undefined
1503
+ : stateRefForTarget(refs, targetName, source);
1504
+ return nodes.filter((node) => targetEntersRef(refs, targetRef, node.value.ref));
1505
+ };
1506
+ const reachable = new Set([coverageNodeKey(target)]);
1507
+ for (let pass = 0; pass < nodes.length; pass++) {
1508
+ for (const node of nodes) {
1509
+ if (['onDone', 'onError'].some((outcome) => transitionArms(node.value.invocation[outcome]).some((arm) => {
1510
+ return nodesForTarget(arm, node.value.ref).some((next) => reachable.has(coverageNodeKey(next)));
1511
+ })))
1512
+ reachable.add(coverageNodeKey(node));
1513
+ }
1514
+ }
1515
+ const queue = [];
1516
+ for (const node of [
1517
+ target,
1518
+ ...nodes.filter((node) => coverageNodeKey(node) !== coverageNodeKey(target)),
1519
+ ]) {
1520
+ if (!reachable.has(coverageNodeKey(node)))
1521
+ continue;
1522
+ const entry = childCoverageDrive(machine, refs, node.value.ref, node.kind === 'acting'
1523
+ ? captainInterruptTarget(node.value)
1524
+ : interruptTargetForRef(node.value.ref, node.value.ref.stableId), candidates);
1525
+ if (entry !== undefined)
1526
+ queue.push({ root: node, ...entry, steps: [] });
1527
+ }
1528
+ let attempts = 0;
1529
+ let entered = false;
1530
+ let limitation;
1531
+ let inputFailure;
1532
+ while (queue.length > 0 && attempts < MAX_COVERAGE_PATH_ATTEMPTS) {
1533
+ await coverageYield();
1534
+ const path = queue.shift();
1535
+ attempts++;
1536
+ let replay = await replayCoveragePath(machine, nodes, path);
1537
+ try {
1538
+ if (replay.inputFailure !== undefined)
1539
+ inputFailure = replay.inputFailure;
1540
+ if (replay.pending !== undefined &&
1541
+ coverageNodeKey(replay.pending.node) === coverageNodeKey(target)) {
1542
+ entered = true;
1543
+ if (await check(replay.actor, replay.pending))
1544
+ return { covered: true, entered };
1545
+ // A target check may have completed its actor. Re-enter the same
1546
+ // authored prefix before considering successors, charging that replay
1547
+ // against the same attempt limit.
1548
+ replay.actor.stop();
1549
+ if (attempts >= MAX_COVERAGE_PATH_ATTEMPTS) {
1550
+ limitation = `exhausted ${MAX_COVERAGE_PATH_ATTEMPTS}-attempt path budget`;
1551
+ break;
1552
+ }
1553
+ attempts++;
1554
+ replay = await replayCoveragePath(machine, nodes, path);
1555
+ }
1556
+ const pending = replay.pending;
1557
+ if (pending === undefined)
1558
+ continue;
1559
+ const node = pending.node;
1560
+ const context = replay.actor.getSnapshot().context;
1561
+ const nextSteps = [];
1562
+ for (const outcome of ['onDone', 'onError']) {
1563
+ for (const [index, arm] of transitionArms(node.value.invocation[outcome]).entries()) {
1564
+ for (const next of nodesForTarget(arm, node.value.ref)) {
1565
+ if (!reachable.has(coverageNodeKey(next)))
1566
+ continue;
1567
+ const dynamic = next.kind === 'child'
1568
+ ? dynamicPlaybookFields(next.value)
1569
+ : undefined;
1570
+ for (const result of coverageArmResults(machine, node, outcome, index, context, pending.input, candidates, dynamic)) {
1571
+ nextSteps.push({ node, result, next });
1572
+ }
1573
+ }
1574
+ }
1575
+ }
1576
+ if (node.kind === 'acting' &&
1577
+ Object.hasOwn(node.value.binding.result, NEEDS_BOSS_REPLY) &&
1578
+ !path.steps.some((step) => step.questionWait !== undefined)) {
1579
+ for (const [index, arm] of transitionArms(node.value.invocation.onDone).entries()) {
1580
+ const rawTarget = rawArmTarget(arm);
1581
+ const waitRef = rawTarget === undefined
1582
+ ? undefined
1583
+ : stateRefForTarget(refs, rawTarget, node.value.ref);
1584
+ if (waitRef === undefined ||
1585
+ (waitRef.stableId !== AWAIT_BOSS_REPLY_STATE &&
1586
+ !tagsOf(waitRef.state).includes('playbook.parked')))
1587
+ continue;
1588
+ for (const result of coverageArmResults(machine, node, 'onDone', index, context, pending.input, candidates, undefined, NEEDS_BOSS_REPLY)) {
1589
+ nextSteps.push({ node, result, next: node, questionWait: waitRef });
1590
+ }
1591
+ }
1592
+ }
1593
+ for (const step of nextSteps) {
1594
+ if (path.steps.length + 1 >= MAX_COVERAGE_PATH_STEPS) {
1595
+ limitation = `exhausted ${MAX_COVERAGE_PATH_STEPS}-invocation path depth`;
1596
+ continue;
1597
+ }
1598
+ const visited = [path.root, ...path.steps.map((step) => step.next)].map(coverageNodeKey);
1599
+ if (step.questionWait === undefined &&
1600
+ visited.includes(coverageNodeKey(step.next))) {
1601
+ limitation ??= 'unsupported repeated-invocation path';
1602
+ continue;
1603
+ }
1604
+ queue.push({ ...path, steps: [...path.steps, step] });
1605
+ }
1606
+ }
1607
+ finally {
1608
+ replay.actor.stop();
1609
+ }
1169
1610
  }
1611
+ if (queue.length > 0)
1612
+ limitation = `exhausted ${MAX_COVERAGE_PATH_ATTEMPTS}-attempt path budget`;
1613
+ return {
1614
+ covered: false,
1615
+ entered,
1616
+ ...(limitation === undefined ? {} : { limitation }),
1617
+ ...(inputFailure === undefined ? {} : { inputFailure }),
1618
+ };
1170
1619
  }
1171
1620
  /**
1172
1621
  * The value the bridge resolves a completed child with: a child that reached
@@ -1240,7 +1689,7 @@ function playbookEntryPlan(machine, playbook, refs, captains, dynamic) {
1240
1689
  for (const [armIndex, arm] of arms.entries()) {
1241
1690
  const rawTarget = rawArmTarget(arm);
1242
1691
  if (rawTarget === undefined ||
1243
- !sameStateRef(stateRefForTarget(refs, rawTarget, captain.ref), playbook.ref)) {
1692
+ !targetEntersRef(refs, stateRefForTarget(refs, rawTarget, captain.ref), playbook.ref)) {
1244
1693
  continue;
1245
1694
  }
1246
1695
  for (const key of Object.keys(captain.binding.result)) {
@@ -1288,7 +1737,7 @@ function captainPredecessorPlan(machine, target, playbooks, refs, captains) {
1288
1737
  for (const [armIndex, arm] of arms.entries()) {
1289
1738
  const rawTarget = rawArmTarget(arm);
1290
1739
  if (rawTarget === undefined ||
1291
- !sameStateRef(stateRefForTarget(refs, rawTarget, playbook.ref), target.ref)) {
1740
+ !targetEntersRef(refs, stateRefForTarget(refs, rawTarget, playbook.ref), target.ref)) {
1292
1741
  continue;
1293
1742
  }
1294
1743
  const guard = orderedArmPredicate(machine, arms, armIndex);
@@ -1342,7 +1791,7 @@ function controllerCaptainEntryPlan(machine, target, refs, captains, candidates,
1342
1791
  for (const resultKey of Object.keys(current.captain.binding.result)) {
1343
1792
  const output = synthOutput(current.captain.binding, resultKey);
1344
1793
  const nextRef = directTargetRef(machine, refs, current.captain, output, initialContext);
1345
- const nextCaptain = captains.find((candidate) => nextRef !== undefined && sameStateRef(candidate.ref, nextRef));
1794
+ const nextCaptain = captains.find((candidate) => targetEntersRef(refs, nextRef, candidate.ref));
1346
1795
  if (nextCaptain !== undefined &&
1347
1796
  !visited.has(stateRefKey(nextCaptain.ref))) {
1348
1797
  queue.push({
@@ -1365,7 +1814,7 @@ function controllerCaptainExitPlan(machine, captain, output, refs, captains, hub
1365
1814
  if (sameStateRef(firstTarget, hub) || firstTarget.state.type === 'final') {
1366
1815
  return { following: [], outcome: 'planned' };
1367
1816
  }
1368
- const firstCaptain = captains.find((candidate) => sameStateRef(candidate.ref, firstTarget));
1817
+ const firstCaptain = captains.find((candidate) => targetEntersRef(refs, firstTarget, candidate.ref));
1369
1818
  if (firstCaptain === undefined) {
1370
1819
  return { following: [], outcome: 'runtime' };
1371
1820
  }
@@ -1391,7 +1840,7 @@ function controllerCaptainExitPlan(machine, captain, output, refs, captains, hub
1391
1840
  if (sameStateRef(nextRef, hub) || nextRef.state.type === 'final') {
1392
1841
  return { following, outcome: 'planned' };
1393
1842
  }
1394
- const nextCaptain = captains.find((candidate) => sameStateRef(candidate.ref, nextRef));
1843
+ const nextCaptain = captains.find((candidate) => targetEntersRef(refs, nextRef, candidate.ref));
1395
1844
  if (nextCaptain !== undefined &&
1396
1845
  !visited.has(stateRefKey(nextCaptain.ref))) {
1397
1846
  queue.push({ captain: nextCaptain, following });
@@ -1540,83 +1989,24 @@ async function probePlaybookOutcome(machine, playbook, refs, captains, outcome,
1540
1989
  `state ${playbook.ref.stableId} declares no nested playbook ${outcome} transition`,
1541
1990
  ];
1542
1991
  }
1543
- const actorId = invocationActorId(machine, playbook);
1544
- const dynamic = dynamicPlaybookFields(playbook);
1545
- const entry = dynamic === undefined
1546
- ? undefined
1547
- : playbookEntryPlan(machine, playbook, refs, captains, dynamic);
1548
- if (dynamic !== undefined && entry === undefined) {
1549
- return [
1550
- `state ${playbook.ref.stableId}: dynamic nested playbook has no reachable Captain entry transition`,
1551
- ];
1552
- }
1553
- const entryRef = entry?.captain.ref ?? playbook.ref;
1554
- const entryId = entry === undefined
1555
- ? interruptTargetForRef(playbook.ref, playbook.ref.stableId)
1556
- : captainInterruptTarget(entry.captain);
1557
- const drive = interruptDriveForRef(machine, refs, entryRef, entryId, interruptValues);
1558
- const input = playbookCoverageInput(machine, playbook, dynamic);
1559
- const expectedPlaybookId = typeof input?.playbookId === 'string'
1560
- ? input.playbookId
1561
- : COVERAGE_PLAYBOOK_ID;
1562
- const fixedEvent = {
1563
- type: `xstate.${outcome === 'onDone' ? 'done' : 'error'}.actor.${actorId}`,
1564
- actorId,
1565
- };
1566
- const context = initializedCoverageContext(machine, dynamic);
1567
- const candidateValues = refs.flatMap((ref) => [
1568
- ref.key,
1569
- ref.stableId,
1570
- ...(ref.configId === undefined ? [] : [ref.configId]),
1571
- ]);
1992
+ const node = { kind: 'child', value: playbook };
1993
+ const candidateValues = [
1994
+ ...interruptValues,
1995
+ ...refs.flatMap((ref) => [
1996
+ ref.key,
1997
+ ref.stableId,
1998
+ ...(ref.configId === undefined ? [] : [ref.configId]),
1999
+ ]),
2000
+ ];
1572
2001
  const findings = [];
1573
2002
  for (const [armIndex, arm] of rawArms.entries()) {
2003
+ await coverageYield();
1574
2004
  const rawGuard = armGuard(arm);
1575
2005
  if (rawGuard !== undefined &&
1576
2006
  resolveGuard(machine, rawGuard) === undefined) {
1577
2007
  findings.push(`state ${playbook.ref.stableId}: nested playbook ${outcome} arm ${armIndex} names an unresolvable guard "${guardLabel(rawGuard)}"`);
1578
2008
  continue;
1579
2009
  }
1580
- const guard = orderedArmPredicate(machine, rawArms, armIndex);
1581
- if (guard === undefined)
1582
- continue;
1583
- let scriptedResult;
1584
- if (outcome === 'onDone') {
1585
- const outputs = [
1586
- nestedSuccessOutput(),
1587
- // A resolved promise may still return a value rejected by the linked
1588
- // runtime's output validator; fallback arms must be independently
1589
- // selectable and driven as well.
1590
- { invalidCoverageOutput: undefined },
1591
- ];
1592
- for (const base of outputs) {
1593
- const assignment = probeGuardAssignment(guard.run, fixedEvent, [{ eventField: 'output', tag: 'o:', base }], [...guard.probeValues, ...candidateValues], { initialContext: context, assignContext: false });
1594
- if (assignment !== undefined) {
1595
- scriptedResult = assignedPayload(base, assignment, 'o:');
1596
- break;
1597
- }
1598
- }
1599
- }
1600
- else {
1601
- const errors = [
1602
- nestedFailure(expectedPlaybookId),
1603
- // A completed child that reached its own failure terminal arrives on
1604
- // this same path, so an arm written only for that shape is driven too.
1605
- nestedFailureTerminal(expectedPlaybookId),
1606
- new Error('coverage: generic nested playbook failure'),
1607
- ];
1608
- for (const base of errors) {
1609
- const assignment = probeGuardAssignment(guard.run, fixedEvent, [{ eventField: 'error', tag: 'r:', base, varyExisting: true }], [...guard.probeValues, ...candidateValues], { initialContext: context, assignContext: false });
1610
- if (assignment !== undefined) {
1611
- scriptedResult = assignedPayload(base, assignment, 'r:');
1612
- break;
1613
- }
1614
- }
1615
- }
1616
- if (scriptedResult === undefined) {
1617
- findings.push(`state ${playbook.ref.stableId}: nested playbook ${outcome} arm ${armIndex} is unsatisfiable under probing`);
1618
- continue;
1619
- }
1620
2010
  const rawTarget = rawArmTarget(arm);
1621
2011
  const target = rawTarget === undefined
1622
2012
  ? undefined
@@ -1629,49 +2019,129 @@ async function probePlaybookOutcome(machine, playbook, refs, captains, outcome,
1629
2019
  findings.push(`state ${playbook.ref.stableId}: nested playbook ${outcome} arm ${armIndex} does not leave the call state`);
1630
2020
  continue;
1631
2021
  }
1632
- const gate = { armed: false };
1633
- let calls = 0;
1634
- const actor = makeActor(machine, entry === undefined
1635
- ? () => null
1636
- : onceScript(entry.captain.binding.sourceItem, entry.output, gate), (actorInput, invokedActorId) => {
1637
- if (!gate.armed ||
1638
- calls > 0 ||
1639
- invokedActorId !== actorId ||
1640
- actorInput.stateId !== playbook.ref.stableId ||
1641
- (dynamic !== undefined &&
1642
- (actorInput.playbookId !== COVERAGE_PLAYBOOK_ID ||
1643
- actorInput.text !== COVERAGE_PLAYBOOK_INPUT))) {
1644
- return null;
1645
- }
1646
- calls++;
1647
- return scriptedResult;
1648
- }, drive.context);
1649
- gate.armed = true;
1650
- actor.send(drive.event);
1651
- let enteredCall = false;
1652
- const settled = await settle(actor, (snapshot) => {
1653
- if (atState(playbook.ref)(snapshot))
1654
- enteredCall = true;
1655
- return ((calls === 1 && atState(target)(snapshot)) ||
1656
- (calls === 0 && actor.coverageErrors.length > 0) ||
1657
- (enteredCall && calls === 0 && leftState(playbook.ref)(snapshot)));
2022
+ let satisfiable = false;
2023
+ const result = await searchCoverageEntry(machine, node, refs, captains, playbookRefs(machine.config), candidateValues, async (actor, pending) => {
2024
+ const outputs = coverageArmResults(machine, node, outcome, armIndex, actor.getSnapshot().context, pending.input, candidateValues, undefined, undefined, false, true);
2025
+ if (outputs.length === 0)
2026
+ return false;
2027
+ satisfiable = true;
2028
+ pending.complete(outputs[0]);
2029
+ return await settle(actor, atState(target), PARALLEL_SETTLE_MS);
1658
2030
  });
1659
- const finalSnapshot = actor.getSnapshot();
1660
- const observedStartFailure = actor.coverageErrors.find(({ eventType }) => eventType === `xstate.error.actor.${actorId}` || calls === 0);
1661
- const inputFailure = calls === 0
1662
- ? observedStartFailure === undefined
1663
- ? finalSnapshot.status === 'error'
1664
- ? coverageErrorMessage(finalSnapshot.error)
1665
- : playbookInputFailure(playbook, finalSnapshot.context)
1666
- : coverageErrorMessage(observedStartFailure.error)
1667
- : undefined;
1668
- actor.stop();
1669
- if (inputFailure !== undefined) {
1670
- findings.push(`state ${playbook.ref.stableId}: nested playbook actor failed to start during ${outcome} coverage: ${inputFailure}`);
2031
+ if (result.covered)
1671
2032
  continue;
2033
+ const label = `state ${playbook.ref.stableId}: nested playbook ${outcome} arm ${armIndex}`;
2034
+ if (result.limitation !== undefined)
2035
+ findings.push(`${label}: ${result.limitation}`);
2036
+ else if (result.inputFailure !== undefined && !result.entered)
2037
+ findings.push(`state ${playbook.ref.stableId}: nested playbook actor failed to start during ${outcome} coverage: ${result.inputFailure}`);
2038
+ else if (!result.entered)
2039
+ findings.push(`${label} has no reachable entry under probing (dead or unsupported entry path)`);
2040
+ else if (!satisfiable)
2041
+ findings.push(`${label} is unsatisfiable under probing`);
2042
+ else
2043
+ findings.push(`${label} did not reach ${target.stableId}`);
2044
+ }
2045
+ return findings;
2046
+ }
2047
+ /** No preemption is needed to audit ordinary acting outcomes and one reply. */
2048
+ async function probeNonPreemptiveActor(machine, captain, refs, captains, playbooks, candidates) {
2049
+ const node = { kind: 'acting', value: captain };
2050
+ const findings = [];
2051
+ const stateId = captainPublicStateId(captain);
2052
+ const label = `state ${stateId}`;
2053
+ const search = (check) => searchCoverageEntry(machine, node, refs, captains, playbooks, candidates, check);
2054
+ const detail = (result) => result.limitation ??
2055
+ (result.inputFailure !== undefined
2056
+ ? `actor failed to start: ${result.inputFailure}`
2057
+ : result.entered
2058
+ ? 'no candidate completed at the declared target under reached-context probing'
2059
+ : 'no reachable entry under probing (dead or unsupported entry path)');
2060
+ for (const key of Object.keys(captain.binding.result)) {
2061
+ await coverageYield();
2062
+ const check = (blankReply) => search(async (actor, pending) => {
2063
+ for (const [index, arm] of transitionArms(captain.invocation.onDone).entries()) {
2064
+ const outputs = coverageArmResults(machine, node, 'onDone', index, actor.getSnapshot().context, pending.input, candidates, undefined, key, false, true);
2065
+ if (outputs.length === 0)
2066
+ continue;
2067
+ const targetName = rawArmTarget(arm);
2068
+ const target = targetName === undefined
2069
+ ? undefined
2070
+ : stateRefForTarget(refs, targetName, captain.ref);
2071
+ if (target === undefined || sameStateRef(target, captain.ref))
2072
+ return false;
2073
+ pending.complete(outputs[0]);
2074
+ if (!(await settle(actor, atState(target), PARALLEL_SETTLE_MS)))
2075
+ return false;
2076
+ if (key !== NEEDS_BOSS_REPLY)
2077
+ return true;
2078
+ if (target.stableId !== AWAIT_BOSS_REPLY_STATE &&
2079
+ !tagsOf(target.state).includes('playbook.parked'))
2080
+ return false;
2081
+ if (!blankReply && target.stableId !== AWAIT_BOSS_REPLY_STATE) {
2082
+ actor.send({
2083
+ type: BOSS_REPLY_EVENT,
2084
+ questionId: 'coverage-unknown-question',
2085
+ answer: 'This answer belongs to no pending branch.',
2086
+ });
2087
+ await settleNoTransition();
2088
+ if (!atState(target)(actor.getSnapshot()))
2089
+ return false;
2090
+ }
2091
+ actor.send({
2092
+ type: BOSS_REPLY_EVENT,
2093
+ questionId: stateId,
2094
+ answer: blankReply ? ' ' : 'Proceed as planned.',
2095
+ });
2096
+ if (blankReply) {
2097
+ await settleNoTransition();
2098
+ return !atState(captain.ref)(actor.getSnapshot());
2099
+ }
2100
+ return await settle(actor, atState(captain.ref), PARALLEL_SETTLE_MS);
2101
+ }
2102
+ return false;
2103
+ });
2104
+ const result = await check(false);
2105
+ if (!result.covered)
2106
+ findings.push(`${label}: result "${key}" remains unproved: ${detail(result)}`);
2107
+ if (key === NEEDS_BOSS_REPLY && result.covered) {
2108
+ const blank = await check(true);
2109
+ if (!blank.covered)
2110
+ findings.push(`${label}: a blank ${BOSS_REPLY_EVENT} answer must not resume the state (${detail(blank)})`);
1672
2111
  }
1673
- if (!settled || calls !== 1 || !atState(target)(finalSnapshot)) {
1674
- findings.push(`state ${playbook.ref.stableId}: nested playbook ${outcome} arm ${armIndex} did not reach ${target.stableId}`);
2112
+ }
2113
+ for (const outcome of ['onDone', 'onError']) {
2114
+ await coverageYield();
2115
+ const arms = transitionArms(captain.invocation[outcome]);
2116
+ if (outcome === 'onError' && arms.length === 0)
2117
+ findings.push(`${label} declares no onError transition`);
2118
+ for (const [index, arm] of arms.entries()) {
2119
+ await coverageYield();
2120
+ const rawGuard = armGuard(arm);
2121
+ if (outcome === 'onDone' && rawGuard === undefined)
2122
+ continue;
2123
+ if (rawGuard !== undefined &&
2124
+ resolveGuard(machine, rawGuard) === undefined) {
2125
+ findings.push(`${label}: ${outcome} arm ${index} names an unresolvable guard "${guardLabel(rawGuard)}"`);
2126
+ continue;
2127
+ }
2128
+ const targetName = rawArmTarget(arm);
2129
+ const target = targetName === undefined
2130
+ ? undefined
2131
+ : stateRefForTarget(refs, targetName, captain.ref);
2132
+ if (target === undefined || sameStateRef(target, captain.ref)) {
2133
+ findings.push(`${label}: ${outcome} arm ${index} has no distinct observable target`);
2134
+ continue;
2135
+ }
2136
+ const result = await search(async (actor, pending) => {
2137
+ const outputs = coverageArmResults(machine, node, outcome, index, actor.getSnapshot().context, pending.input, candidates, undefined, undefined, true, true);
2138
+ if (outputs.length === 0)
2139
+ return false;
2140
+ pending.complete(outputs[0]);
2141
+ return await settle(actor, atState(target), PARALLEL_SETTLE_MS);
2142
+ });
2143
+ if (!result.covered)
2144
+ findings.push(`${label}: ${outcome} arm ${index} remains unproved: ${detail(result)}`);
1675
2145
  }
1676
2146
  }
1677
2147
  return findings;
@@ -1761,6 +2231,7 @@ async function probeParallelJoins(machine, parallel, refs, captains, interruptVa
1761
2231
  const normalizedTargets = arms.map((arm) => rawArmTarget(arm) ?? null);
1762
2232
  const findings = [];
1763
2233
  for (const [armIndex, rawTarget] of normalizedTargets.entries()) {
2234
+ await coverageYield();
1764
2235
  if (rawTarget === null) {
1765
2236
  findings.push(`parallel state ${parallel.stableId}: onDone join arm ${armIndex} coverage is unsupported for a target-less arm`);
1766
2237
  continue;
@@ -1773,6 +2244,7 @@ async function probeParallelJoins(machine, parallel, refs, captains, interruptVa
1773
2244
  }
1774
2245
  let exercised = false;
1775
2246
  for (const combination of outputs) {
2247
+ await coverageYield();
1776
2248
  const entries = branches.map((captain, index) => ({
1777
2249
  captain,
1778
2250
  output: combination[index],
@@ -1804,7 +2276,6 @@ async function probeParallelJoins(machine, parallel, refs, captains, interruptVa
1804
2276
  export function fsmCoverageTestTimeout(fsmModule) {
1805
2277
  const machine = findMachine(fsmModule);
1806
2278
  const config = machine.config;
1807
- const states = (config.states ?? {});
1808
2279
  const refs = stateRefs(config);
1809
2280
  const captains = captainRefs(config);
1810
2281
  const playbooks = playbookRefs(config);
@@ -1812,54 +2283,96 @@ export function fsmCoverageTestTimeout(fsmModule) {
1812
2283
  const controller = isControllerMachine(config);
1813
2284
  const rootInterruptProbes = transitionArms((config.on ?? {})[INTERRUPT_EVENT]).length;
1814
2285
  const rootSettles = rootInterruptProbes * SETTLE_MS;
1815
- const initial = typeof config.initial === 'string' ? config.initial : undefined;
1816
2286
  const entryEvents = new Set([
1817
- ...Object.keys(initial === undefined ? {} : (states[initial]?.on ?? {})),
2287
+ ...initialActiveRefs(config, refs).flatMap((ref) => Object.keys(ref.state.on ?? {})),
1818
2288
  ...Object.keys(config.on ?? {}),
1819
2289
  ]);
1820
2290
  entryEvents.delete(INTERRUPT_EVENT);
1821
2291
  const entrySettles = entryEvents.size * SETTLE_MS;
1822
2292
  let captainSettles = 0;
1823
2293
  let guardProbeCalls = rootInterruptProbes;
2294
+ let pathSearches = controller
2295
+ ? 0
2296
+ : playbooks.reduce((total, playbook) => total +
2297
+ transitionArms(playbook.invocation.onDone).length +
2298
+ transitionArms(playbook.invocation.onError).length, 0);
1824
2299
  for (const captain of captains) {
1825
2300
  const resultKeys = Object.keys(captain.binding.result);
2301
+ const guardedDoneArms = transitionArms(captain.invocation.onDone).filter((arm) => armGuard(arm) !== undefined).length;
2302
+ const errorArms = transitionArms(captain.invocation.onError).length;
2303
+ if (!controller && rootInterruptProbes === 0) {
2304
+ pathSearches +=
2305
+ resultKeys.length +
2306
+ (resultKeys.includes(NEEDS_BOSS_REPLY) ? 1 : 0) +
2307
+ guardedDoneArms +
2308
+ errorArms;
2309
+ continue;
2310
+ }
1826
2311
  captainSettles +=
1827
2312
  resultKeys.reduce((total, key) => total + (key === NEEDS_BOSS_REPLY ? 4 : controller ? 2 : 1), 0) * SETTLE_MS;
1828
2313
  // One directly selectable onError arm is driven.
1829
2314
  captainSettles += SETTLE_MS;
1830
- const guardedDoneArms = transitionArms(captain.invocation.onDone).filter((arm) => armGuard(arm) !== undefined).length;
1831
- const errorArms = transitionArms(captain.invocation.onError).length;
1832
- // Result acceptance probes each guarded arm once, then the arm audit probes
1833
- // every result's structured and bare output forms.
1834
- guardProbeCalls += resultKeys.length * guardedDoneArms * 3 + errorArms;
2315
+ // Acceptance includes ordered fallbacks. Script arm probes have one zero
2316
+ // status and a capped set of nonzero statuses; ordinary actors also probe
2317
+ // malformed bare outputs for defensive arms.
2318
+ const resultCandidates = captain.binding.actor === 'script'
2319
+ ? 1 + MAX_SCRIPT_EXIT_STATUSES
2320
+ : resultKeys.length;
2321
+ guardProbeCalls +=
2322
+ resultCandidates * transitionArms(captain.invocation.onDone).length +
2323
+ guardedDoneArms *
2324
+ (captain.binding.actor === 'script'
2325
+ ? resultCandidates
2326
+ : resultKeys.length * 2) +
2327
+ errorArms;
1835
2328
  // Every result, the blank Boss-reply check, and onError enter through an
1836
2329
  // independently context-probed interrupt plan.
1837
2330
  guardProbeCalls +=
1838
2331
  resultKeys.length + (resultKeys.includes(NEEDS_BOSS_REPLY) ? 1 : 0) + 1;
1839
2332
  }
1840
- guardProbeCalls += playbooks.reduce((total, playbook) => total +
1841
- transitionArms(playbook.invocation.onDone).length +
1842
- transitionArms(playbook.invocation.onError).length, 0);
1843
- // Nested success/error and parallel question/join helpers each reuse one
1844
- // context-probed entry plan per state and outcome.
1845
- guardProbeCalls += 2 * playbooks.length + 2 * parallels.length;
1846
- const playbookSettles = playbooks.reduce((total, playbook) => total +
1847
- (transitionArms(playbook.invocation.onDone).length +
1848
- transitionArms(playbook.invocation.onError).length) *
1849
- SETTLE_MS, 0);
2333
+ const nodes = coverageNodes(captains, playbooks);
2334
+ const entryArmCount = [
2335
+ config.on ?? {},
2336
+ ...initialActiveRefs(config, refs).map((ref) => ref.state.on ?? {}),
2337
+ ].reduce((total, events) => total +
2338
+ Object.values(events).reduce((count, arms) => count + transitionArms(arms).length, 0), 0);
2339
+ const maxNodeCandidates = Math.max(0, ...nodes.map((node) => {
2340
+ const done = transitionArms(node.value.invocation.onDone).length;
2341
+ const error = transitionArms(node.value.invocation.onError).length;
2342
+ if (node.kind === 'child')
2343
+ return 3 * (done + error);
2344
+ const keys = Object.keys(node.value.binding.result);
2345
+ const candidates = node.value.binding.actor === 'script'
2346
+ ? keys.length * MAX_SCRIPT_EXIT_STATUSES
2347
+ : 2 * keys.length;
2348
+ // Target malformed-output probes and predecessor question revisits both
2349
+ // fit this allowance; real searches stop early on a satisfying target.
2350
+ return (done * (candidates + (keys.includes(NEEDS_BOSS_REPLY) ? 1 : 0)) + error);
2351
+ }));
2352
+ guardProbeCalls +=
2353
+ pathSearches *
2354
+ (nodes.length * (1 + entryArmCount) +
2355
+ MAX_COVERAGE_PATH_ATTEMPTS * 2 * maxNodeCandidates);
2356
+ // Parallel question/join helpers retain their independent entry probes.
2357
+ guardProbeCalls += 2 * parallels.length;
2358
+ // Each charged replay contains at most eight invocation waits, one parked
2359
+ // question wait and two target/reply settles, plus no-transition checks.
2360
+ const pathSettles = pathSearches *
2361
+ MAX_COVERAGE_PATH_ATTEMPTS *
2362
+ ((MAX_COVERAGE_PATH_STEPS + 3) * PARALLEL_SETTLE_MS + 2 * 10);
1850
2363
  const parallelSettles = parallels.reduce((total, parallel) => total +
1851
2364
  2 * PARALLEL_QUESTION_SETTLE_MS +
1852
2365
  transitionArms(parallel.state.onDone).length *
1853
2366
  MAX_PARALLEL_COMBINATIONS *
1854
2367
  PARALLEL_SETTLE_MS, 0);
1855
2368
  const guardAllowance = guardProbeCalls * Math.ceil(MAX_PROBES / PROBES_PER_TIMEOUT_MILLISECOND);
1856
- return Math.max(MIN_COVERAGE_TEST_TIMEOUT_MS, rootSettles +
2369
+ return Math.min(MAX_COVERAGE_TEST_TIMEOUT_MS, Math.max(MIN_COVERAGE_TEST_TIMEOUT_MS, rootSettles +
1857
2370
  entrySettles +
1858
2371
  captainSettles +
1859
- playbookSettles +
2372
+ pathSettles +
1860
2373
  parallelSettles +
1861
2374
  guardAllowance +
1862
- COVERAGE_TIMEOUT_MARGIN_MS);
2375
+ COVERAGE_TIMEOUT_MARGIN_MS));
1863
2376
  }
1864
2377
  /**
1865
2378
  * Checks transition coverage over a compiled `playbook` artifact's machine
@@ -1869,6 +2382,34 @@ export function fsmCoverageTestTimeout(fsmModule) {
1869
2382
  * surface entirely, in which case interrupt coverage is skipped.
1870
2383
  */
1871
2384
  export async function checkFsmCoverage(fsmModule, opts = {}) {
2385
+ opts.signal?.throwIfAborted();
2386
+ if (opts.timeoutMs !== undefined &&
2387
+ (!Number.isFinite(opts.timeoutMs) || opts.timeoutMs <= 0))
2388
+ throw new RangeError('FSM coverage timeout must be a positive finite number');
2389
+ const started = performance.now();
2390
+ const run = {
2391
+ signal: opts.signal,
2392
+ deadline: started +
2393
+ Math.min(opts.timeoutMs ?? Infinity, MAX_COVERAGE_TEST_TIMEOUT_MS),
2394
+ lastYield: started,
2395
+ actors: new Set(),
2396
+ };
2397
+ return coverageRuns.run(run, async () => {
2398
+ let completed = false;
2399
+ try {
2400
+ run.deadline = Math.min(run.deadline, started + fsmCoverageTestTimeout(fsmModule));
2401
+ await coverageYield(true);
2402
+ const findings = await runFsmCoverage(fsmModule, opts.sourceText ?? '');
2403
+ await coverageYield(true);
2404
+ completed = true;
2405
+ return findings;
2406
+ }
2407
+ finally {
2408
+ stopCoverageActors(run.actors, !completed);
2409
+ }
2410
+ });
2411
+ }
2412
+ async function runFsmCoverage(fsmModule, sourceText) {
1872
2413
  const findings = [];
1873
2414
  const machine = findMachine(fsmModule);
1874
2415
  const config = machine.config;
@@ -1878,8 +2419,8 @@ export async function checkFsmCoverage(fsmModule, opts = {}) {
1878
2419
  const playbooks = playbookRefs(config);
1879
2420
  const captainByRef = new Map(captains.map((captain) => [stateRefKey(captain.ref), captain]));
1880
2421
  const sourceCandidates = [
1881
- ...identifierLiterals(opts.sourceText ?? ''),
1882
- ...numericLiterals(opts.sourceText ?? ''),
2422
+ ...identifierLiterals(sourceText),
2423
+ ...numericLiterals(sourceText),
1883
2424
  ];
1884
2425
  const isController = isControllerMachine(config);
1885
2426
  const controllerNearMisses = captains.flatMap((captain) => {
@@ -1896,12 +2437,14 @@ export async function checkFsmCoverage(fsmModule, opts = {}) {
1896
2437
  const parallelRefs = refs.filter((ref) => ref.state.type === 'parallel');
1897
2438
  findings.push(...repeatedParallelRoleFindings(refs, captains));
1898
2439
  for (const { captain, nearMiss } of controllerNearMisses) {
2440
+ await coverageYield();
1899
2441
  const detail = nearMiss.missing.length > 0
1900
2442
  ? `missing ${JSON.stringify(nearMiss.missing[0])}`
1901
2443
  : `extra ${JSON.stringify(nearMiss.extra[0])}`;
1902
2444
  findings.push(`state ${captain.binding.stateId}: controller decision contract near-miss (${detail}); the controller domain requires exactly ${CONTROLLER_ACTION_GUARDS.join(', ')}`);
1903
2445
  }
1904
2446
  for (const ref of parallelRefs) {
2447
+ await coverageYield();
1905
2448
  if (!normalizeArms(ref.state.onDone).some((arm) => arm.target !== null)) {
1906
2449
  findings.push(`parallel state ${ref.stableId} declares no onDone join`);
1907
2450
  }
@@ -1942,11 +2485,13 @@ export async function checkFsmCoverage(fsmModule, opts = {}) {
1942
2485
  if (!canJump && handlesInterruptSomewhere && !controllerContractCandidate) {
1943
2486
  findings.push(`machine declares no root ${INTERRUPT_EVENT} event`);
1944
2487
  }
1945
- if (canJump && !isController) {
2488
+ if (!isController) {
1946
2489
  for (const playbook of playbooks) {
2490
+ await coverageYield();
1947
2491
  findings.push(...(await probePlaybookInvocation(machine, playbook, refs, captains, sourceCandidates)));
1948
2492
  }
1949
- for (const parallel of parallelRefs) {
2493
+ for (const parallel of canJump ? parallelRefs : []) {
2494
+ await coverageYield();
1950
2495
  findings.push(...(await probeParallelQuestions(machine, parallel, refs, captains, sourceCandidates)), ...(await probeParallelJoins(machine, parallel, refs, captains, sourceCandidates)));
1951
2496
  }
1952
2497
  }
@@ -1963,6 +2508,7 @@ export async function checkFsmCoverage(fsmModule, opts = {}) {
1963
2508
  // captain state parks in it).
1964
2509
  if (canJump) {
1965
2510
  for (const [armIndex, arm] of rootArms.entries()) {
2511
+ await coverageYield();
1966
2512
  if (arm.target === null)
1967
2513
  continue;
1968
2514
  const target = stateRefForTarget(refs, arm.target);
@@ -1997,6 +2543,7 @@ export async function checkFsmCoverage(fsmModule, opts = {}) {
1997
2543
  const initialRef = initialPath.at(-1);
1998
2544
  const entryArms = Object.assign({}, config.on ?? {}, ...initialPath.map((ref) => ref.state.on ?? {}));
1999
2545
  for (const [event, raw] of Object.entries(entryArms)) {
2546
+ await coverageYield();
2000
2547
  if (event === INTERRUPT_EVENT)
2001
2548
  continue;
2002
2549
  const arms = normalizeArms(raw);
@@ -2026,8 +2573,11 @@ export async function checkFsmCoverage(fsmModule, opts = {}) {
2026
2573
  if (unsupportedController)
2027
2574
  return findings;
2028
2575
  for (const captain of captains) {
2029
- if (!canJump && !isController)
2030
- break;
2576
+ await coverageYield();
2577
+ if (!canJump && !isController) {
2578
+ findings.push(...(await probeNonPreemptiveActor(machine, captain, refs, captains, playbooks, [...stateCandidates, ...sourceCandidates])));
2579
+ continue;
2580
+ }
2031
2581
  const state = captain.binding;
2032
2582
  const stateKey = state.stateId;
2033
2583
  const stateId = captainPublicStateId(captain);
@@ -2037,23 +2587,21 @@ export async function checkFsmCoverage(fsmModule, opts = {}) {
2037
2587
  const rawDoneArms = transitionArms(captain.invocation.onDone);
2038
2588
  const onDoneArms = normalizeArms(captain.invocation.onDone);
2039
2589
  const controllerResultByArm = new Map();
2040
- // Every declared result needs an arm that explicitly accepts its complete
2041
- // valid output. A sole unguarded arm accepts the whole local result
2042
- // contract; an array's unguarded arm is a fallback and cannot make an
2043
- // otherwise orphaned key look covered.
2590
+ // Every declared result needs an ordered arm accepting its valid output.
2591
+ // A fallback is reachable exactly when all preceding guards reject it.
2592
+ // Result names alone cannot distinguish authored failure from an orphan.
2044
2593
  for (const key of Object.keys(state.result)) {
2594
+ await coverageYield();
2045
2595
  const output = synthOutput(state, key);
2046
2596
  const accepting = new Map();
2597
+ const acceptedOutputs = new Map();
2047
2598
  for (const [index, arm] of rawDoneArms.entries()) {
2599
+ await coverageYield();
2048
2600
  const target = onDoneArms[index]?.target ?? null;
2049
2601
  const rawGuard = armGuard(arm);
2050
- if (rawGuard === undefined) {
2051
- if (rawDoneArms.length === 1 && target !== null) {
2052
- accepting.set(index, undefined);
2053
- }
2054
- continue;
2055
- }
2056
- if (target === null || resolveGuard(machine, rawGuard) === undefined) {
2602
+ if (target === null ||
2603
+ (rawGuard !== undefined &&
2604
+ resolveGuard(machine, rawGuard) === undefined)) {
2057
2605
  continue;
2058
2606
  }
2059
2607
  const guard = orderedArmPredicate(machine, rawDoneArms, index);
@@ -2064,8 +2612,18 @@ export async function checkFsmCoverage(fsmModule, opts = {}) {
2064
2612
  if (assignment !== undefined)
2065
2613
  accepting.set(index, assignment);
2066
2614
  }
2067
- else if (doneGuardSatisfiable(guard, doneEvent, output, candidates)) {
2068
- accepting.set(index, undefined);
2615
+ else {
2616
+ const outputs = resultOutputCandidates(state, key, [
2617
+ ...(resolveGuard(machine, rawGuard)?.probeValues ?? []),
2618
+ ...candidates,
2619
+ ]);
2620
+ for (const candidate of outputs) {
2621
+ await coverageYield();
2622
+ if (doneGuardSatisfiable(guard, doneEvent, candidate, candidates, state.actor === 'script')) {
2623
+ accepting.set(index, undefined);
2624
+ acceptedOutputs.set(JSON.stringify(candidate), candidate);
2625
+ }
2626
+ }
2069
2627
  }
2070
2628
  }
2071
2629
  if (accepting.size === 0) {
@@ -2093,37 +2651,15 @@ export async function checkFsmCoverage(fsmModule, opts = {}) {
2093
2651
  // initial context is a known accepting arm. Encountering an unresolved
2094
2652
  // guard first makes driving unsafe: XState reports that error
2095
2653
  // asynchronously, so the arm audit below owns the finding (c887fc4).
2096
- let safeToDrive = true;
2097
- for (const [index, arm] of rawDoneArms.entries()) {
2098
- const rawGuard = armGuard(arm);
2099
- if (rawGuard === undefined) {
2100
- directArm = index;
2101
- break;
2102
- }
2103
- const guard = resolveGuard(machine, rawGuard);
2104
- if (guard === undefined) {
2105
- safeToDrive = false;
2106
- break;
2107
- }
2108
- try {
2109
- if (guard.run({
2110
- context: initialCoverageContext,
2111
- event: { ...doneEvent, output },
2112
- })) {
2113
- directArm = index;
2114
- break;
2115
- }
2116
- }
2117
- catch {
2118
- safeToDrive = false;
2654
+ for (const candidate of acceptedOutputs.values()) {
2655
+ await coverageYield();
2656
+ const selected = directlySelectedArm(machine, captain, candidate, initialCoverageContext);
2657
+ if (selected !== undefined && accepting.has(selected)) {
2658
+ directArm = selected;
2659
+ drivenOutput = candidate;
2119
2660
  break;
2120
2661
  }
2121
2662
  }
2122
- if (!safeToDrive ||
2123
- directArm === undefined ||
2124
- !accepting.has(directArm)) {
2125
- continue;
2126
- }
2127
2663
  }
2128
2664
  if (directArm === undefined) {
2129
2665
  continue;
@@ -2251,9 +2787,11 @@ export async function checkFsmCoverage(fsmModule, opts = {}) {
2251
2787
  actor.stop();
2252
2788
  }
2253
2789
  // Every onDone arm is satisfiable under the actual done-event identity.
2254
- // Try each key's full output and bare malformed form; neither probe may
2255
- // invent a different event type or actor id.
2790
+ // Ordinary actors also probe bare malformed forms for authored defensive
2791
+ // arms. Scripts resolve only their exact runtime guard/exit-status pair;
2792
+ // invented payload fields or impossible statuses cannot cover an arm.
2256
2793
  for (const [index, arm] of rawDoneArms.entries()) {
2794
+ await coverageYield();
2257
2795
  const rawGuard = armGuard(arm);
2258
2796
  if (rawGuard === undefined)
2259
2797
  continue;
@@ -2267,8 +2805,15 @@ export async function checkFsmCoverage(fsmModule, opts = {}) {
2267
2805
  // ordered reachability of later arms unsafe to evaluate.
2268
2806
  if (guard === undefined)
2269
2807
  continue;
2270
- const anyOutput = Object.keys(state.result).some((key) => doneGuardSatisfiable(guard, doneEvent, synthOutput(state, key), candidates) ||
2271
- doneGuardSatisfiable(guard, doneEvent, { guard: key }, candidates));
2808
+ const anyOutput = Object.keys(state.result).some((key) => {
2809
+ const outputs = resultOutputCandidates(state, key, [
2810
+ ...declaredGuard.probeValues,
2811
+ ...candidates,
2812
+ ]);
2813
+ return (outputs.some((candidate) => doneGuardSatisfiable(guard, doneEvent, candidate, candidates, state.actor === 'script')) ||
2814
+ (state.actor !== 'script' &&
2815
+ doneGuardSatisfiable(guard, doneEvent, { guard: key }, candidates)));
2816
+ });
2272
2817
  if (!anyOutput) {
2273
2818
  findings.push(`state ${stateKey}: onDone arm ${index} (target ${onDoneArms[index]?.target ?? 'none'}) is unsatisfiable under probing`);
2274
2819
  }
@@ -2285,6 +2830,7 @@ export async function checkFsmCoverage(fsmModule, opts = {}) {
2285
2830
  const forcedError = new Error('coverage: forced captain failure');
2286
2831
  let hasUnresolvableErrorGuard = false;
2287
2832
  for (const [index, arm] of rawErrorArms.entries()) {
2833
+ await coverageYield();
2288
2834
  const rawGuard = armGuard(arm);
2289
2835
  if (rawGuard !== undefined) {
2290
2836
  const declaredGuard = resolveGuard(machine, rawGuard);
@@ -2306,6 +2852,7 @@ export async function checkFsmCoverage(fsmModule, opts = {}) {
2306
2852
  let directErrorArm;
2307
2853
  let errorDriveSafe = true;
2308
2854
  for (const [index, arm] of rawErrorArms.entries()) {
2855
+ await coverageYield();
2309
2856
  const rawGuard = armGuard(arm);
2310
2857
  if (rawGuard === undefined) {
2311
2858
  directErrorArm = index;
@@ -2326,9 +2873,13 @@ export async function checkFsmCoverage(fsmModule, opts = {}) {
2326
2873
  }
2327
2874
  }
2328
2875
  catch {
2876
+ coverageCheckpoint();
2329
2877
  errorDriveSafe = false;
2330
2878
  break;
2331
2879
  }
2880
+ finally {
2881
+ coverageCheckpoint();
2882
+ }
2332
2883
  }
2333
2884
  if (!errorDriveSafe || directErrorArm === undefined)
2334
2885
  continue;