@sublang/slc 0.4.0 → 0.6.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 (120) hide show
  1. package/README.md +100 -131
  2. package/dist/app.d.ts +20 -20
  3. package/dist/app.d.ts.map +1 -1
  4. package/dist/app.js +37 -30
  5. package/dist/app.js.map +1 -1
  6. package/dist/artifacts.d.ts +6 -6
  7. package/dist/artifacts.d.ts.map +1 -1
  8. package/dist/artifacts.js +9 -9
  9. package/dist/artifacts.js.map +1 -1
  10. package/dist/cli.js +3 -3
  11. package/dist/cligent-agent.d.ts +1 -1
  12. package/dist/cligent-agent.js +5 -5
  13. package/dist/compiled-executor.d.ts +14 -6
  14. package/dist/compiled-executor.d.ts.map +1 -1
  15. package/dist/compiled-executor.js +133 -31
  16. package/dist/compiled-executor.js.map +1 -1
  17. package/dist/config-file.d.ts +10 -6
  18. package/dist/config-file.d.ts.map +1 -1
  19. package/dist/config-file.js +23 -10
  20. package/dist/config-file.js.map +1 -1
  21. package/dist/config.d.ts +22 -18
  22. package/dist/config.d.ts.map +1 -1
  23. package/dist/config.js +90 -27
  24. package/dist/config.js.map +1 -1
  25. package/dist/emitted-imports.d.ts +14 -0
  26. package/dist/emitted-imports.d.ts.map +1 -1
  27. package/dist/emitted-imports.js +63 -7
  28. package/dist/emitted-imports.js.map +1 -1
  29. package/dist/entry-module.d.ts +5 -3
  30. package/dist/entry-module.d.ts.map +1 -1
  31. package/dist/entry-module.js +41 -13
  32. package/dist/entry-module.js.map +1 -1
  33. package/dist/execution.d.ts +10 -10
  34. package/dist/execution.d.ts.map +1 -1
  35. package/dist/execution.js +13 -13
  36. package/dist/execution.js.map +1 -1
  37. package/dist/hash.js +2 -2
  38. package/dist/host-capabilities.d.ts +115 -0
  39. package/dist/host-capabilities.d.ts.map +1 -0
  40. package/dist/host-capabilities.js +507 -0
  41. package/dist/host-capabilities.js.map +1 -0
  42. package/dist/index.d.ts +1 -0
  43. package/dist/index.d.ts.map +1 -1
  44. package/dist/index.js +1 -0
  45. package/dist/index.js.map +1 -1
  46. package/dist/interpreter.d.ts +2 -2
  47. package/dist/interpreter.d.ts.map +1 -1
  48. package/dist/interpreter.js +7 -7
  49. package/dist/invocation.d.ts +6 -6
  50. package/dist/invocation.d.ts.map +1 -1
  51. package/dist/invocation.js +1 -1
  52. package/dist/link.d.ts +4 -4
  53. package/dist/link.d.ts.map +1 -1
  54. package/dist/link.js +11 -11
  55. package/dist/link.js.map +1 -1
  56. package/dist/phase-runner.d.ts +4 -4
  57. package/dist/phase-runner.js +9 -9
  58. package/dist/phase.d.ts +4 -4
  59. package/dist/phase.d.ts.map +1 -1
  60. package/dist/phase.js +7 -7
  61. package/dist/phase.js.map +1 -1
  62. package/dist/pin-closure.d.ts +35 -7
  63. package/dist/pin-closure.d.ts.map +1 -1
  64. package/dist/pin-closure.js +135 -21
  65. package/dist/pin-closure.js.map +1 -1
  66. package/dist/pin-currency.d.ts +9 -4
  67. package/dist/pin-currency.d.ts.map +1 -1
  68. package/dist/pin-currency.js +57 -20
  69. package/dist/pin-currency.js.map +1 -1
  70. package/dist/pin-generate.d.ts +7 -7
  71. package/dist/pin-generate.d.ts.map +1 -1
  72. package/dist/pin-generate.js +14 -8
  73. package/dist/pin-generate.js.map +1 -1
  74. package/dist/pin-inputs.d.ts +41 -0
  75. package/dist/pin-inputs.d.ts.map +1 -0
  76. package/dist/pin-inputs.js +179 -0
  77. package/dist/pin-inputs.js.map +1 -0
  78. package/dist/pin-paths.d.ts +1 -1
  79. package/dist/pin-paths.js +4 -4
  80. package/dist/pin-paths.js.map +1 -1
  81. package/dist/pins.d.ts +4 -4
  82. package/dist/pins.d.ts.map +1 -1
  83. package/dist/pins.js +5 -5
  84. package/dist/pins.js.map +1 -1
  85. package/dist/pipeline.d.ts +5 -5
  86. package/dist/pipeline.js +10 -10
  87. package/dist/playbook-contract.d.ts +53 -5
  88. package/dist/playbook-contract.d.ts.map +1 -1
  89. package/dist/playbook-contract.js +70 -21
  90. package/dist/playbook-contract.js.map +1 -1
  91. package/dist/playbook-ports.d.ts +9 -9
  92. package/dist/playbook-ports.d.ts.map +1 -1
  93. package/dist/playbook-ports.js +6 -4
  94. package/dist/playbook-ports.js.map +1 -1
  95. package/dist/progress.d.ts +8 -9
  96. package/dist/progress.d.ts.map +1 -1
  97. package/dist/progress.js +3 -3
  98. package/dist/progress.js.map +1 -1
  99. package/dist/resolver.d.ts +9 -9
  100. package/dist/resolver.d.ts.map +1 -1
  101. package/dist/resolver.js +12 -12
  102. package/dist/resolver.js.map +1 -1
  103. package/dist/reviewing-agent.d.ts +18 -0
  104. package/dist/reviewing-agent.d.ts.map +1 -0
  105. package/dist/reviewing-agent.js +295 -0
  106. package/dist/reviewing-agent.js.map +1 -0
  107. package/dist/runner.d.ts +1 -1
  108. package/dist/runner.d.ts.map +1 -1
  109. package/dist/runner.js +195 -80
  110. package/dist/runner.js.map +1 -1
  111. package/dist/slc.config.template.yaml +6 -2
  112. package/dist/verify-coverage.d.ts +4 -3
  113. package/dist/verify-coverage.d.ts.map +1 -1
  114. package/dist/verify-coverage.js +509 -71
  115. package/dist/verify-coverage.js.map +1 -1
  116. package/dist/verify.d.ts +111 -15
  117. package/dist/verify.d.ts.map +1 -1
  118. package/dist/verify.js +656 -59
  119. package/dist/verify.js.map +1 -1
  120. package/package.json +3 -3
