@optique/core 1.0.0-dev.1711 → 1.0.0-dev.1714

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.
@@ -712,9 +712,33 @@ function or(...args) {
712
712
  orderedParsers.sort(([_, a], [__, b]) => activeState?.[0] === a ? -1 : activeState?.[0] === b ? 1 : a - b);
713
713
  let zeroConsumedBranch = null;
714
714
  let zeroConsumedCount = 0;
715
+ let provisionalConsuming = null;
716
+ let provisionalAmbiguous = false;
715
717
  for (const [parser, i] of orderedParsers) {
716
718
  const result = parser.parse(withChildContext(context, i, activeState == null || activeState[0] !== i || !activeState[1].success ? parser.initialState : activeState[1].next.state, parser));
717
719
  if (result.success && result.consumed.length > 0) {
720
+ if (result.provisional) {
721
+ const activeBranchLocked = activeState != null && activeState[1].success && activeState[1].consumed.length > 0;
722
+ if (activeBranchLocked && activeState[0] === i) {
723
+ provisionalConsuming = {
724
+ index: i,
725
+ parser,
726
+ result
727
+ };
728
+ continue;
729
+ }
730
+ if (activeBranchLocked && provisionalConsuming != null && provisionalConsuming.index === activeState[0]) continue;
731
+ if (provisionalConsuming == null && !provisionalAmbiguous) provisionalConsuming = {
732
+ index: i,
733
+ parser,
734
+ result
735
+ };
736
+ else {
737
+ provisionalConsuming = null;
738
+ provisionalAmbiguous = true;
739
+ }
740
+ continue;
741
+ }
718
742
  if (activeState?.[0] !== i && activeState?.[1].success) {
719
743
  if (activeState[1].consumed.length === 0) {
720
744
  const mergedExec$2 = mergeChildExec(context.exec, result.next.exec);
@@ -812,6 +836,66 @@ function or(...args) {
812
836
  consumed: []
813
837
  };
814
838
  }
839
+ if (provisionalConsuming !== null) {
840
+ const activeIsLockedDifferent = activeState != null && activeState[1].success && activeState[1].consumed.length > 0 && activeState[0] !== provisionalConsuming.index;
841
+ if (!activeIsLockedDifferent) {
842
+ const mergedExec = mergeChildExec(context.exec, provisionalConsuming.result.next.exec);
843
+ return {
844
+ success: true,
845
+ provisional: true,
846
+ next: {
847
+ ...context,
848
+ buffer: provisionalConsuming.result.next.buffer,
849
+ optionsTerminated: provisionalConsuming.result.next.optionsTerminated,
850
+ state: createExclusiveState(context.state, provisionalConsuming.index, provisionalConsuming.parser, provisionalConsuming.result),
851
+ ...mergedExec != null ? {
852
+ exec: mergedExec,
853
+ dependencyRegistry: mergedExec.dependencyRegistry
854
+ } : {}
855
+ },
856
+ consumed: provisionalConsuming.result.consumed
857
+ };
858
+ }
859
+ if (activeState != null && activeState[1].success) {
860
+ const previouslyConsumed = activeState[1].consumed;
861
+ const checkResult = provisionalConsuming.parser.parse({
862
+ ...withChildContext(context, provisionalConsuming.index, provisionalConsuming.parser.initialState, provisionalConsuming.parser),
863
+ buffer: previouslyConsumed
864
+ });
865
+ const canConsumeShared = checkResult.success && checkResult.consumed.length === previouslyConsumed.length && checkResult.consumed.every((c, idx) => c === previouslyConsumed[idx]);
866
+ if (canConsumeShared && checkResult.success) {
867
+ const replayExec = mergeChildExec(context.exec, checkResult.next.exec);
868
+ const replayedResult = provisionalConsuming.parser.parse(withChildContext({
869
+ ...context,
870
+ ...replayExec != null ? {
871
+ exec: replayExec,
872
+ dependencyRegistry: replayExec.dependencyRegistry
873
+ } : {}
874
+ }, provisionalConsuming.index, checkResult.next.state, provisionalConsuming.parser));
875
+ if (replayedResult.success) {
876
+ const mergedExec = mergeChildExec(replayExec, replayedResult.next.exec);
877
+ return {
878
+ success: true,
879
+ provisional: true,
880
+ next: {
881
+ ...context,
882
+ buffer: replayedResult.next.buffer,
883
+ optionsTerminated: replayedResult.next.optionsTerminated,
884
+ state: createExclusiveState(context.state, provisionalConsuming.index, provisionalConsuming.parser, {
885
+ ...replayedResult,
886
+ consumed: [...previouslyConsumed, ...replayedResult.consumed]
887
+ }),
888
+ ...mergedExec != null ? {
889
+ exec: mergedExec,
890
+ dependencyRegistry: mergedExec.dependencyRegistry
891
+ } : {}
892
+ },
893
+ consumed: replayedResult.consumed
894
+ };
895
+ }
896
+ }
897
+ }
898
+ }
815
899
  return {
816
900
  ...error,
817
901
  success: false
@@ -824,10 +908,34 @@ function or(...args) {
824
908
  orderedParsers.sort(([_, a], [__, b]) => activeState?.[0] === a ? -1 : activeState?.[0] === b ? 1 : a - b);
825
909
  let zeroConsumedBranch = null;
826
910
  let zeroConsumedCount = 0;
911
+ let provisionalConsuming = null;
912
+ let provisionalAmbiguous = false;
827
913
  for (const [parser, i] of orderedParsers) {
828
914
  const resultOrPromise = parser.parse(withChildContext(context, i, activeState == null || activeState[0] !== i || !activeState[1].success ? parser.initialState : activeState[1].next.state, parser));
829
915
  const result = await resultOrPromise;
830
916
  if (result.success && result.consumed.length > 0) {
917
+ if (result.provisional) {
918
+ const activeBranchLocked = activeState != null && activeState[1].success && activeState[1].consumed.length > 0;
919
+ if (activeBranchLocked && activeState[0] === i) {
920
+ provisionalConsuming = {
921
+ index: i,
922
+ parser,
923
+ result
924
+ };
925
+ continue;
926
+ }
927
+ if (activeBranchLocked && provisionalConsuming != null && provisionalConsuming.index === activeState[0]) continue;
928
+ if (provisionalConsuming == null && !provisionalAmbiguous) provisionalConsuming = {
929
+ index: i,
930
+ parser,
931
+ result
932
+ };
933
+ else {
934
+ provisionalConsuming = null;
935
+ provisionalAmbiguous = true;
936
+ }
937
+ continue;
938
+ }
831
939
  if (activeState?.[0] !== i && activeState?.[1].success) {
832
940
  if (activeState[1].consumed.length === 0) {
833
941
  const mergedExec$2 = mergeChildExec(context.exec, result.next.exec);
@@ -927,6 +1035,66 @@ function or(...args) {
927
1035
  consumed: []
928
1036
  };
929
1037
  }
1038
+ if (provisionalConsuming !== null) {
1039
+ const activeIsLockedDifferent = activeState != null && activeState[1].success && activeState[1].consumed.length > 0 && activeState[0] !== provisionalConsuming.index;
1040
+ if (!activeIsLockedDifferent) {
1041
+ const mergedExec = mergeChildExec(context.exec, provisionalConsuming.result.next.exec);
1042
+ return {
1043
+ success: true,
1044
+ provisional: true,
1045
+ next: {
1046
+ ...context,
1047
+ buffer: provisionalConsuming.result.next.buffer,
1048
+ optionsTerminated: provisionalConsuming.result.next.optionsTerminated,
1049
+ state: createExclusiveState(context.state, provisionalConsuming.index, provisionalConsuming.parser, provisionalConsuming.result),
1050
+ ...mergedExec != null ? {
1051
+ exec: mergedExec,
1052
+ dependencyRegistry: mergedExec.dependencyRegistry
1053
+ } : {}
1054
+ },
1055
+ consumed: provisionalConsuming.result.consumed
1056
+ };
1057
+ }
1058
+ if (activeState != null && activeState[1].success) {
1059
+ const previouslyConsumed = activeState[1].consumed;
1060
+ const checkResult = await provisionalConsuming.parser.parse({
1061
+ ...withChildContext(context, provisionalConsuming.index, provisionalConsuming.parser.initialState, provisionalConsuming.parser),
1062
+ buffer: previouslyConsumed
1063
+ });
1064
+ const canConsumeShared = checkResult.success && checkResult.consumed.length === previouslyConsumed.length && checkResult.consumed.every((c, idx) => c === previouslyConsumed[idx]);
1065
+ if (canConsumeShared && checkResult.success) {
1066
+ const replayExec = mergeChildExec(context.exec, checkResult.next.exec);
1067
+ const replayedResult = await provisionalConsuming.parser.parse(withChildContext({
1068
+ ...context,
1069
+ ...replayExec != null ? {
1070
+ exec: replayExec,
1071
+ dependencyRegistry: replayExec.dependencyRegistry
1072
+ } : {}
1073
+ }, provisionalConsuming.index, checkResult.next.state, provisionalConsuming.parser));
1074
+ if (replayedResult.success) {
1075
+ const mergedExec = mergeChildExec(replayExec, replayedResult.next.exec);
1076
+ return {
1077
+ success: true,
1078
+ provisional: true,
1079
+ next: {
1080
+ ...context,
1081
+ buffer: replayedResult.next.buffer,
1082
+ optionsTerminated: replayedResult.next.optionsTerminated,
1083
+ state: createExclusiveState(context.state, provisionalConsuming.index, provisionalConsuming.parser, {
1084
+ ...replayedResult,
1085
+ consumed: [...previouslyConsumed, ...replayedResult.consumed]
1086
+ }),
1087
+ ...mergedExec != null ? {
1088
+ exec: mergedExec,
1089
+ dependencyRegistry: mergedExec.dependencyRegistry
1090
+ } : {}
1091
+ },
1092
+ consumed: replayedResult.consumed
1093
+ };
1094
+ }
1095
+ }
1096
+ }
1097
+ }
930
1098
  return {
931
1099
  ...error,
932
1100
  success: false
@@ -1020,7 +1188,8 @@ function longestMatch(...args) {
1020
1188
  const result = parser.parse(withChildContext(context, i, activeState == null || activeState[0] !== i || !activeState[1].success ? parser.initialState : activeState[1].next.state, parser));
1021
1189
  if (result.success) {
1022
1190
  const consumed = context.buffer.length - result.next.buffer.length;
1023
- if (bestMatch === null || consumed > bestMatch.consumed) bestMatch = {
1191
+ const bestIsProvisional = bestMatch != null && bestMatch.result.success && !!bestMatch.result.provisional;
1192
+ if (bestMatch === null || consumed > bestMatch.consumed || consumed === bestMatch.consumed && bestIsProvisional && !result.provisional) bestMatch = {
1024
1193
  index: i,
1025
1194
  parser,
1026
1195
  result,
@@ -1060,7 +1229,8 @@ function longestMatch(...args) {
1060
1229
  const result = await resultOrPromise;
1061
1230
  if (result.success) {
1062
1231
  const consumed = context.buffer.length - result.next.buffer.length;
1063
- if (bestMatch === null || consumed > bestMatch.consumed) bestMatch = {
1232
+ const bestIsProvisional = bestMatch != null && bestMatch.result.success && !!bestMatch.result.provisional;
1233
+ if (bestMatch === null || consumed > bestMatch.consumed || consumed === bestMatch.consumed && bestIsProvisional && !result.provisional) bestMatch = {
1064
1234
  index: i,
1065
1235
  parser,
1066
1236
  result,
@@ -3656,18 +3826,35 @@ function group(label, parser, options = {}) {
3656
3826
  * // defaultResult.value = [undefined, {}]
3657
3827
  * ```
3658
3828
  *
3659
- * ### Async discriminator limitation
3829
+ * ### Speculative branch parsing
3660
3830
  *
3661
3831
  * When the discriminator is an async parser that succeeds without consuming
3662
3832
  * input (e.g., `prompt(option(...))` with no CLI input), branch selection
3663
- * is deferred to the complete phase. If the selected branch needs to
3664
- * consume remaining tokens, those tokens cannot be consumed because the
3665
- * branch is not known during parse. In practice, this means
3666
- * `conditional(prompt(option(...)), { key: option(...) })` cannot parse
3667
- * branch-specific tokens when the discriminator requires interactive
3668
- * resolution. Provide a default branch or ensure the discriminator
3669
- * can resolve synchronously (e.g., via `bindEnv()` or `withDefault()`)
3670
- * to avoid this limitation.
3833
+ * is normally deferred to the complete phase. To allow branch-specific
3834
+ * tokens to be consumed, `conditional()` speculatively tries all named
3835
+ * branches during parse. If exactly one branch can consume tokens, it is
3836
+ * tentatively selected and verified against the resolved discriminator
3837
+ * during the complete phase.
3838
+ *
3839
+ * If the discriminator resolves to a different branch than the one that
3840
+ * consumed tokens (contradictory input), the parse fails. When multiple
3841
+ * branches can consume the same tokens (ambiguous), speculation is skipped
3842
+ * entirely to keep branch selection order-independent.
3843
+ *
3844
+ * #### Known limitations
3845
+ *
3846
+ * - When a default branch accepts the same tokens as a named branch,
3847
+ * speculation prefers the named branch. If the discriminator later
3848
+ * resolves to a value not in the named branches, the parse fails
3849
+ * instead of falling back to the default branch. To avoid this,
3850
+ * ensure named branch options are distinct from the default branch.
3851
+ * - Within `longestMatch()`, a longer speculative match can beat a
3852
+ * shorter definitive one. If the speculative match fails during
3853
+ * completion, the tokens consumed by it are not recoverable.
3854
+ * - The dependency runtime seeds both discriminator and branch sources
3855
+ * before verifying the speculative selection. A discriminator that
3856
+ * depends on branch-local dependency sources could be circularly
3857
+ * confirmed by the speculative branch.
3671
3858
  *
3672
3859
  * @since 0.8.0
3673
3860
  */
@@ -3880,13 +4067,87 @@ function conditional(discriminator, branches, defaultBranch, options) {
3880
4067
  const discriminatorResult = await discriminator.parse({ ...withChildContext(context, "_discriminator", state.discriminatorState, discriminator) });
3881
4068
  if (discriminatorResult.success) {
3882
4069
  if (discriminatorResult.consumed.length === 0 && discriminator.$mode === "async") {
4070
+ const discriminatorExec = mergeChildExec(context.exec, discriminatorResult.next.exec);
4071
+ const speculationContext = {
4072
+ ...context,
4073
+ buffer: discriminatorResult.next.buffer,
4074
+ optionsTerminated: discriminatorResult.next.optionsTerminated,
4075
+ ...discriminatorExec != null ? {
4076
+ exec: discriminatorExec,
4077
+ dependencyRegistry: discriminatorExec.dependencyRegistry
4078
+ } : {}
4079
+ };
4080
+ let speculativeHit;
4081
+ let provisionalHit;
4082
+ let provisionalAmbiguous = false;
4083
+ let speculativeError;
4084
+ let ambiguous = false;
4085
+ for (const [key, bp] of branchParsers) {
4086
+ const branchResult = await bp.parse(withChildContext(speculationContext, "_branch", bp.initialState, bp, bp.usage));
4087
+ if (branchResult.success && branchResult.consumed.length > 0) {
4088
+ if (branchResult.provisional) {
4089
+ if (provisionalHit == null && !provisionalAmbiguous) provisionalHit = {
4090
+ key,
4091
+ bp,
4092
+ result: branchResult
4093
+ };
4094
+ else {
4095
+ provisionalHit = void 0;
4096
+ provisionalAmbiguous = true;
4097
+ }
4098
+ continue;
4099
+ }
4100
+ if (speculativeHit != null) {
4101
+ ambiguous = true;
4102
+ break;
4103
+ }
4104
+ speculativeHit = {
4105
+ key,
4106
+ bp,
4107
+ result: branchResult
4108
+ };
4109
+ }
4110
+ if (!branchResult.success && branchResult.consumed > 0 && (speculativeError == null || speculativeError.consumed < branchResult.consumed)) speculativeError = branchResult;
4111
+ }
4112
+ if (speculativeHit != null && (provisionalHit != null || provisionalAmbiguous)) ambiguous = true;
4113
+ if (speculativeHit == null && !ambiguous && !provisionalAmbiguous && provisionalHit != null) speculativeHit = provisionalHit;
4114
+ if (speculativeHit != null && !ambiguous) {
4115
+ const { key, bp, result: branchResult } = speculativeHit;
4116
+ if (branchResult.success) {
4117
+ const annotatedDiscriminatorState$2 = getAnnotatedChildState(state, discriminatorResult.next.state, discriminator);
4118
+ const mergedExec = mergeChildExec(discriminatorExec, branchResult.next.exec);
4119
+ return {
4120
+ success: true,
4121
+ provisional: true,
4122
+ next: {
4123
+ ...branchResult.next,
4124
+ state: {
4125
+ ...state,
4126
+ discriminatorState: annotatedDiscriminatorState$2,
4127
+ selectedBranch: {
4128
+ kind: "branch",
4129
+ key
4130
+ },
4131
+ branchState: getAnnotatedChildState(state, branchResult.next.state, bp),
4132
+ speculative: true
4133
+ },
4134
+ ...mergedExec != null ? {
4135
+ exec: mergedExec,
4136
+ dependencyRegistry: mergedExec.dependencyRegistry
4137
+ } : {}
4138
+ },
4139
+ consumed: branchResult.consumed
4140
+ };
4141
+ }
4142
+ }
3883
4143
  let deferredBranchState = state.branchState;
3884
4144
  if (defaultBranch !== void 0) {
3885
- const defaultResult = await defaultBranch.parse(withChildContext(context, "_branch", state.branchState ?? defaultBranch.initialState, defaultBranch, defaultBranch.usage));
4145
+ const defaultResult = await defaultBranch.parse(withChildContext(speculationContext, "_branch", state.branchState ?? defaultBranch.initialState, defaultBranch, defaultBranch.usage));
3886
4146
  if (defaultResult.success && defaultResult.consumed.length > 0) {
3887
- const defaultExec = mergeChildExec(context.exec, defaultResult.next.exec);
4147
+ const defaultExec = mergeChildExec(discriminatorExec ?? context.exec, defaultResult.next.exec);
3888
4148
  return {
3889
4149
  success: true,
4150
+ ...defaultResult.provisional ? { provisional: true } : {},
3890
4151
  next: {
3891
4152
  ...defaultResult.next,
3892
4153
  state: {
@@ -3903,10 +4164,10 @@ function conditional(discriminator, branches, defaultBranch, options) {
3903
4164
  };
3904
4165
  }
3905
4166
  if (!defaultResult.success && defaultResult.consumed > 0) return defaultResult;
3906
- if (defaultResult.success && defaultResult.consumed.length === 0 && context.buffer.length === 0) deferredBranchState = getAnnotatedChildState(state, defaultResult.next.state, defaultBranch);
4167
+ if (defaultResult.success && defaultResult.consumed.length === 0 && speculationContext.buffer.length === 0) deferredBranchState = getAnnotatedChildState(state, defaultResult.next.state, defaultBranch);
3907
4168
  }
4169
+ if (speculativeError != null && !ambiguous) return speculativeError;
3908
4170
  const annotatedDiscriminatorState$1 = getAnnotatedChildState(state, discriminatorResult.next.state, discriminator);
3909
- const mergedExec = mergeChildExec(context.exec, discriminatorResult.next.exec);
3910
4171
  return {
3911
4172
  success: true,
3912
4173
  provisional: true,
@@ -3917,9 +4178,9 @@ function conditional(discriminator, branches, defaultBranch, options) {
3917
4178
  discriminatorState: annotatedDiscriminatorState$1,
3918
4179
  branchState: deferredBranchState
3919
4180
  },
3920
- ...mergedExec != null ? {
3921
- exec: mergedExec,
3922
- dependencyRegistry: mergedExec.dependencyRegistry
4181
+ ...discriminatorExec != null ? {
4182
+ exec: discriminatorExec,
4183
+ dependencyRegistry: discriminatorExec.dependencyRegistry
3923
4184
  } : {}
3924
4185
  },
3925
4186
  consumed: []
@@ -4141,6 +4402,18 @@ function conditional(discriminator, branches, defaultBranch, options) {
4141
4402
  };
4142
4403
  };
4143
4404
  const completeAsync = async (state, exec) => {
4405
+ let wasSpeculative = false;
4406
+ if (state.speculative && state.selectedBranch?.kind === "branch") if (exec?.phase !== "parse" && exec?.phase !== "suggest") {
4407
+ wasSpeculative = true;
4408
+ state = {
4409
+ ...state,
4410
+ speculative: void 0
4411
+ };
4412
+ } else state = {
4413
+ ...state,
4414
+ discriminatorValue: state.selectedBranch.key,
4415
+ speculative: void 0
4416
+ };
4144
4417
  if (state.selectedBranch === void 0) {
4145
4418
  if (exec?.phase !== "parse" && exec?.phase !== "suggest") {
4146
4419
  const annotatedDiscriminatorStateForDeferred = getAnnotatedChildState(state, state.discriminatorState, discriminator);
@@ -4225,20 +4498,30 @@ function conditional(discriminator, branches, defaultBranch, options) {
4225
4498
  };
4226
4499
  const needsDiscriminatorCompletion = state.selectedBranch.kind !== "default" && !(state.discriminatorValue != null && state.discriminatorValue === state.selectedBranch.key);
4227
4500
  const discriminatorCompleteResult = needsDiscriminatorCompletion ? await discriminator.complete(annotatedDiscriminatorState, withChildExecPath(completionExec, "_discriminator")) : void 0;
4228
- const branchResult = unwrapCompleteResult(await branchParser.complete(resolvedBranchState, withChildExecPath(completionExec, "_branch")));
4229
- if (!branchResult.success) {
4230
- if (state.discriminatorValue !== void 0 && options?.errors?.branchError) return {
4231
- success: false,
4232
- error: options.errors.branchError(state.discriminatorValue, branchResult.error)
4233
- };
4234
- return branchResult;
4235
- }
4236
4501
  let discriminatorValue;
4237
4502
  if (state.selectedBranch.kind === "default") discriminatorValue = void 0;
4238
4503
  else if (state.discriminatorValue != null && state.discriminatorValue === state.selectedBranch.key) discriminatorValue = state.discriminatorValue;
4239
4504
  else {
4240
4505
  const completedDiscriminator = unwrapCompleteResult(discriminatorCompleteResult);
4241
- discriminatorValue = completedDiscriminator.success ? completedDiscriminator.value : state.selectedBranch.key;
4506
+ if (completedDiscriminator.success) discriminatorValue = completedDiscriminator.value;
4507
+ else if (wasSpeculative) return completedDiscriminator;
4508
+ else discriminatorValue = state.selectedBranch.key;
4509
+ }
4510
+ if (wasSpeculative && state.selectedBranch.kind === "branch" && discriminatorValue !== state.selectedBranch.key) {
4511
+ const speculativeKey = state.selectedBranch.key;
4512
+ const resolvedKey = discriminatorValue ?? "";
4513
+ return {
4514
+ success: false,
4515
+ error: options?.errors?.branchMismatch ? options.errors.branchMismatch(resolvedKey, speculativeKey) : require_message.message`Branch mismatch: tokens for ${speculativeKey} were consumed, but the discriminator resolved to ${resolvedKey}.`
4516
+ };
4517
+ }
4518
+ const branchResult = unwrapCompleteResult(await branchParser.complete(resolvedBranchState, withChildExecPath(completionExec, "_branch")));
4519
+ if (!branchResult.success) {
4520
+ if (discriminatorValue !== void 0 && options?.errors?.branchError) return {
4521
+ success: false,
4522
+ error: options.errors.branchError(discriminatorValue, branchResult.error)
4523
+ };
4524
+ return branchResult;
4242
4525
  }
4243
4526
  return {
4244
4527
  success: true,
@@ -1316,6 +1316,7 @@ interface ConditionalState<TDiscriminator extends string> {
1316
1316
  readonly discriminatorValue: TDiscriminator | undefined;
1317
1317
  readonly selectedBranch: SelectedBranch<TDiscriminator> | undefined;
1318
1318
  readonly branchState: unknown;
1319
+ readonly speculative?: true;
1319
1320
  }
1320
1321
  /**
1321
1322
  * Options for customizing error messages in the {@link conditional} combinator.
@@ -1331,6 +1332,20 @@ interface ConditionalErrorOptions {
1331
1332
  * Custom error message for no matching input.
1332
1333
  */
1333
1334
  noMatch?: Message | ((context: NoMatchContext) => Message);
1335
+ /**
1336
+ * Custom error message when speculative branch parsing committed
1337
+ * to one branch but the resolved discriminator value names a
1338
+ * different branch. This is the contradictory-input case: tokens
1339
+ * specific to one branch were consumed during the parse phase,
1340
+ * but the discriminator (e.g., from `prompt()` or a deferred
1341
+ * config source) ultimately resolved to a different key.
1342
+ *
1343
+ * Receives both the discriminator value the parser actually
1344
+ * resolved to (`discriminatorValue`) and the speculative key the
1345
+ * branch tokens were committed to (`speculativeKey`).
1346
+ * @since 0.10.1
1347
+ */
1348
+ branchMismatch?: (discriminatorValue: string, speculativeKey: string) => Message;
1334
1349
  }
1335
1350
  /**
1336
1351
  * Options for customizing the {@link conditional} combinator behavior.
@@ -1316,6 +1316,7 @@ interface ConditionalState<TDiscriminator extends string> {
1316
1316
  readonly discriminatorValue: TDiscriminator | undefined;
1317
1317
  readonly selectedBranch: SelectedBranch<TDiscriminator> | undefined;
1318
1318
  readonly branchState: unknown;
1319
+ readonly speculative?: true;
1319
1320
  }
1320
1321
  /**
1321
1322
  * Options for customizing error messages in the {@link conditional} combinator.
@@ -1331,6 +1332,20 @@ interface ConditionalErrorOptions {
1331
1332
  * Custom error message for no matching input.
1332
1333
  */
1333
1334
  noMatch?: Message | ((context: NoMatchContext) => Message);
1335
+ /**
1336
+ * Custom error message when speculative branch parsing committed
1337
+ * to one branch but the resolved discriminator value names a
1338
+ * different branch. This is the contradictory-input case: tokens
1339
+ * specific to one branch were consumed during the parse phase,
1340
+ * but the discriminator (e.g., from `prompt()` or a deferred
1341
+ * config source) ultimately resolved to a different key.
1342
+ *
1343
+ * Receives both the discriminator value the parser actually
1344
+ * resolved to (`discriminatorValue`) and the speculative key the
1345
+ * branch tokens were committed to (`speculativeKey`).
1346
+ * @since 0.10.1
1347
+ */
1348
+ branchMismatch?: (discriminatorValue: string, speculativeKey: string) => Message;
1334
1349
  }
1335
1350
  /**
1336
1351
  * Options for customizing the {@link conditional} combinator behavior.
@@ -712,9 +712,33 @@ function or(...args) {
712
712
  orderedParsers.sort(([_, a], [__, b]) => activeState?.[0] === a ? -1 : activeState?.[0] === b ? 1 : a - b);
713
713
  let zeroConsumedBranch = null;
714
714
  let zeroConsumedCount = 0;
715
+ let provisionalConsuming = null;
716
+ let provisionalAmbiguous = false;
715
717
  for (const [parser, i] of orderedParsers) {
716
718
  const result = parser.parse(withChildContext(context, i, activeState == null || activeState[0] !== i || !activeState[1].success ? parser.initialState : activeState[1].next.state, parser));
717
719
  if (result.success && result.consumed.length > 0) {
720
+ if (result.provisional) {
721
+ const activeBranchLocked = activeState != null && activeState[1].success && activeState[1].consumed.length > 0;
722
+ if (activeBranchLocked && activeState[0] === i) {
723
+ provisionalConsuming = {
724
+ index: i,
725
+ parser,
726
+ result
727
+ };
728
+ continue;
729
+ }
730
+ if (activeBranchLocked && provisionalConsuming != null && provisionalConsuming.index === activeState[0]) continue;
731
+ if (provisionalConsuming == null && !provisionalAmbiguous) provisionalConsuming = {
732
+ index: i,
733
+ parser,
734
+ result
735
+ };
736
+ else {
737
+ provisionalConsuming = null;
738
+ provisionalAmbiguous = true;
739
+ }
740
+ continue;
741
+ }
718
742
  if (activeState?.[0] !== i && activeState?.[1].success) {
719
743
  if (activeState[1].consumed.length === 0) {
720
744
  const mergedExec$2 = mergeChildExec(context.exec, result.next.exec);
@@ -812,6 +836,66 @@ function or(...args) {
812
836
  consumed: []
813
837
  };
814
838
  }
839
+ if (provisionalConsuming !== null) {
840
+ const activeIsLockedDifferent = activeState != null && activeState[1].success && activeState[1].consumed.length > 0 && activeState[0] !== provisionalConsuming.index;
841
+ if (!activeIsLockedDifferent) {
842
+ const mergedExec = mergeChildExec(context.exec, provisionalConsuming.result.next.exec);
843
+ return {
844
+ success: true,
845
+ provisional: true,
846
+ next: {
847
+ ...context,
848
+ buffer: provisionalConsuming.result.next.buffer,
849
+ optionsTerminated: provisionalConsuming.result.next.optionsTerminated,
850
+ state: createExclusiveState(context.state, provisionalConsuming.index, provisionalConsuming.parser, provisionalConsuming.result),
851
+ ...mergedExec != null ? {
852
+ exec: mergedExec,
853
+ dependencyRegistry: mergedExec.dependencyRegistry
854
+ } : {}
855
+ },
856
+ consumed: provisionalConsuming.result.consumed
857
+ };
858
+ }
859
+ if (activeState != null && activeState[1].success) {
860
+ const previouslyConsumed = activeState[1].consumed;
861
+ const checkResult = provisionalConsuming.parser.parse({
862
+ ...withChildContext(context, provisionalConsuming.index, provisionalConsuming.parser.initialState, provisionalConsuming.parser),
863
+ buffer: previouslyConsumed
864
+ });
865
+ const canConsumeShared = checkResult.success && checkResult.consumed.length === previouslyConsumed.length && checkResult.consumed.every((c, idx) => c === previouslyConsumed[idx]);
866
+ if (canConsumeShared && checkResult.success) {
867
+ const replayExec = mergeChildExec(context.exec, checkResult.next.exec);
868
+ const replayedResult = provisionalConsuming.parser.parse(withChildContext({
869
+ ...context,
870
+ ...replayExec != null ? {
871
+ exec: replayExec,
872
+ dependencyRegistry: replayExec.dependencyRegistry
873
+ } : {}
874
+ }, provisionalConsuming.index, checkResult.next.state, provisionalConsuming.parser));
875
+ if (replayedResult.success) {
876
+ const mergedExec = mergeChildExec(replayExec, replayedResult.next.exec);
877
+ return {
878
+ success: true,
879
+ provisional: true,
880
+ next: {
881
+ ...context,
882
+ buffer: replayedResult.next.buffer,
883
+ optionsTerminated: replayedResult.next.optionsTerminated,
884
+ state: createExclusiveState(context.state, provisionalConsuming.index, provisionalConsuming.parser, {
885
+ ...replayedResult,
886
+ consumed: [...previouslyConsumed, ...replayedResult.consumed]
887
+ }),
888
+ ...mergedExec != null ? {
889
+ exec: mergedExec,
890
+ dependencyRegistry: mergedExec.dependencyRegistry
891
+ } : {}
892
+ },
893
+ consumed: replayedResult.consumed
894
+ };
895
+ }
896
+ }
897
+ }
898
+ }
815
899
  return {
816
900
  ...error,
817
901
  success: false
@@ -824,10 +908,34 @@ function or(...args) {
824
908
  orderedParsers.sort(([_, a], [__, b]) => activeState?.[0] === a ? -1 : activeState?.[0] === b ? 1 : a - b);
825
909
  let zeroConsumedBranch = null;
826
910
  let zeroConsumedCount = 0;
911
+ let provisionalConsuming = null;
912
+ let provisionalAmbiguous = false;
827
913
  for (const [parser, i] of orderedParsers) {
828
914
  const resultOrPromise = parser.parse(withChildContext(context, i, activeState == null || activeState[0] !== i || !activeState[1].success ? parser.initialState : activeState[1].next.state, parser));
829
915
  const result = await resultOrPromise;
830
916
  if (result.success && result.consumed.length > 0) {
917
+ if (result.provisional) {
918
+ const activeBranchLocked = activeState != null && activeState[1].success && activeState[1].consumed.length > 0;
919
+ if (activeBranchLocked && activeState[0] === i) {
920
+ provisionalConsuming = {
921
+ index: i,
922
+ parser,
923
+ result
924
+ };
925
+ continue;
926
+ }
927
+ if (activeBranchLocked && provisionalConsuming != null && provisionalConsuming.index === activeState[0]) continue;
928
+ if (provisionalConsuming == null && !provisionalAmbiguous) provisionalConsuming = {
929
+ index: i,
930
+ parser,
931
+ result
932
+ };
933
+ else {
934
+ provisionalConsuming = null;
935
+ provisionalAmbiguous = true;
936
+ }
937
+ continue;
938
+ }
831
939
  if (activeState?.[0] !== i && activeState?.[1].success) {
832
940
  if (activeState[1].consumed.length === 0) {
833
941
  const mergedExec$2 = mergeChildExec(context.exec, result.next.exec);
@@ -927,6 +1035,66 @@ function or(...args) {
927
1035
  consumed: []
928
1036
  };
929
1037
  }
1038
+ if (provisionalConsuming !== null) {
1039
+ const activeIsLockedDifferent = activeState != null && activeState[1].success && activeState[1].consumed.length > 0 && activeState[0] !== provisionalConsuming.index;
1040
+ if (!activeIsLockedDifferent) {
1041
+ const mergedExec = mergeChildExec(context.exec, provisionalConsuming.result.next.exec);
1042
+ return {
1043
+ success: true,
1044
+ provisional: true,
1045
+ next: {
1046
+ ...context,
1047
+ buffer: provisionalConsuming.result.next.buffer,
1048
+ optionsTerminated: provisionalConsuming.result.next.optionsTerminated,
1049
+ state: createExclusiveState(context.state, provisionalConsuming.index, provisionalConsuming.parser, provisionalConsuming.result),
1050
+ ...mergedExec != null ? {
1051
+ exec: mergedExec,
1052
+ dependencyRegistry: mergedExec.dependencyRegistry
1053
+ } : {}
1054
+ },
1055
+ consumed: provisionalConsuming.result.consumed
1056
+ };
1057
+ }
1058
+ if (activeState != null && activeState[1].success) {
1059
+ const previouslyConsumed = activeState[1].consumed;
1060
+ const checkResult = await provisionalConsuming.parser.parse({
1061
+ ...withChildContext(context, provisionalConsuming.index, provisionalConsuming.parser.initialState, provisionalConsuming.parser),
1062
+ buffer: previouslyConsumed
1063
+ });
1064
+ const canConsumeShared = checkResult.success && checkResult.consumed.length === previouslyConsumed.length && checkResult.consumed.every((c, idx) => c === previouslyConsumed[idx]);
1065
+ if (canConsumeShared && checkResult.success) {
1066
+ const replayExec = mergeChildExec(context.exec, checkResult.next.exec);
1067
+ const replayedResult = await provisionalConsuming.parser.parse(withChildContext({
1068
+ ...context,
1069
+ ...replayExec != null ? {
1070
+ exec: replayExec,
1071
+ dependencyRegistry: replayExec.dependencyRegistry
1072
+ } : {}
1073
+ }, provisionalConsuming.index, checkResult.next.state, provisionalConsuming.parser));
1074
+ if (replayedResult.success) {
1075
+ const mergedExec = mergeChildExec(replayExec, replayedResult.next.exec);
1076
+ return {
1077
+ success: true,
1078
+ provisional: true,
1079
+ next: {
1080
+ ...context,
1081
+ buffer: replayedResult.next.buffer,
1082
+ optionsTerminated: replayedResult.next.optionsTerminated,
1083
+ state: createExclusiveState(context.state, provisionalConsuming.index, provisionalConsuming.parser, {
1084
+ ...replayedResult,
1085
+ consumed: [...previouslyConsumed, ...replayedResult.consumed]
1086
+ }),
1087
+ ...mergedExec != null ? {
1088
+ exec: mergedExec,
1089
+ dependencyRegistry: mergedExec.dependencyRegistry
1090
+ } : {}
1091
+ },
1092
+ consumed: replayedResult.consumed
1093
+ };
1094
+ }
1095
+ }
1096
+ }
1097
+ }
930
1098
  return {
931
1099
  ...error,
932
1100
  success: false
@@ -1020,7 +1188,8 @@ function longestMatch(...args) {
1020
1188
  const result = parser.parse(withChildContext(context, i, activeState == null || activeState[0] !== i || !activeState[1].success ? parser.initialState : activeState[1].next.state, parser));
1021
1189
  if (result.success) {
1022
1190
  const consumed = context.buffer.length - result.next.buffer.length;
1023
- if (bestMatch === null || consumed > bestMatch.consumed) bestMatch = {
1191
+ const bestIsProvisional = bestMatch != null && bestMatch.result.success && !!bestMatch.result.provisional;
1192
+ if (bestMatch === null || consumed > bestMatch.consumed || consumed === bestMatch.consumed && bestIsProvisional && !result.provisional) bestMatch = {
1024
1193
  index: i,
1025
1194
  parser,
1026
1195
  result,
@@ -1060,7 +1229,8 @@ function longestMatch(...args) {
1060
1229
  const result = await resultOrPromise;
1061
1230
  if (result.success) {
1062
1231
  const consumed = context.buffer.length - result.next.buffer.length;
1063
- if (bestMatch === null || consumed > bestMatch.consumed) bestMatch = {
1232
+ const bestIsProvisional = bestMatch != null && bestMatch.result.success && !!bestMatch.result.provisional;
1233
+ if (bestMatch === null || consumed > bestMatch.consumed || consumed === bestMatch.consumed && bestIsProvisional && !result.provisional) bestMatch = {
1064
1234
  index: i,
1065
1235
  parser,
1066
1236
  result,
@@ -3656,18 +3826,35 @@ function group(label, parser, options = {}) {
3656
3826
  * // defaultResult.value = [undefined, {}]
3657
3827
  * ```
3658
3828
  *
3659
- * ### Async discriminator limitation
3829
+ * ### Speculative branch parsing
3660
3830
  *
3661
3831
  * When the discriminator is an async parser that succeeds without consuming
3662
3832
  * input (e.g., `prompt(option(...))` with no CLI input), branch selection
3663
- * is deferred to the complete phase. If the selected branch needs to
3664
- * consume remaining tokens, those tokens cannot be consumed because the
3665
- * branch is not known during parse. In practice, this means
3666
- * `conditional(prompt(option(...)), { key: option(...) })` cannot parse
3667
- * branch-specific tokens when the discriminator requires interactive
3668
- * resolution. Provide a default branch or ensure the discriminator
3669
- * can resolve synchronously (e.g., via `bindEnv()` or `withDefault()`)
3670
- * to avoid this limitation.
3833
+ * is normally deferred to the complete phase. To allow branch-specific
3834
+ * tokens to be consumed, `conditional()` speculatively tries all named
3835
+ * branches during parse. If exactly one branch can consume tokens, it is
3836
+ * tentatively selected and verified against the resolved discriminator
3837
+ * during the complete phase.
3838
+ *
3839
+ * If the discriminator resolves to a different branch than the one that
3840
+ * consumed tokens (contradictory input), the parse fails. When multiple
3841
+ * branches can consume the same tokens (ambiguous), speculation is skipped
3842
+ * entirely to keep branch selection order-independent.
3843
+ *
3844
+ * #### Known limitations
3845
+ *
3846
+ * - When a default branch accepts the same tokens as a named branch,
3847
+ * speculation prefers the named branch. If the discriminator later
3848
+ * resolves to a value not in the named branches, the parse fails
3849
+ * instead of falling back to the default branch. To avoid this,
3850
+ * ensure named branch options are distinct from the default branch.
3851
+ * - Within `longestMatch()`, a longer speculative match can beat a
3852
+ * shorter definitive one. If the speculative match fails during
3853
+ * completion, the tokens consumed by it are not recoverable.
3854
+ * - The dependency runtime seeds both discriminator and branch sources
3855
+ * before verifying the speculative selection. A discriminator that
3856
+ * depends on branch-local dependency sources could be circularly
3857
+ * confirmed by the speculative branch.
3671
3858
  *
3672
3859
  * @since 0.8.0
3673
3860
  */
@@ -3880,13 +4067,87 @@ function conditional(discriminator, branches, defaultBranch, options) {
3880
4067
  const discriminatorResult = await discriminator.parse({ ...withChildContext(context, "_discriminator", state.discriminatorState, discriminator) });
3881
4068
  if (discriminatorResult.success) {
3882
4069
  if (discriminatorResult.consumed.length === 0 && discriminator.$mode === "async") {
4070
+ const discriminatorExec = mergeChildExec(context.exec, discriminatorResult.next.exec);
4071
+ const speculationContext = {
4072
+ ...context,
4073
+ buffer: discriminatorResult.next.buffer,
4074
+ optionsTerminated: discriminatorResult.next.optionsTerminated,
4075
+ ...discriminatorExec != null ? {
4076
+ exec: discriminatorExec,
4077
+ dependencyRegistry: discriminatorExec.dependencyRegistry
4078
+ } : {}
4079
+ };
4080
+ let speculativeHit;
4081
+ let provisionalHit;
4082
+ let provisionalAmbiguous = false;
4083
+ let speculativeError;
4084
+ let ambiguous = false;
4085
+ for (const [key, bp] of branchParsers) {
4086
+ const branchResult = await bp.parse(withChildContext(speculationContext, "_branch", bp.initialState, bp, bp.usage));
4087
+ if (branchResult.success && branchResult.consumed.length > 0) {
4088
+ if (branchResult.provisional) {
4089
+ if (provisionalHit == null && !provisionalAmbiguous) provisionalHit = {
4090
+ key,
4091
+ bp,
4092
+ result: branchResult
4093
+ };
4094
+ else {
4095
+ provisionalHit = void 0;
4096
+ provisionalAmbiguous = true;
4097
+ }
4098
+ continue;
4099
+ }
4100
+ if (speculativeHit != null) {
4101
+ ambiguous = true;
4102
+ break;
4103
+ }
4104
+ speculativeHit = {
4105
+ key,
4106
+ bp,
4107
+ result: branchResult
4108
+ };
4109
+ }
4110
+ if (!branchResult.success && branchResult.consumed > 0 && (speculativeError == null || speculativeError.consumed < branchResult.consumed)) speculativeError = branchResult;
4111
+ }
4112
+ if (speculativeHit != null && (provisionalHit != null || provisionalAmbiguous)) ambiguous = true;
4113
+ if (speculativeHit == null && !ambiguous && !provisionalAmbiguous && provisionalHit != null) speculativeHit = provisionalHit;
4114
+ if (speculativeHit != null && !ambiguous) {
4115
+ const { key, bp, result: branchResult } = speculativeHit;
4116
+ if (branchResult.success) {
4117
+ const annotatedDiscriminatorState$2 = getAnnotatedChildState(state, discriminatorResult.next.state, discriminator);
4118
+ const mergedExec = mergeChildExec(discriminatorExec, branchResult.next.exec);
4119
+ return {
4120
+ success: true,
4121
+ provisional: true,
4122
+ next: {
4123
+ ...branchResult.next,
4124
+ state: {
4125
+ ...state,
4126
+ discriminatorState: annotatedDiscriminatorState$2,
4127
+ selectedBranch: {
4128
+ kind: "branch",
4129
+ key
4130
+ },
4131
+ branchState: getAnnotatedChildState(state, branchResult.next.state, bp),
4132
+ speculative: true
4133
+ },
4134
+ ...mergedExec != null ? {
4135
+ exec: mergedExec,
4136
+ dependencyRegistry: mergedExec.dependencyRegistry
4137
+ } : {}
4138
+ },
4139
+ consumed: branchResult.consumed
4140
+ };
4141
+ }
4142
+ }
3883
4143
  let deferredBranchState = state.branchState;
3884
4144
  if (defaultBranch !== void 0) {
3885
- const defaultResult = await defaultBranch.parse(withChildContext(context, "_branch", state.branchState ?? defaultBranch.initialState, defaultBranch, defaultBranch.usage));
4145
+ const defaultResult = await defaultBranch.parse(withChildContext(speculationContext, "_branch", state.branchState ?? defaultBranch.initialState, defaultBranch, defaultBranch.usage));
3886
4146
  if (defaultResult.success && defaultResult.consumed.length > 0) {
3887
- const defaultExec = mergeChildExec(context.exec, defaultResult.next.exec);
4147
+ const defaultExec = mergeChildExec(discriminatorExec ?? context.exec, defaultResult.next.exec);
3888
4148
  return {
3889
4149
  success: true,
4150
+ ...defaultResult.provisional ? { provisional: true } : {},
3890
4151
  next: {
3891
4152
  ...defaultResult.next,
3892
4153
  state: {
@@ -3903,10 +4164,10 @@ function conditional(discriminator, branches, defaultBranch, options) {
3903
4164
  };
3904
4165
  }
3905
4166
  if (!defaultResult.success && defaultResult.consumed > 0) return defaultResult;
3906
- if (defaultResult.success && defaultResult.consumed.length === 0 && context.buffer.length === 0) deferredBranchState = getAnnotatedChildState(state, defaultResult.next.state, defaultBranch);
4167
+ if (defaultResult.success && defaultResult.consumed.length === 0 && speculationContext.buffer.length === 0) deferredBranchState = getAnnotatedChildState(state, defaultResult.next.state, defaultBranch);
3907
4168
  }
4169
+ if (speculativeError != null && !ambiguous) return speculativeError;
3908
4170
  const annotatedDiscriminatorState$1 = getAnnotatedChildState(state, discriminatorResult.next.state, discriminator);
3909
- const mergedExec = mergeChildExec(context.exec, discriminatorResult.next.exec);
3910
4171
  return {
3911
4172
  success: true,
3912
4173
  provisional: true,
@@ -3917,9 +4178,9 @@ function conditional(discriminator, branches, defaultBranch, options) {
3917
4178
  discriminatorState: annotatedDiscriminatorState$1,
3918
4179
  branchState: deferredBranchState
3919
4180
  },
3920
- ...mergedExec != null ? {
3921
- exec: mergedExec,
3922
- dependencyRegistry: mergedExec.dependencyRegistry
4181
+ ...discriminatorExec != null ? {
4182
+ exec: discriminatorExec,
4183
+ dependencyRegistry: discriminatorExec.dependencyRegistry
3923
4184
  } : {}
3924
4185
  },
3925
4186
  consumed: []
@@ -4141,6 +4402,18 @@ function conditional(discriminator, branches, defaultBranch, options) {
4141
4402
  };
4142
4403
  };
4143
4404
  const completeAsync = async (state, exec) => {
4405
+ let wasSpeculative = false;
4406
+ if (state.speculative && state.selectedBranch?.kind === "branch") if (exec?.phase !== "parse" && exec?.phase !== "suggest") {
4407
+ wasSpeculative = true;
4408
+ state = {
4409
+ ...state,
4410
+ speculative: void 0
4411
+ };
4412
+ } else state = {
4413
+ ...state,
4414
+ discriminatorValue: state.selectedBranch.key,
4415
+ speculative: void 0
4416
+ };
4144
4417
  if (state.selectedBranch === void 0) {
4145
4418
  if (exec?.phase !== "parse" && exec?.phase !== "suggest") {
4146
4419
  const annotatedDiscriminatorStateForDeferred = getAnnotatedChildState(state, state.discriminatorState, discriminator);
@@ -4225,20 +4498,30 @@ function conditional(discriminator, branches, defaultBranch, options) {
4225
4498
  };
4226
4499
  const needsDiscriminatorCompletion = state.selectedBranch.kind !== "default" && !(state.discriminatorValue != null && state.discriminatorValue === state.selectedBranch.key);
4227
4500
  const discriminatorCompleteResult = needsDiscriminatorCompletion ? await discriminator.complete(annotatedDiscriminatorState, withChildExecPath(completionExec, "_discriminator")) : void 0;
4228
- const branchResult = unwrapCompleteResult(await branchParser.complete(resolvedBranchState, withChildExecPath(completionExec, "_branch")));
4229
- if (!branchResult.success) {
4230
- if (state.discriminatorValue !== void 0 && options?.errors?.branchError) return {
4231
- success: false,
4232
- error: options.errors.branchError(state.discriminatorValue, branchResult.error)
4233
- };
4234
- return branchResult;
4235
- }
4236
4501
  let discriminatorValue;
4237
4502
  if (state.selectedBranch.kind === "default") discriminatorValue = void 0;
4238
4503
  else if (state.discriminatorValue != null && state.discriminatorValue === state.selectedBranch.key) discriminatorValue = state.discriminatorValue;
4239
4504
  else {
4240
4505
  const completedDiscriminator = unwrapCompleteResult(discriminatorCompleteResult);
4241
- discriminatorValue = completedDiscriminator.success ? completedDiscriminator.value : state.selectedBranch.key;
4506
+ if (completedDiscriminator.success) discriminatorValue = completedDiscriminator.value;
4507
+ else if (wasSpeculative) return completedDiscriminator;
4508
+ else discriminatorValue = state.selectedBranch.key;
4509
+ }
4510
+ if (wasSpeculative && state.selectedBranch.kind === "branch" && discriminatorValue !== state.selectedBranch.key) {
4511
+ const speculativeKey = state.selectedBranch.key;
4512
+ const resolvedKey = discriminatorValue ?? "";
4513
+ return {
4514
+ success: false,
4515
+ error: options?.errors?.branchMismatch ? options.errors.branchMismatch(resolvedKey, speculativeKey) : message`Branch mismatch: tokens for ${speculativeKey} were consumed, but the discriminator resolved to ${resolvedKey}.`
4516
+ };
4517
+ }
4518
+ const branchResult = unwrapCompleteResult(await branchParser.complete(resolvedBranchState, withChildExecPath(completionExec, "_branch")));
4519
+ if (!branchResult.success) {
4520
+ if (discriminatorValue !== void 0 && options?.errors?.branchError) return {
4521
+ success: false,
4522
+ error: options.errors.branchError(discriminatorValue, branchResult.error)
4523
+ };
4524
+ return branchResult;
4242
4525
  }
4243
4526
  return {
4244
4527
  success: true,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@optique/core",
3
- "version": "1.0.0-dev.1711+3514d59b",
3
+ "version": "1.0.0-dev.1714+ef964285",
4
4
  "description": "Type-safe combinatorial command-line interface parser",
5
5
  "keywords": [
6
6
  "CLI",