@@ -2,25 +2,27 @@
2
2
  // SPDX-FileCopyrightText: 2026 SubLang International <https://sublang.ai>
3
3
  /**
4
4
  * FSM transition-coverage verification for a compiled `playbook` artifact
5
- * (VERIFY-6; DR-009).
5
+ * (verification-6; DR-009).
6
6
  *
7
7
  * {@link checkFsmCoverage} drives the artifact's machine with scripted Captain
8
8
  * and nested-playbook actors and returns findings when a declared transition
9
- * is not reachable: every Captain result key must fire a transition out (with
10
- * `needsBossReply` suspending in the Boss-reply wait state and resuming on
11
- * `BOSS_REPLY`), nested calls must transition on success and failure, every
12
- * `onError` arm must land on its target, every `BOSS_INTERRUPT` target must be
13
- * enterable, and guard-free root entry events must transition.
9
+ * is not reachable: every ordinary Captain result key must fire a transition
10
+ * out (with `needsBossReply` suspending in the Boss-reply wait state and
11
+ * resuming on `BOSS_REPLY`), every controller action must return to its session
12
+ * hub or shutdown final without a wait, nested calls must transition on success
13
+ * and failure, every `onError` arm must land on its target, every
14
+ * `BOSS_INTERRUPT` target must be enterable, and guard-free root entry events
15
+ * must transition.
14
16
  * Context-dependent `onDone` arms that a jumped-in actor cannot satisfy are
15
17
  * covered by deterministic guard-satisfiability probing — candidate values
16
18
  * mined from the guard's own source — so an unsatisfiable arm is still flagged.
17
19
  * The checker needs only the artifact and `xstate`; the emitted per-artifact
18
- * test runs it beside the artifacts. See specs/dev/verification.md.
20
+ * test runs it beside the artifacts. See specs/packages/verification.md.
19
21
  */
20
22
  import { mkdir, readFile, writeFile } from 'node:fs/promises';
21
23
  import { join } from 'node:path';
22
24
  import { createActor, fromPromise } from 'xstate';
23
- import { AWAIT_BOSS_REPLY_STATE, BOSS_REPLY_EVENT, INTERRUPT_EVENT, NEEDS_BOSS_REPLY, VERIFY_MODULE, enumerateCaptainStates, enumerateScriptStates, loadFsmModule, normalizeArms, } from './verify.js';
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';
24
26
  /** The `gears2fsm`-mandated captain actor name a machine declares. */
25
27
  export const CAPTAIN_ACTOR = 'captain';
26
28
  function stateRefKey(ref) {
@@ -200,20 +202,31 @@ function initializedCoverageContext(machine, dynamic) {
200
202
  ...coverageGuardContext(dynamic),
201
203
  };
202
204
  }
203
- /** Payload fields a result description requires, per the adjudicator convention. */
204
- const REQUIRED_FIELD = /Output shall include `([A-Za-z_][A-Za-z0-9_]*):/g;
205
+ /** The `Output shall include` clause a result description declares. */
206
+ const OUTPUT_CLAUSE = /Output shall include\s+(.+)$/;
207
+ /** Every backticked `field:` property named inside that clause. */
208
+ const REQUIRED_FIELD = /`([A-Za-z_$][A-Za-z0-9_$]*):/g;
205
209
  /** Structured fields named by the generic Captain result contracts. */
206
210
  const STRUCTURED_RESULT_FIELD = /\b(response|question|remainingPlan|nextPlaybookId|nextPlaybookInput)\b/g;
207
211
  function requiredFields(description) {
212
+ const clause = OUTPUT_CLAUSE.exec(description)?.[1] ?? '';
208
213
  return [
209
214
  ...new Set([
210
- ...[...description.matchAll(REQUIRED_FIELD)].map((match) => match[1]),
215
+ ...[...clause.matchAll(REQUIRED_FIELD)].map((match) => match[1]),
211
216
  ...[...description.matchAll(STRUCTURED_RESULT_FIELD)].map((match) => match[1]),
212
217
  ]),
213
218
  ];
214
219
  }
215
220
  function synthesizedFieldValue(field) {
216
221
  switch (field) {
222
+ case 'text':
223
+ return COVERAGE_FINAL_RESPONSE;
224
+ case 'playbookId':
225
+ return COVERAGE_PLAYBOOK_ID;
226
+ case 'input':
227
+ return COVERAGE_PLAYBOOK_INPUT;
228
+ case 'actionId':
229
+ return 'coverage-runtime-action';
217
230
  case 'question':
218
231
  return 'What should happen next?';
219
232
  case 'remainingPlan':
@@ -278,6 +291,21 @@ function stateRefs(config) {
278
291
  visit((config.states ?? {}));
279
292
  return out;
280
293
  }
294
+ /** The root initial state followed through every compound initial child. */
295
+ function initialActiveRefs(config, refs) {
296
+ const active = [];
297
+ let path = [];
298
+ let initial = config.initial;
299
+ while (typeof initial === 'string') {
300
+ path = [...path, initial];
301
+ const ref = refs.find((candidate) => candidate.path.join('\u0000') === path.join('\u0000'));
302
+ if (ref === undefined)
303
+ break;
304
+ active.push(ref);
305
+ initial = ref.state.initial;
306
+ }
307
+ return active;
308
+ }
281
309
  /** Captain bindings paired with the nested state node that owns the invoke. */
282
310
  function captainRefs(config) {
283
311
  const out = [];
@@ -361,7 +389,7 @@ function playbookRefs(config) {
361
389
  ? [{ invocation, invocationIndex, ref }]
362
390
  : []));
363
391
  }
364
- // Local copy: this module is copied verbatim beside the artifact (VERIFY-12),
392
+ // Local copy: this module is copied verbatim beside the artifact (verification-12),
365
393
  // so it may not import a sibling module.
366
394
  function coverageErrorMessage(error) {
367
395
  try {
@@ -454,7 +482,7 @@ function persistedSnapshotWithContext(provided, context) {
454
482
  seed.stop();
455
483
  }
456
484
  }
457
- function makeActor(machine, script, playbookScript = () => null, restoredContext) {
485
+ function makeActor(machine, script, playbookScript = () => null, restoredContext, observeTransition) {
458
486
  const coverageErrors = [];
459
487
  const workActor = fromPromise(async ({ input }) => {
460
488
  const output = script(input ?? {});
@@ -493,6 +521,32 @@ function makeActor(machine, script, playbookScript = () => null, restoredContext
493
521
  !('event' in inspection)) {
494
522
  return;
495
523
  }
524
+ const inspectedTransitions = inspection
525
+ ._transitions;
526
+ if (inspection.type === '@xstate.microstep' &&
527
+ Array.isArray(inspectedTransitions)) {
528
+ for (const transition of inspectedTransitions) {
529
+ if (typeof transition !== 'object' || transition === null) {
530
+ continue;
531
+ }
532
+ const source = transition.source;
533
+ const target = transition.target;
534
+ observeTransition?.({
535
+ ...(typeof source === 'object' &&
536
+ source !== null &&
537
+ typeof source.id === 'string'
538
+ ? { sourceId: source.id }
539
+ : {}),
540
+ targetIds: Array.isArray(target)
541
+ ? target.flatMap((candidate) => typeof candidate === 'object' &&
542
+ candidate !== null &&
543
+ typeof candidate.id === 'string'
544
+ ? [candidate.id]
545
+ : [])
546
+ : [],
547
+ });
548
+ }
549
+ }
496
550
  const event = inspection.event;
497
551
  if (typeof event !== 'object' || event === null)
498
552
  return;
@@ -730,6 +784,55 @@ function interruptDriveForRef(machine, refs, target, targetId, extraValues = [],
730
784
  satisfiable: true,
731
785
  };
732
786
  }
787
+ /** A bounded satisfying initial/root event that enters one Captain state. */
788
+ function entryDriveForRef(machine, refs, target, extraValues = [], initialContext = initializedCoverageContext(machine)) {
789
+ const initialPath = initialActiveRefs(machine.config, refs);
790
+ const surfaces = [
791
+ ...initialPath
792
+ .slice()
793
+ .reverse()
794
+ .map((source) => ({ source, events: source.state.on ?? {} })),
795
+ { source: undefined, events: machine.config.on ?? {} },
796
+ ];
797
+ for (const { source, events } of surfaces) {
798
+ for (const [eventType, raw] of Object.entries(events)) {
799
+ if (eventType === INTERRUPT_EVENT)
800
+ continue;
801
+ const arms = transitionArms(raw);
802
+ for (const [armIndex, arm] of arms.entries()) {
803
+ const rawTarget = rawArmTarget(arm);
804
+ if (rawTarget === undefined ||
805
+ !sameStateRef(stateRefForTarget(refs, rawTarget, source), target)) {
806
+ continue;
807
+ }
808
+ const guard = orderedArmPredicate(machine, arms, armIndex);
809
+ if (guard === undefined)
810
+ continue;
811
+ const base = { type: eventType };
812
+ const assignment = probeGuardAssignment(guard.run, {}, [{ tag: 'e:', base }], [
813
+ ...guard.probeValues,
814
+ ...refs.flatMap((ref) => [
815
+ ref.key,
816
+ ref.stableId,
817
+ ...(ref.configId === undefined ? [] : [ref.configId]),
818
+ ]),
819
+ ...extraValues,
820
+ ], {
821
+ initialContext,
822
+ varyExistingContext: true,
823
+ });
824
+ if (assignment !== undefined) {
825
+ return {
826
+ event: assignedPayload(base, assignment, 'e:'),
827
+ context: assignment.context,
828
+ satisfiable: true,
829
+ };
830
+ }
831
+ }
832
+ }
833
+ }
834
+ return undefined;
835
+ }
733
836
  /** The arm XState selects for one concrete invocation output and context. */
734
837
  function directlySelectedArm(machine, captain, output, context = coverageGuardContext()) {
735
838
  const event = {
@@ -783,6 +886,17 @@ function minedLiterals(fn) {
783
886
  // identifier-like string literals recover them without dragging prompt prose
784
887
  // into the candidate pool.
785
888
  const IDENTIFIER_LITERAL = /^[A-Za-z][A-Za-z0-9_-]{0,31}$/;
889
+ // Bound comparisons (cycle limits, retry counters, exit statuses) route on
890
+ // integers that neither the string miner nor the generic value pool supplies:
891
+ // GENERIC_VALUES carries only 1, and minedLiterals matches quoted text alone. A
892
+ // machine gating on `>= 2` or `=== 0` is therefore reported unsatisfiable purely
893
+ // for want of a candidate. Mining the artifact's own integers recovers exactly
894
+ // the bounds it routes on.
895
+ function numericLiterals(sourceText) {
896
+ return [
897
+ ...new Set((sourceText.match(/(?<![\w.$])\d+(?![\w.$])/g) ?? []).map(Number)),
898
+ ];
899
+ }
786
900
  /** Mines identifier-like string literals from an artifact's source text. */
787
901
  export function identifierLiterals(sourceText) {
788
902
  const literals = sourceText.match(/'(?:[^'\\\n]|\\.)*'|"(?:[^"\\\n]|\\.)*"/g) ?? [];
@@ -955,6 +1069,10 @@ function overlaidObject(base, assigned) {
955
1069
  function doneGuardSatisfiable(guard, event, output, extraValues) {
956
1070
  return probeGuardSatisfiable(guard.run, event, [{ eventField: 'output', tag: 'o:', base: output }], [...guard.probeValues, ...extraValues]);
957
1071
  }
1072
+ /** A concrete initialized-context/output assignment selecting one done arm. */
1073
+ function controllerDoneGuardAssignment(guard, event, output, extraValues, initialContext) {
1074
+ return probeGuardAssignment(guard.run, event, [{ eventField: 'output', tag: 'o:', base: output }], [...guard.probeValues, ...extraValues], { initialContext, varyExistingContext: true });
1075
+ }
958
1076
  function errorGuardSatisfiable(guard, event, error, extraValues) {
959
1077
  return probeGuardSatisfiable(guard.run, event, [{ eventField: 'error', tag: 'r:', base: error, varyExisting: true }], [...guard.probeValues, ...extraValues]);
960
1078
  }
@@ -977,6 +1095,36 @@ function parallelBranchCaptains(parallel, refs, captains) {
977
1095
  }
978
1096
  return selected;
979
1097
  }
1098
+ function repeatedParallelRoleFindings(refs, captains) {
1099
+ const findings = [];
1100
+ for (const parallel of refs.filter((ref) => ref.state.type === 'parallel')) {
1101
+ const regions = refs.filter((ref) => ref.parent === parallel);
1102
+ const priorRoles = new Set();
1103
+ for (const region of regions) {
1104
+ // Sequential appearances inside one region are never concurrent. Fold
1105
+ // the complete descendant region to a set, then compare across siblings;
1106
+ // nested parallel descendants are therefore visible to their ancestors
1107
+ // and are also checked independently at their own parallel boundary.
1108
+ const regionRoles = new Set(captains.flatMap((captain) => {
1109
+ if (captain.binding.actor !== 'player' ||
1110
+ (!sameStateRef(captain.ref, region) &&
1111
+ !isDescendantOf(captain.ref, region))) {
1112
+ return [];
1113
+ }
1114
+ const role = captain.binding.role?.trim().toLowerCase();
1115
+ return role === undefined || role === '' ? [] : [role];
1116
+ }));
1117
+ for (const role of regionRoles) {
1118
+ if (priorRoles.has(role)) {
1119
+ findings.push(`parallel state ${parallel.stableId} repeats canonical role ${role}`);
1120
+ }
1121
+ }
1122
+ for (const role of regionRoles)
1123
+ priorRoles.add(role);
1124
+ }
1125
+ }
1126
+ return [...new Set(findings)];
1127
+ }
980
1128
  function scriptedOutputs(entries) {
981
1129
  const calls = new Map();
982
1130
  const byState = new Map(entries.map(({ captain, output }) => [
@@ -1134,10 +1282,165 @@ function captainPredecessorPlan(machine, target, playbooks, refs, captains) {
1134
1282
  }
1135
1283
  return undefined;
1136
1284
  }
1137
- function captainProbeActor(machine, captain, result, gate, refs, captains, playbooks, interruptValues) {
1285
+ /**
1286
+ * Finds a controller path from a hub event to an acting state. Controller-only
1287
+ * intermediate states are entered through earlier Captain results rather than
1288
+ * through the ordinary workflow interrupt surface.
1289
+ */
1290
+ function controllerCaptainEntryPlan(machine, target, refs, captains, candidates, initialContext) {
1291
+ const queue = [];
1292
+ for (const captain of captains) {
1293
+ const drive = entryDriveForRef(machine, refs, captain.ref, candidates, initialContext);
1294
+ if (drive?.satisfiable === true) {
1295
+ queue.push({ captain, drive, preceding: [] });
1296
+ }
1297
+ }
1298
+ const visited = new Set();
1299
+ while (queue.length > 0) {
1300
+ const current = queue.shift();
1301
+ if (current === undefined)
1302
+ break;
1303
+ const key = stateRefKey(current.captain.ref);
1304
+ if (visited.has(key))
1305
+ continue;
1306
+ visited.add(key);
1307
+ if (sameStateRef(current.captain.ref, target.ref)) {
1308
+ return { drive: current.drive, preceding: current.preceding };
1309
+ }
1310
+ for (const resultKey of Object.keys(current.captain.binding.result)) {
1311
+ const output = synthOutput(current.captain.binding, resultKey);
1312
+ const nextRef = directTargetRef(machine, refs, current.captain, output, initialContext);
1313
+ const nextCaptain = captains.find((candidate) => nextRef !== undefined && sameStateRef(candidate.ref, nextRef));
1314
+ if (nextCaptain !== undefined &&
1315
+ !visited.has(stateRefKey(nextCaptain.ref))) {
1316
+ queue.push({
1317
+ captain: nextCaptain,
1318
+ drive: current.drive,
1319
+ preceding: [
1320
+ ...current.preceding,
1321
+ { captain: current.captain, output },
1322
+ ],
1323
+ });
1324
+ }
1325
+ }
1326
+ }
1327
+ return undefined;
1328
+ }
1329
+ function controllerCaptainExitPlan(machine, captain, output, refs, captains, hub, initialContext) {
1330
+ const firstTarget = directTargetRef(machine, refs, captain, output, initialContext);
1331
+ if (firstTarget === undefined)
1332
+ return { following: [], outcome: 'dead' };
1333
+ if (sameStateRef(firstTarget, hub) || firstTarget.state.type === 'final') {
1334
+ return { following: [], outcome: 'planned' };
1335
+ }
1336
+ const firstCaptain = captains.find((candidate) => sameStateRef(candidate.ref, firstTarget));
1337
+ if (firstCaptain === undefined) {
1338
+ return { following: [], outcome: 'runtime' };
1339
+ }
1340
+ const queue = [{ captain: firstCaptain, following: [] }];
1341
+ const visited = new Set();
1342
+ while (queue.length > 0) {
1343
+ const current = queue.shift();
1344
+ if (current === undefined)
1345
+ break;
1346
+ const currentKey = stateRefKey(current.captain.ref);
1347
+ if (visited.has(currentKey))
1348
+ continue;
1349
+ visited.add(currentKey);
1350
+ for (const resultKey of Object.keys(current.captain.binding.result)) {
1351
+ const nextOutput = synthOutput(current.captain.binding, resultKey);
1352
+ const following = [
1353
+ ...current.following,
1354
+ { captain: current.captain, output: nextOutput },
1355
+ ];
1356
+ const nextRef = directTargetRef(machine, refs, current.captain, nextOutput, initialContext);
1357
+ if (nextRef === undefined)
1358
+ continue;
1359
+ if (sameStateRef(nextRef, hub) || nextRef.state.type === 'final') {
1360
+ return { following, outcome: 'planned' };
1361
+ }
1362
+ const nextCaptain = captains.find((candidate) => sameStateRef(candidate.ref, nextRef));
1363
+ if (nextCaptain !== undefined &&
1364
+ !visited.has(stateRefKey(nextCaptain.ref))) {
1365
+ queue.push({ captain: nextCaptain, following });
1366
+ }
1367
+ }
1368
+ }
1369
+ return { following: [], outcome: 'dead' };
1370
+ }
1371
+ function controllerCaptainProbeActor(machine, captain, result, gate, refs, captains, candidates, initialContext) {
1372
+ const entry = controllerCaptainEntryPlan(machine, captain, refs, captains, candidates, initialContext);
1373
+ if (entry === undefined)
1374
+ return undefined;
1375
+ const hub = initialActiveRefs(machine.config, refs).at(-1);
1376
+ const outcomes = [...entry.preceding, { captain, output: result }];
1377
+ let controllerExitPlanned;
1378
+ if (!(result instanceof Error)) {
1379
+ const exit = controllerCaptainExitPlan(machine, captain, result, refs, captains, hub, initialContext);
1380
+ controllerExitPlanned =
1381
+ exit.outcome === 'runtime' ? undefined : exit.outcome === 'planned';
1382
+ outcomes.push(...exit.following);
1383
+ }
1384
+ const probedOccurrence = entry.preceding.length;
1385
+ let controllerResultConsumed = false;
1386
+ let controllerDirectTransitionTargets;
1387
+ let occurrence = 0;
1388
+ const actor = makeActor(machine, (input) => {
1389
+ if (!gate.armed)
1390
+ return null;
1391
+ const planned = outcomes[occurrence];
1392
+ if (planned === undefined)
1393
+ return null;
1394
+ const stateId = typeof input.stateId === 'string' ? input.stateId : undefined;
1395
+ const sourceItem = typeof input.sourceItem === 'string' ? input.sourceItem : undefined;
1396
+ const matchesState = stateId === undefined ||
1397
+ stateId === captainPublicStateId(planned.captain);
1398
+ const matchesSource = sourceItem === undefined ||
1399
+ sourceItem === planned.captain.binding.sourceItem;
1400
+ if ((stateId === undefined && sourceItem === undefined) ||
1401
+ !matchesState ||
1402
+ !matchesSource) {
1403
+ return null;
1404
+ }
1405
+ if (occurrence === probedOccurrence) {
1406
+ controllerResultConsumed = true;
1407
+ }
1408
+ occurrence += 1;
1409
+ return planned.output;
1410
+ }, () => null, entry.drive.context, (transition) => {
1411
+ if (controllerResultConsumed &&
1412
+ controllerDirectTransitionTargets === undefined &&
1413
+ transition.sourceId === resolvedStateNode(machine, captain.ref)?.id) {
1414
+ controllerDirectTransitionTargets = transition.targetIds;
1415
+ }
1416
+ });
1417
+ return {
1418
+ actor,
1419
+ event: entry.drive.event,
1420
+ controllerDirectTransitionTargets: () => controllerDirectTransitionTargets,
1421
+ ...(controllerExitPlanned === undefined ? {} : { controllerExitPlanned }),
1422
+ };
1423
+ }
1424
+ function captainProbeActor(machine, captain, result, gate, refs, captains, playbooks, interruptValues, controllerContext) {
1425
+ if (controllerContext !== undefined) {
1426
+ return controllerCaptainProbeActor(machine, captain, result, gate, refs, captains, interruptValues, controllerContext);
1427
+ }
1138
1428
  const predecessor = captainPredecessorPlan(machine, captain, playbooks, refs, captains);
1139
1429
  if (predecessor === undefined) {
1140
- const drive = interruptDriveForRef(machine, refs, captain.ref, captainInterruptTarget(captain), interruptValues);
1430
+ // A root BOSS_INTERRUPT makes the interrupt route PREFERRED, not exclusive.
1431
+ // A state legitimately outside the interrupt target set - every script
1432
+ // state, since a script state is not agent-invoking and so cannot receive a
1433
+ // Boss turn - is still entered by its own typed entry event. Falling back to
1434
+ // the entry drive keeps such a state auditable instead of reporting it
1435
+ // unreachable.
1436
+ const interruptDrive = transitionArms((machine.config.on ?? {})[INTERRUPT_EVENT]).length > 0
1437
+ ? interruptDriveForRef(machine, refs, captain.ref, captainInterruptTarget(captain), interruptValues)
1438
+ : undefined;
1439
+ const drive = interruptDrive?.satisfiable === true
1440
+ ? interruptDrive
1441
+ : entryDriveForRef(machine, refs, captain.ref, interruptValues);
1442
+ if (drive === undefined || !drive.satisfiable)
1443
+ return undefined;
1141
1444
  return {
1142
1445
  actor: makeActor(machine, result instanceof Error
1143
1446
  ? throwingScript(captain.binding.sourceItem, gate)
@@ -1155,6 +1458,8 @@ function captainProbeActor(machine, captain, result, gate, refs, captains, playb
1155
1458
  ? interruptTargetForRef(predecessor.playbook.ref, predecessor.playbook.ref.stableId)
1156
1459
  : captainInterruptTarget(predecessor.entry.captain);
1157
1460
  const drive = interruptDriveForRef(machine, refs, entryRef, entryId, interruptValues);
1461
+ if (!drive.satisfiable)
1462
+ return undefined;
1158
1463
  const actor = makeActor(machine, (input) => {
1159
1464
  if (!gate.armed)
1160
1465
  return null;
@@ -1469,6 +1774,7 @@ export function fsmCoverageTestTimeout(fsmModule) {
1469
1774
  const captains = captainRefs(config);
1470
1775
  const playbooks = playbookRefs(config);
1471
1776
  const parallels = refs.filter((ref) => ref.state.type === 'parallel');
1777
+ const controller = isControllerMachine(config);
1472
1778
  const rootInterruptProbes = transitionArms((config.on ?? {})[INTERRUPT_EVENT]).length;
1473
1779
  const rootSettles = rootInterruptProbes * SETTLE_MS;
1474
1780
  const initial = typeof config.initial === 'string' ? config.initial : undefined;
@@ -1483,7 +1789,7 @@ export function fsmCoverageTestTimeout(fsmModule) {
1483
1789
  for (const captain of captains) {
1484
1790
  const resultKeys = Object.keys(captain.binding.result);
1485
1791
  captainSettles +=
1486
- resultKeys.reduce((total, key) => total + (key === NEEDS_BOSS_REPLY ? 4 : 1), 0) * SETTLE_MS;
1792
+ resultKeys.reduce((total, key) => total + (key === NEEDS_BOSS_REPLY ? 4 : controller ? 2 : 1), 0) * SETTLE_MS;
1487
1793
  // One directly selectable onError arm is driven.
1488
1794
  captainSettles += SETTLE_MS;
1489
1795
  const guardedDoneArms = transitionArms(captain.invocation.onDone).filter((arm) => armGuard(arm) !== undefined).length;
@@ -1522,7 +1828,7 @@ export function fsmCoverageTestTimeout(fsmModule) {
1522
1828
  }
1523
1829
  /**
1524
1830
  * Checks transition coverage over a compiled `playbook` artifact's machine
1525
- * (VERIFY-6) and returns findings (empty when every declared transition is
1831
+ * (verification-6) and returns findings (empty when every declared transition is
1526
1832
  * reachable). Drives the machine through the `gears2fsm` surfaces it
1527
1833
  * declares; a workflow without pre-emption may omit the `BOSS_INTERRUPT`
1528
1834
  * surface entirely, in which case interrupt coverage is skipped.
@@ -1536,8 +1842,30 @@ export async function checkFsmCoverage(fsmModule, opts = {}) {
1536
1842
  const captains = captainRefs(config);
1537
1843
  const playbooks = playbookRefs(config);
1538
1844
  const captainByRef = new Map(captains.map((captain) => [stateRefKey(captain.ref), captain]));
1539
- const sourceCandidates = identifierLiterals(opts.sourceText ?? '');
1845
+ const sourceCandidates = [
1846
+ ...identifierLiterals(opts.sourceText ?? ''),
1847
+ ...numericLiterals(opts.sourceText ?? ''),
1848
+ ];
1849
+ const isController = isControllerMachine(config);
1850
+ const controllerNearMisses = captains.flatMap((captain) => {
1851
+ if (captain.binding.actor !== 'captain' ||
1852
+ captain.binding.result[NEEDS_BOSS_REPLY] !== undefined) {
1853
+ return [];
1854
+ }
1855
+ const nearMiss = controllerDecisionNearMiss(captain.binding.result);
1856
+ return nearMiss === undefined ? [] : [{ captain, nearMiss }];
1857
+ });
1858
+ const controllerContractCandidate = isController || controllerNearMisses.length > 0;
1859
+ const initialCoverageContext = initializedCoverageContext(machine);
1860
+ const controllerContext = isController ? initialCoverageContext : undefined;
1540
1861
  const parallelRefs = refs.filter((ref) => ref.state.type === 'parallel');
1862
+ findings.push(...repeatedParallelRoleFindings(refs, captains));
1863
+ for (const { captain, nearMiss } of controllerNearMisses) {
1864
+ const detail = nearMiss.missing.length > 0
1865
+ ? `missing ${JSON.stringify(nearMiss.missing[0])}`
1866
+ : `extra ${JSON.stringify(nearMiss.extra[0])}`;
1867
+ findings.push(`state ${captain.binding.stateId}: controller decision contract near-miss (${detail}); the controller domain requires exactly ${CONTROLLER_ACTION_GUARDS.join(', ')}`);
1868
+ }
1541
1869
  for (const ref of parallelRefs) {
1542
1870
  if (!normalizeArms(ref.state.onDone).some((arm) => arm.target !== null)) {
1543
1871
  findings.push(`parallel state ${ref.stableId} declares no onDone join`);
@@ -1555,6 +1883,20 @@ export async function checkFsmCoverage(fsmModule, opts = {}) {
1555
1883
  if (finalStates.length === 0) {
1556
1884
  findings.push('machine declares no final state');
1557
1885
  }
1886
+ const controllerHasPlayer = isController &&
1887
+ captains.some((captain) => captain.binding.actor === 'player');
1888
+ const controllerHasPlaybook = isController && playbooks.length > 0;
1889
+ const controllerHasParallel = isController && parallelRefs.length > 0;
1890
+ if (controllerHasPlayer) {
1891
+ findings.push('controller machine declares delegated-player invocation');
1892
+ }
1893
+ if (controllerHasPlaybook) {
1894
+ findings.push('controller machine declares nested-playbook invocation');
1895
+ }
1896
+ if (controllerHasParallel) {
1897
+ findings.push('controller machine declares parallel state');
1898
+ }
1899
+ const unsupportedController = controllerHasPlayer || controllerHasPlaybook || controllerHasParallel;
1558
1900
  const rootArms = normalizeArms((config.on ?? {})[INTERRUPT_EVENT]);
1559
1901
  const canJump = rootArms.length > 0;
1560
1902
  // A workflow without pre-emption declares no interrupt surface at all
@@ -1562,10 +1904,10 @@ export async function checkFsmCoverage(fsmModule, opts = {}) {
1562
1904
  // that handles the event somewhere but not at the root is malformed.
1563
1905
  // Prompt text merely mentioning the event name does not count.
1564
1906
  const handlesInterruptSomewhere = refs.some((ref) => Object.hasOwn(ref.state.on ?? {}, INTERRUPT_EVENT));
1565
- if (!canJump && handlesInterruptSomewhere) {
1907
+ if (!canJump && handlesInterruptSomewhere && !controllerContractCandidate) {
1566
1908
  findings.push(`machine declares no root ${INTERRUPT_EVENT} event`);
1567
1909
  }
1568
- if (canJump) {
1910
+ if (canJump && !isController) {
1569
1911
  for (const playbook of playbooks) {
1570
1912
  findings.push(...(await probePlaybookInvocation(machine, playbook, refs, captains, sourceCandidates)));
1571
1913
  }
@@ -1576,7 +1918,10 @@ export async function checkFsmCoverage(fsmModule, opts = {}) {
1576
1918
  const waitStates = refs.filter((ref) => ref.stableId === AWAIT_BOSS_REPLY_STATE ||
1577
1919
  (tagsOf(ref.state).includes('playbook.parked') &&
1578
1920
  ref.state.on?.[BOSS_REPLY_EVENT] !== undefined));
1579
- if (waitStates.length === 0) {
1921
+ if (isController && waitStates.length > 0) {
1922
+ findings.push('controller machine declares a Boss-reply wait state');
1923
+ }
1924
+ else if (!controllerContractCandidate && waitStates.length === 0) {
1580
1925
  findings.push(`machine declares no ${AWAIT_BOSS_REPLY_STATE} state or branch-local Boss-reply wait state`);
1581
1926
  }
1582
1927
  // Every BOSS_INTERRUPT target is enterable (the captain hangs, so entering a
@@ -1613,19 +1958,17 @@ export async function checkFsmCoverage(fsmModule, opts = {}) {
1613
1958
  }
1614
1959
  }
1615
1960
  // Guard-free root entry events transition from the initial state.
1616
- const initial = typeof config.initial === 'string' ? config.initial : null;
1617
- const initialRef = initial === null
1618
- ? undefined
1619
- : refs.find((ref) => ref.path.length === 1 && ref.key === initial);
1620
- const entryArms = {
1621
- ...(initial !== null ? (states[initial]?.on ?? {}) : {}),
1622
- ...(config.on ?? {}),
1623
- };
1961
+ const initialPath = initialActiveRefs(config, refs);
1962
+ const initialRef = initialPath.at(-1);
1963
+ const entryArms = Object.assign({}, config.on ?? {}, ...initialPath.map((ref) => ref.state.on ?? {}));
1624
1964
  for (const [event, raw] of Object.entries(entryArms)) {
1625
1965
  if (event === INTERRUPT_EVENT)
1626
1966
  continue;
1627
1967
  const arms = normalizeArms(raw);
1628
- const free = arms.find((arm) => !arm.guarded && arm.target !== null && arm.target !== initial);
1968
+ const free = arms.find((arm) => !arm.guarded &&
1969
+ arm.target !== null &&
1970
+ arm.target !== initialRef?.key &&
1971
+ arm.target !== initialRef?.stableId);
1629
1972
  if (free === undefined)
1630
1973
  continue;
1631
1974
  const actor = makeActor(machine, () => null);
@@ -1641,8 +1984,14 @@ export async function checkFsmCoverage(fsmModule, opts = {}) {
1641
1984
  ref.stableId,
1642
1985
  ...(ref.configId === undefined ? [] : [ref.configId]),
1643
1986
  ]);
1987
+ // Playbook 10 controllers are host decision loops. Player delegation,
1988
+ // nested playbook invocation, and parallel regions are invalid controller
1989
+ // surfaces, so their explicit findings above own the result; do not mask
1990
+ // them by trying the ordinary workflow drivers against an unsupported shape.
1991
+ if (unsupportedController)
1992
+ return findings;
1644
1993
  for (const captain of captains) {
1645
- if (!canJump)
1994
+ if (!canJump && !isController)
1646
1995
  break;
1647
1996
  const state = captain.binding;
1648
1997
  const stateKey = state.stateId;
@@ -1652,78 +2001,155 @@ export async function checkFsmCoverage(fsmModule, opts = {}) {
1652
2001
  const errorEvent = invocationEvent(machine, captain, 'error');
1653
2002
  const rawDoneArms = transitionArms(captain.invocation.onDone);
1654
2003
  const onDoneArms = normalizeArms(captain.invocation.onDone);
2004
+ const controllerResultByArm = new Map();
1655
2005
  // Every declared result needs an arm that explicitly accepts its complete
1656
2006
  // valid output. A sole unguarded arm accepts the whole local result
1657
2007
  // contract; an array's unguarded arm is a fallback and cannot make an
1658
2008
  // otherwise orphaned key look covered.
1659
2009
  for (const key of Object.keys(state.result)) {
1660
2010
  const output = synthOutput(state, key);
1661
- const accepting = new Set();
2011
+ const accepting = new Map();
1662
2012
  for (const [index, arm] of rawDoneArms.entries()) {
1663
2013
  const target = onDoneArms[index]?.target ?? null;
1664
2014
  const rawGuard = armGuard(arm);
1665
2015
  if (rawGuard === undefined) {
1666
- if (rawDoneArms.length === 1 && target !== null)
1667
- accepting.add(index);
2016
+ if (rawDoneArms.length === 1 && target !== null) {
2017
+ accepting.set(index, undefined);
2018
+ }
1668
2019
  continue;
1669
2020
  }
1670
2021
  if (target === null || resolveGuard(machine, rawGuard) === undefined) {
1671
2022
  continue;
1672
2023
  }
1673
2024
  const guard = orderedArmPredicate(machine, rawDoneArms, index);
1674
- if (guard !== undefined &&
1675
- doneGuardSatisfiable(guard, doneEvent, output, candidates)) {
1676
- accepting.add(index);
2025
+ if (guard === undefined)
2026
+ continue;
2027
+ if (isController) {
2028
+ const assignment = controllerDoneGuardAssignment(guard, doneEvent, output, candidates, initialCoverageContext);
2029
+ if (assignment !== undefined)
2030
+ accepting.set(index, assignment);
2031
+ }
2032
+ else if (doneGuardSatisfiable(guard, doneEvent, output, candidates)) {
2033
+ accepting.set(index, undefined);
1677
2034
  }
1678
2035
  }
1679
2036
  if (accepting.size === 0) {
1680
2037
  findings.push(`state ${stateKey}: result "${key}" has no reachable accepting transition`);
1681
2038
  continue;
1682
2039
  }
1683
- // Drive only when the first arm XState would inspect under the real
1684
- // initial context is a known accepting arm. Encountering an unresolved
1685
- // guard first makes driving unsafe: XState reports that error
1686
- // asynchronously, so the arm audit below owns the finding (c887fc4).
1687
2040
  let directArm;
1688
- let safeToDrive = true;
1689
- for (const [index, arm] of rawDoneArms.entries()) {
1690
- const rawGuard = armGuard(arm);
1691
- if (rawGuard === undefined) {
1692
- directArm = index;
1693
- break;
2041
+ let drivenOutput = output;
2042
+ let drivenControllerContext = controllerContext;
2043
+ if (isController) {
2044
+ if (isControllerDecisionResult(state.result) && accepting.size !== 1) {
2045
+ findings.push(`state ${stateKey}: controller result "${key}" selects ${accepting.size} accepting action arms`);
2046
+ continue;
1694
2047
  }
1695
- const guard = resolveGuard(machine, rawGuard);
1696
- if (guard === undefined) {
1697
- safeToDrive = false;
1698
- break;
2048
+ const selected = accepting.entries().next().value;
2049
+ directArm = selected?.[0];
2050
+ const assignment = selected?.[1];
2051
+ if (assignment !== undefined) {
2052
+ drivenOutput = assignedPayload(output, assignment, 'o:');
2053
+ drivenControllerContext = assignment.context;
1699
2054
  }
1700
- try {
1701
- if (guard.run({
1702
- context: initializedCoverageContext(machine),
1703
- event: { ...doneEvent, output },
1704
- })) {
2055
+ }
2056
+ else {
2057
+ // Drive only when the first arm XState would inspect under the real
2058
+ // initial context is a known accepting arm. Encountering an unresolved
2059
+ // guard first makes driving unsafe: XState reports that error
2060
+ // asynchronously, so the arm audit below owns the finding (c887fc4).
2061
+ let safeToDrive = true;
2062
+ for (const [index, arm] of rawDoneArms.entries()) {
2063
+ const rawGuard = armGuard(arm);
2064
+ if (rawGuard === undefined) {
1705
2065
  directArm = index;
1706
2066
  break;
1707
2067
  }
2068
+ const guard = resolveGuard(machine, rawGuard);
2069
+ if (guard === undefined) {
2070
+ safeToDrive = false;
2071
+ break;
2072
+ }
2073
+ try {
2074
+ if (guard.run({
2075
+ context: initialCoverageContext,
2076
+ event: { ...doneEvent, output },
2077
+ })) {
2078
+ directArm = index;
2079
+ break;
2080
+ }
2081
+ }
2082
+ catch {
2083
+ safeToDrive = false;
2084
+ break;
2085
+ }
1708
2086
  }
1709
- catch {
1710
- safeToDrive = false;
1711
- break;
2087
+ if (!safeToDrive ||
2088
+ directArm === undefined ||
2089
+ !accepting.has(directArm)) {
2090
+ continue;
1712
2091
  }
1713
2092
  }
1714
- if (!safeToDrive ||
1715
- directArm === undefined ||
1716
- !accepting.has(directArm)) {
2093
+ if (directArm === undefined) {
1717
2094
  continue;
1718
2095
  }
2096
+ let controllerDirectTarget;
2097
+ let controllerDirectTargetNodeId;
2098
+ if (isController) {
2099
+ if (isControllerDecisionResult(state.result)) {
2100
+ const priorResult = controllerResultByArm.get(directArm);
2101
+ if (priorResult !== undefined) {
2102
+ findings.push(`state ${stateKey}: controller result "${key}" selects direct arm ${directArm}, already selected by result "${priorResult}"`);
2103
+ continue;
2104
+ }
2105
+ controllerResultByArm.set(directArm, key);
2106
+ }
2107
+ const directTarget = rawArmTarget(rawDoneArms[directArm]);
2108
+ controllerDirectTarget =
2109
+ directTarget === undefined
2110
+ ? undefined
2111
+ : stateRefForTarget(refs, directTarget, captain.ref);
2112
+ controllerDirectTargetNodeId =
2113
+ controllerDirectTarget === undefined
2114
+ ? undefined
2115
+ : resolvedStateNode(machine, controllerDirectTarget)?.id;
2116
+ if (directTarget === undefined ||
2117
+ controllerDirectTarget === undefined ||
2118
+ controllerDirectTargetNodeId === undefined) {
2119
+ findings.push(`state ${stateKey}: controller result "${key}" declares no resolvable direct action target`);
2120
+ continue;
2121
+ }
2122
+ }
1719
2123
  const gate = { armed: false };
1720
- const probe = captainProbeActor(machine, captain, output, gate, refs, captains, playbooks, sourceCandidates);
2124
+ const probe = captainProbeActor(machine, captain, drivenOutput, gate, refs, captains, playbooks, sourceCandidates, drivenControllerContext);
2125
+ if (probe === undefined) {
2126
+ const finding = `state ${stateKey} has no reachable entry transition`;
2127
+ if (!findings.includes(finding))
2128
+ findings.push(finding);
2129
+ continue;
2130
+ }
1721
2131
  const actor = probe.actor;
1722
2132
  gate.armed = true;
1723
2133
  actor.send(probe.event);
1724
- const left = await settle(actor, leftState(captain.ref));
1725
- if (!left) {
1726
- findings.push(`state ${stateKey}: result "${key}" fired no transition`);
2134
+ if (controllerDirectTarget !== undefined &&
2135
+ controllerDirectTargetNodeId !== undefined &&
2136
+ !(await settle(actor, () => probe
2137
+ .controllerDirectTransitionTargets?.()
2138
+ ?.includes(controllerDirectTargetNodeId) === true))) {
2139
+ findings.push(`state ${stateKey}: controller result "${key}" did not reach its declared direct action target ${controllerDirectTarget.stableId}`);
2140
+ actor.stop();
2141
+ continue;
2142
+ }
2143
+ const transitioned = isController && probe.controllerExitPlanned === false
2144
+ ? false
2145
+ : await settle(actor, isController
2146
+ ? (snapshot) => snapshot.status === 'done' ||
2147
+ (initialRef !== undefined && atState(initialRef)(snapshot))
2148
+ : leftState(captain.ref));
2149
+ if (!transitioned) {
2150
+ findings.push(isController
2151
+ ? `state ${stateKey}: controller result "${key}" reached neither the session hub nor a shutdown final`
2152
+ : `state ${stateKey}: result "${key}" fired no transition`);
1727
2153
  }
1728
2154
  else if (key === NEEDS_BOSS_REPLY) {
1729
2155
  const waitTarget = rawArmTarget(rawDoneArms[directArm]) ?? onDoneArms[directArm]?.target;
@@ -1763,7 +2189,13 @@ export async function checkFsmCoverage(fsmModule, opts = {}) {
1763
2189
  }
1764
2190
  actor.stop();
1765
2191
  const blankGate = { armed: false };
1766
- const blankProbe = captainProbeActor(machine, captain, synthOutput(state, NEEDS_BOSS_REPLY), blankGate, refs, captains, playbooks, sourceCandidates);
2192
+ const blankProbe = captainProbeActor(machine, captain, synthOutput(state, NEEDS_BOSS_REPLY), blankGate, refs, captains, playbooks, sourceCandidates, controllerContext);
2193
+ if (blankProbe === undefined) {
2194
+ const finding = `state ${stateKey} has no reachable entry transition`;
2195
+ if (!findings.includes(finding))
2196
+ findings.push(finding);
2197
+ continue;
2198
+ }
1767
2199
  const blank = blankProbe.actor;
1768
2200
  blankGate.armed = true;
1769
2201
  blank.send(blankProbe.event);
@@ -1851,7 +2283,7 @@ export async function checkFsmCoverage(fsmModule, opts = {}) {
1851
2283
  }
1852
2284
  try {
1853
2285
  if (guard.run({
1854
- context: initializedCoverageContext(machine),
2286
+ context: initialCoverageContext,
1855
2287
  event: { ...errorEvent, error: forcedError },
1856
2288
  })) {
1857
2289
  directErrorArm = index;
@@ -1871,7 +2303,13 @@ export async function checkFsmCoverage(fsmModule, opts = {}) {
1871
2303
  ? undefined
1872
2304
  : stateRefForTarget(refs, rawTarget, captain.ref);
1873
2305
  const gate = { armed: false };
1874
- const probe = captainProbeActor(machine, captain, forcedError, gate, refs, captains, playbooks, sourceCandidates);
2306
+ const probe = captainProbeActor(machine, captain, forcedError, gate, refs, captains, playbooks, sourceCandidates, controllerContext);
2307
+ if (probe === undefined) {
2308
+ const finding = `state ${stateKey} has no reachable entry transition`;
2309
+ if (!findings.includes(finding))
2310
+ findings.push(finding);
2311
+ continue;
2312
+ }
1875
2313
  const actor = probe.actor;
1876
2314
  gate.armed = true;
1877
2315
  actor.send(probe.event);
@@ -1887,7 +2325,7 @@ export async function checkFsmCoverage(fsmModule, opts = {}) {
1887
2325
  }
1888
2326
  /**
1889
2327
  * Builds a per-artifact vitest module running the transition-coverage check
1890
- * beside the artifacts (VERIFY-6).
2328
+ * beside the artifacts (verification-6).
1891
2329
  */
1892
2330
  export function generateFsmCoverageTest(opts) {
1893
2331
  const commentBasename = JSON.stringify(opts.basename)
@@ -1922,7 +2360,7 @@ describe(${suiteName}, () => {
1922
2360
  }
1923
2361
  /**
1924
2362
  * Emits the transition-coverage test beside a compiled `playbook` artifact
1925
- * (VERIFY-6): validates the produced `fsm` drives cleanly, then writes
2363
+ * (verification-6): validates the produced `fsm` drives cleanly, then writes
1926
2364
  * `<basename>.fsm.coverage.test.ts` and returns its path with any coverage
1927
2365
  * findings as diagnostics.
1928
2366
  *