@optique/core 1.0.0-dev.1616 → 1.0.0-dev.1659

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.
@@ -1,5 +1,6 @@
1
1
  import { DependencyRegistryLike } from "./registry-types.cjs";
2
2
  import { ValueParserResult } from "./valueparser.cjs";
3
+ import { ParserDependencyMetadata } from "./dependency-metadata.cjs";
3
4
 
4
5
  //#region src/dependency-runtime.d.ts
5
6
 
@@ -76,7 +77,32 @@ interface ReplayKey {
76
77
  * @internal
77
78
  * @since 1.0.0
78
79
  */
79
-
80
+ interface RuntimeNode {
81
+ /** Path from root to this parser node. */
82
+ readonly path: readonly PropertyKey[];
83
+ /** The parser (only the metadata field is inspected). */
84
+ readonly parser: {
85
+ readonly dependencyMetadata?: ParserDependencyMetadata;
86
+ };
87
+ /** The parser's current state. */
88
+ readonly state: unknown;
89
+ /**
90
+ * Whether the parser consumed explicit input during parsing.
91
+ * When `true`, the parser's state reflects user-provided input (which
92
+ * may have failed validation). Missing-source defaults must not override
93
+ * explicit parse failures.
94
+ * @since 1.0.0
95
+ */
96
+ readonly matched?: boolean;
97
+ /**
98
+ * Snapshotted default dependency values for derived parsers.
99
+ * Constructs should populate this at node creation time (once) to
100
+ * avoid re-evaluating dynamic `getDefaultDependencyValues()` thunks
101
+ * at replay time.
102
+ * @since 1.0.0
103
+ */
104
+ readonly defaultDependencyValues?: readonly unknown[];
105
+ }
80
106
  /**
81
107
  * Dependency runtime context for centralized dependency resolution.
82
108
  *
@@ -120,4 +146,4 @@ interface DependencyRuntimeContext {
120
146
  * @since 1.0.0
121
147
  */
122
148
  //#endregion
123
- export { DependencyRuntimeContext };
149
+ export { DependencyRuntimeContext, RuntimeNode };
@@ -1,5 +1,6 @@
1
1
  import { DependencyRegistryLike } from "./registry-types.js";
2
2
  import { ValueParserResult } from "./valueparser.js";
3
+ import { ParserDependencyMetadata } from "./dependency-metadata.js";
3
4
 
4
5
  //#region src/dependency-runtime.d.ts
5
6
 
@@ -76,7 +77,32 @@ interface ReplayKey {
76
77
  * @internal
77
78
  * @since 1.0.0
78
79
  */
79
-
80
+ interface RuntimeNode {
81
+ /** Path from root to this parser node. */
82
+ readonly path: readonly PropertyKey[];
83
+ /** The parser (only the metadata field is inspected). */
84
+ readonly parser: {
85
+ readonly dependencyMetadata?: ParserDependencyMetadata;
86
+ };
87
+ /** The parser's current state. */
88
+ readonly state: unknown;
89
+ /**
90
+ * Whether the parser consumed explicit input during parsing.
91
+ * When `true`, the parser's state reflects user-provided input (which
92
+ * may have failed validation). Missing-source defaults must not override
93
+ * explicit parse failures.
94
+ * @since 1.0.0
95
+ */
96
+ readonly matched?: boolean;
97
+ /**
98
+ * Snapshotted default dependency values for derived parsers.
99
+ * Constructs should populate this at node creation time (once) to
100
+ * avoid re-evaluating dynamic `getDefaultDependencyValues()` thunks
101
+ * at replay time.
102
+ * @since 1.0.0
103
+ */
104
+ readonly defaultDependencyValues?: readonly unknown[];
105
+ }
80
106
  /**
81
107
  * Dependency runtime context for centralized dependency resolution.
82
108
  *
@@ -120,4 +146,4 @@ interface DependencyRuntimeContext {
120
146
  * @since 1.0.0
121
147
  */
122
148
  //#endregion
123
- export { DependencyRuntimeContext };
149
+ export { DependencyRuntimeContext, RuntimeNode };
@@ -1,4 +1,6 @@
1
+ import { message } from "./message.js";
1
2
  import { dependencyId, isDeferredParseState, isDependencySourceState, isPendingDependencySourceState, parseWithDependency } from "./dependency.js";
3
+ import { unmatchedNonCliDependencySourceStateMarker } from "./parser.js";
2
4
 
3
5
  //#region src/dependency-runtime.ts
4
6
  const symbolIds = /* @__PURE__ */ new WeakMap();
@@ -18,7 +20,11 @@ var DependencyRuntimeContextImpl = class {
18
20
  #replayCache = /* @__PURE__ */ new Map();
19
21
  #failedSources = /* @__PURE__ */ new Set();
20
22
  constructor(registry) {
21
- this.registry = registry;
23
+ if (registry instanceof FailedAwareRegistry) {
24
+ this.registry = registry.rebindFailedSources(this.#failedSources);
25
+ return;
26
+ }
27
+ this.registry = new FailedAwareRegistry(registry, this.#failedSources);
22
28
  }
23
29
  registerSource(sourceId, value, _origin) {
24
30
  this.registry.set(sourceId, value);
@@ -48,6 +54,47 @@ var DependencyRuntimeContextImpl = class {
48
54
  return resolveRequest(this, request);
49
55
  }
50
56
  };
57
+ /**
58
+ * Registry wrapper that hides values for sources that have failed.
59
+ *
60
+ * The wrapper lets clones share an underlying registry while maintaining
61
+ * an isolated failed-source view, so later lookups do not reuse stale
62
+ * values after extraction errors.
63
+ *
64
+ * @internal
65
+ */
66
+ var FailedAwareRegistry = class FailedAwareRegistry {
67
+ #inner;
68
+ #failedSources;
69
+ constructor(inner, failedSources) {
70
+ this.#inner = inner;
71
+ this.#failedSources = failedSources;
72
+ }
73
+ set(id, value) {
74
+ this.#inner.set(id, value);
75
+ this.#failedSources.delete(id);
76
+ }
77
+ get(id) {
78
+ if (this.#failedSources.has(id)) return void 0;
79
+ return this.#inner.get(id);
80
+ }
81
+ has(id) {
82
+ if (this.#failedSources.has(id)) return false;
83
+ return this.#inner.has(id);
84
+ }
85
+ copyFailedSources(target) {
86
+ for (const sourceId of this.#failedSources) target.add(sourceId);
87
+ }
88
+ rebindFailedSources(target) {
89
+ this.copyFailedSources(target);
90
+ return new FailedAwareRegistry(this.#inner, target);
91
+ }
92
+ clone() {
93
+ const failedSources = new Set(this.#failedSources);
94
+ const innerClone = this.#inner.clone();
95
+ return innerClone instanceof FailedAwareRegistry ? innerClone.rebindFailedSources(failedSources) : new FailedAwareRegistry(innerClone, failedSources);
96
+ }
97
+ };
51
98
  function resolveRequest(ctx, request) {
52
99
  const values = [];
53
100
  const usedDefaults = [];
@@ -55,13 +102,13 @@ function resolveRequest(ctx, request) {
55
102
  let defaultedCount = 0;
56
103
  for (let i = 0; i < request.dependencyIds.length; i++) {
57
104
  const id = request.dependencyIds[i];
58
- if (ctx.hasSource(id)) {
105
+ if (ctx.isSourceFailed(id)) {
106
+ values.push(void 0);
107
+ usedDefaults.push(false);
108
+ } else if (ctx.hasSource(id)) {
59
109
  values.push(ctx.getSource(id));
60
110
  usedDefaults.push(false);
61
111
  resolvedCount++;
62
- } else if (ctx.isSourceFailed(id)) {
63
- values.push(void 0);
64
- usedDefaults.push(false);
65
112
  } else if (request.defaultValues != null && i < request.defaultValues.length) {
66
113
  values.push(request.defaultValues[i]);
67
114
  usedDefaults.push(true);
@@ -126,11 +173,80 @@ function createDependencyRuntimeContext(registry) {
126
173
  return new DependencyRuntimeContextImpl(registry ?? new SimpleRegistry());
127
174
  }
128
175
  /**
176
+ * Creates a stable fingerprint from dependency values.
177
+ *
178
+ * @param values The dependency values to fingerprint.
179
+ * @returns A string fingerprint.
180
+ * @internal
181
+ * @since 1.0.0
182
+ */
183
+ function createDependencyFingerprint(values) {
184
+ return values.map((v) => {
185
+ const raw = fingerprintValue(v);
186
+ return `${raw.length}:${raw}`;
187
+ }).join("");
188
+ }
189
+ const objectIds = /* @__PURE__ */ new WeakMap();
190
+ let objectIdCounter = 0;
191
+ function fingerprintValue(v) {
192
+ if (v === void 0) return "u:";
193
+ if (v === null) return "n:";
194
+ if (typeof v === "string") return `s:${v}`;
195
+ if (typeof v === "number") {
196
+ if (Object.is(v, -0)) return "d:-0";
197
+ return `d:${v}`;
198
+ }
199
+ if (typeof v === "boolean") return `b:${v}`;
200
+ if (typeof v === "symbol") return `y:${stableSymbolKey(v)}`;
201
+ if (typeof v === "object" || typeof v === "function") {
202
+ let id = objectIds.get(v);
203
+ if (id === void 0) {
204
+ id = objectIdCounter++;
205
+ objectIds.set(v, id);
206
+ }
207
+ return `o:${id}`;
208
+ }
209
+ return `?:${String(v)}`;
210
+ }
211
+ /**
212
+ * Creates a {@link ReplayKey} from a path, raw input, and dependency values.
213
+ *
214
+ * @param path The parser path.
215
+ * @param rawInput The raw input string.
216
+ * @param dependencyValues The dependency values.
217
+ * @returns A replay key.
218
+ * @internal
219
+ * @since 1.0.0
220
+ */
221
+ const parserIds = /* @__PURE__ */ new WeakMap();
222
+ let parserIdCounter = 0;
223
+ /** Get a stable identity string for a replayParse function reference. */
224
+ function getParserFingerprint(replayParse) {
225
+ let id = parserIds.get(replayParse);
226
+ if (id === void 0) {
227
+ id = parserIdCounter++;
228
+ parserIds.set(replayParse, id);
229
+ }
230
+ return `p:${id}`;
231
+ }
232
+ function createReplayKey(path, rawInput, dependencyValues, replayParse) {
233
+ return {
234
+ path,
235
+ rawInput,
236
+ dependencyFingerprint: createDependencyFingerprint(dependencyValues),
237
+ parserFingerprint: replayParse != null ? getParserFingerprint(replayParse) : ""
238
+ };
239
+ }
240
+ /**
129
241
  * Collects explicit source values from parser states and registers them
130
242
  * in the runtime context.
131
243
  *
132
244
  * @param nodes The runtime nodes to inspect.
133
245
  * @param runtime The dependency runtime context.
246
+ * @throws Propagates synchronous errors thrown by `extractSourceValue()`.
247
+ * @throws {TypeError} If `extractSourceValue()` returns a promise-like result.
248
+ * Use {@link collectExplicitSourceValuesAsync} when async source
249
+ * extraction is expected.
134
250
  * @internal
135
251
  * @since 1.0.0
136
252
  */
@@ -140,10 +256,205 @@ function collectExplicitSourceValues(nodes, runtime) {
140
256
  if (meta?.source == null) continue;
141
257
  if (meta.source.extractSourceValue == null) continue;
142
258
  const result = meta.source.extractSourceValue(node.state);
143
- if (result == null) continue;
144
- if (result.success) runtime.registerSource(meta.source.sourceId, result.value, "cli");
145
- else runtime.markSourceFailed(meta.source.sourceId);
259
+ if (isPromiseLike(result)) throw new TypeError(`collectExplicitSourceValues() received an async extractSourceValue() result for ${String(meta.source.sourceId)}. Use collectExplicitSourceValuesAsync() instead.`);
260
+ registerExplicitSourceValue(meta.source.sourceId, result, runtime);
261
+ }
262
+ }
263
+ function registerExplicitSourceValue(sourceId, result, runtime) {
264
+ if (result == null) return;
265
+ if (result.success) runtime.registerSource(sourceId, result.value, "cli");
266
+ else runtime.markSourceFailed(sourceId);
267
+ }
268
+ function isPromiseLike(value) {
269
+ return value != null && (typeof value === "object" || typeof value === "function") && "then" in value && typeof value.then === "function";
270
+ }
271
+ /**
272
+ * Async version of {@link collectExplicitSourceValues}.
273
+ * Awaits async `extractSourceValue` results instead of rejecting
274
+ * promise-like values as the synchronous variant does.
275
+ *
276
+ * @param nodes The runtime nodes to inspect.
277
+ * @param runtime The dependency runtime context.
278
+ * @throws Propagates errors thrown or rejected by `extractSourceValue()`.
279
+ * @internal
280
+ * @since 1.0.0
281
+ */
282
+ async function collectExplicitSourceValuesAsync(nodes, runtime) {
283
+ for (const node of nodes) {
284
+ const meta = node.parser.dependencyMetadata;
285
+ if (meta?.source == null) continue;
286
+ if (meta.source.extractSourceValue == null) continue;
287
+ const result = await meta.source.extractSourceValue(node.state);
288
+ registerExplicitSourceValue(meta.source.sourceId, result, runtime);
289
+ }
290
+ }
291
+ /**
292
+ * Fills missing source defaults for source parsers whose state is
293
+ * unpopulated.
294
+ *
295
+ * Returns an array of failures for sources whose default evaluation
296
+ * failed (either threw or returned `{ success: false }`). The caller
297
+ * should propagate these so that dependent parsers see the real error
298
+ * instead of silently treating the source as absent.
299
+ *
300
+ * @param nodes The runtime nodes to inspect.
301
+ * @param runtime The dependency runtime context.
302
+ * @returns Failures from default evaluation (empty if all succeeded).
303
+ * @throws {TypeError} If `getMissingSourceValue()` returns a promise-like
304
+ * result. Use {@link fillMissingSourceDefaultsAsync} when async
305
+ * default extraction is expected.
306
+ * @internal
307
+ * @since 1.0.0
308
+ */
309
+ function fillMissingSourceDefaults(nodes, runtime) {
310
+ const failures = [];
311
+ for (const node of nodes) {
312
+ const meta = node.parser.dependencyMetadata;
313
+ if (meta?.source == null) continue;
314
+ if (runtime.hasSource(meta.source.sourceId)) continue;
315
+ if (runtime.isSourceFailed(meta.source.sourceId)) continue;
316
+ if (node.matched === true) continue;
317
+ if (!meta.source.preservesSourceValue) continue;
318
+ if (meta.source.getMissingSourceValue == null) continue;
319
+ let result;
320
+ try {
321
+ result = meta.source.getMissingSourceValue();
322
+ } catch (e) {
323
+ const msg = e instanceof Error ? e.message : String(e);
324
+ failures.push({
325
+ sourceId: meta.source.sourceId,
326
+ path: node.path,
327
+ error: {
328
+ success: false,
329
+ error: message`Default value evaluation failed: ${msg}`
330
+ }
331
+ });
332
+ continue;
333
+ }
334
+ if (isPromiseLike(result)) throw new TypeError(`fillMissingSourceDefaults() received an async getMissingSourceValue() result for ${String(meta.source.sourceId)}. Use fillMissingSourceDefaultsAsync() instead.`);
335
+ if (result.success) runtime.registerSource(meta.source.sourceId, result.value, "default");
336
+ else failures.push({
337
+ sourceId: meta.source.sourceId,
338
+ path: node.path,
339
+ error: result
340
+ });
341
+ }
342
+ return failures;
343
+ }
344
+ /**
345
+ * Async version of {@link fillMissingSourceDefaults}.
346
+ * Awaits async `getMissingSourceValue` results.
347
+ *
348
+ * @param nodes The runtime nodes to inspect.
349
+ * @param runtime The dependency runtime context.
350
+ * @returns Failures from default evaluation (empty if all succeeded).
351
+ * @internal
352
+ * @since 1.0.0
353
+ */
354
+ async function fillMissingSourceDefaultsAsync(nodes, runtime) {
355
+ const failures = [];
356
+ for (const node of nodes) {
357
+ const meta = node.parser.dependencyMetadata;
358
+ if (meta?.source == null) continue;
359
+ if (runtime.hasSource(meta.source.sourceId)) continue;
360
+ if (runtime.isSourceFailed(meta.source.sourceId)) continue;
361
+ if (node.matched === true) continue;
362
+ if (!meta.source.preservesSourceValue) continue;
363
+ if (meta.source.getMissingSourceValue == null) continue;
364
+ let result;
365
+ try {
366
+ result = await meta.source.getMissingSourceValue();
367
+ } catch (e) {
368
+ const msg = e instanceof Error ? e.message : String(e);
369
+ failures.push({
370
+ sourceId: meta.source.sourceId,
371
+ path: node.path,
372
+ error: {
373
+ success: false,
374
+ error: message`Default value evaluation failed: ${msg}`
375
+ }
376
+ });
377
+ continue;
378
+ }
379
+ if (result.success) runtime.registerSource(meta.source.sourceId, result.value, "default");
380
+ else failures.push({
381
+ sourceId: meta.source.sourceId,
382
+ path: node.path,
383
+ error: result
384
+ });
385
+ }
386
+ return failures;
387
+ }
388
+ /**
389
+ * Replays a derived parser with resolved dependency values (sync).
390
+ *
391
+ * Returns `undefined` if dependencies cannot be resolved.
392
+ *
393
+ * @param node The runtime node with derived metadata.
394
+ * @param rawInput The raw input to replay.
395
+ * @param runtime The dependency runtime context.
396
+ * @returns The replay result, or `undefined`.
397
+ * @throws {TypeError} If `replayParse()` returns a promise-like result.
398
+ * Use {@link replayDerivedParserAsync} for async replay.
399
+ * @internal
400
+ * @since 1.0.0
401
+ */
402
+ function replayDerivedParser(node, rawInput, runtime) {
403
+ const meta = node.parser.dependencyMetadata;
404
+ if (meta?.derived == null) return void 0;
405
+ let defaults = node.defaultDependencyValues;
406
+ if (defaults == null && meta.derived.getDefaultDependencyValues != null) try {
407
+ defaults = meta.derived.getDefaultDependencyValues();
408
+ } catch {
409
+ return void 0;
410
+ }
411
+ const resolution = runtime.resolveDependencies({
412
+ dependencyIds: meta.derived.dependencyIds,
413
+ defaultValues: defaults
414
+ });
415
+ if (resolution.kind === "missing") return void 0;
416
+ if (resolution.kind === "partial") return void 0;
417
+ const key = createReplayKey(node.path, rawInput, resolution.values, meta.derived.replayParse);
418
+ const cached = runtime.getReplayResult(key);
419
+ if (cached != null) return cached;
420
+ const result = meta.derived.replayParse(rawInput, resolution.values);
421
+ if (isPromiseLike(result)) throw new TypeError("replayDerivedParser() received an async replayParse() result. Use replayDerivedParserAsync() instead.");
422
+ runtime.setReplayResult(key, result);
423
+ return result;
424
+ }
425
+ /**
426
+ * Replays a derived parser with resolved dependency values (async).
427
+ *
428
+ * Returns `undefined` if dependencies cannot be resolved.
429
+ *
430
+ * @param node The runtime node with derived metadata.
431
+ * @param rawInput The raw input to replay.
432
+ * @param runtime The dependency runtime context.
433
+ * @returns The replay result, or `undefined`.
434
+ * @internal
435
+ * @since 1.0.0
436
+ */
437
+ async function replayDerivedParserAsync(node, rawInput, runtime) {
438
+ const meta = node.parser.dependencyMetadata;
439
+ if (meta?.derived == null) return void 0;
440
+ let defaults = node.defaultDependencyValues;
441
+ if (defaults == null && meta.derived.getDefaultDependencyValues != null) try {
442
+ defaults = meta.derived.getDefaultDependencyValues();
443
+ } catch {
444
+ return void 0;
146
445
  }
446
+ const resolution = runtime.resolveDependencies({
447
+ dependencyIds: meta.derived.dependencyIds,
448
+ defaultValues: defaults
449
+ });
450
+ if (resolution.kind === "missing") return void 0;
451
+ if (resolution.kind === "partial") return void 0;
452
+ const key = createReplayKey(node.path, rawInput, resolution.values, meta.derived.replayParse);
453
+ const cached = runtime.getReplayResult(key);
454
+ if (cached != null) return cached;
455
+ const result = await meta.derived.replayParse(rawInput, resolution.values);
456
+ runtime.setReplayResult(key, result);
457
+ return result;
147
458
  }
148
459
  /**
149
460
  * Checks if a value is a plain object (not a class instance) for the
@@ -171,7 +482,7 @@ function resolveSingleDeferred(deferred, runtime) {
171
482
  if (resolution.usedDefaults.every((d) => d)) return deferred.preliminaryResult;
172
483
  const depValue = isMultiDep ? resolution.values : resolution.values[0];
173
484
  const result = deferred.parser[parseWithDependency](deferred.rawInput, depValue);
174
- if (result instanceof Promise) return deferred.preliminaryResult;
485
+ if (isPromiseLike(result)) throw new TypeError("resolveStateWithRuntime() received an async parseWithDependency() result. Use resolveStateWithRuntimeAsync() instead.");
175
486
  return result;
176
487
  }
177
488
  /**
@@ -180,8 +491,14 @@ function resolveSingleDeferred(deferred, runtime) {
180
491
  *
181
492
  * This must run BEFORE deferred resolution so that all source values
182
493
  * are available when replaying derived parsers.
494
+ *
495
+ * @param state The state tree to traverse.
496
+ * @param runtime The dependency runtime context to populate.
497
+ * @param visited Cycle guard for recursive traversal.
498
+ * @param excludedFields Optional property keys to skip at the current level.
499
+ * This exclusion set is not propagated recursively.
183
500
  */
184
- function collectSourcesFromState(state, runtime, visited = /* @__PURE__ */ new WeakSet()) {
501
+ function collectSourcesFromState(state, runtime, visited = /* @__PURE__ */ new WeakSet(), excludedFields) {
185
502
  if (state == null || typeof state !== "object") return;
186
503
  if (visited.has(state)) return;
187
504
  visited.add(state);
@@ -197,7 +514,10 @@ function collectSourcesFromState(state, runtime, visited = /* @__PURE__ */ new W
197
514
  for (const item of state) collectSourcesFromState(item, runtime, visited);
198
515
  return;
199
516
  }
200
- if (typeof state === "object") for (const key of Reflect.ownKeys(state)) collectSourcesFromState(state[key], runtime, visited);
517
+ if (typeof state === "object") for (const key of Reflect.ownKeys(state)) {
518
+ if (excludedFields?.has(key)) continue;
519
+ collectSourcesFromState(state[key], runtime, visited);
520
+ }
201
521
  }
202
522
  /**
203
523
  * Recursively resolves all {@link DeferredParseState} objects in a state
@@ -214,6 +534,9 @@ function collectSourcesFromState(state, runtime, visited = /* @__PURE__ */ new W
214
534
  * @param state The state tree to resolve.
215
535
  * @param runtime The dependency runtime context.
216
536
  * @returns The resolved state tree.
537
+ * @throws {TypeError} If a deferred parser returns a promise-like result from
538
+ * `parseWithDependency()`. Use {@link resolveStateWithRuntimeAsync}
539
+ * for async resolution.
217
540
  * @internal
218
541
  * @since 1.0.0
219
542
  */
@@ -230,10 +553,16 @@ function resolveDeferredInState(state, runtime, visited = /* @__PURE__ */ new We
230
553
  }
231
554
  if (isDeferredParseState(state)) return resolveSingleDeferred(state, runtime);
232
555
  if (isDependencySourceState(state)) return state;
233
- if (Array.isArray(state)) return state.map((item) => resolveDeferredInState(item, runtime, visited));
556
+ if (Array.isArray(state)) {
557
+ const resolved = state.map((item) => resolveDeferredInState(item, runtime, visited));
558
+ return resolved.every((item, index) => item === state[index]) ? state : resolved;
559
+ }
234
560
  if (isPlainObject(state)) {
561
+ const keys = Reflect.ownKeys(state);
562
+ const resolvedEntries = keys.map((key) => [key, resolveDeferredInState(state[key], runtime, visited)]);
563
+ if (resolvedEntries.every(([key, value]) => value === state[key])) return state;
235
564
  const resolved = Object.create(Object.getPrototypeOf(state));
236
- for (const key of Reflect.ownKeys(state)) resolved[key] = resolveDeferredInState(state[key], runtime, visited);
565
+ for (const [key, value] of resolvedEntries) resolved[key] = value;
237
566
  return resolved;
238
567
  }
239
568
  return state;
@@ -272,13 +601,18 @@ async function resolveDeferredInStateAsync(state, runtime, visited = /* @__PURE_
272
601
  return Promise.resolve(deferred.parser[parseWithDependency](deferred.rawInput, depValue));
273
602
  }
274
603
  if (isDependencySourceState(state)) return state;
275
- if (Array.isArray(state)) return Promise.all(state.map((item) => resolveDeferredInStateAsync(item, runtime, visited)));
604
+ if (Array.isArray(state)) {
605
+ const resolved = await Promise.all(state.map((item) => resolveDeferredInStateAsync(item, runtime, visited)));
606
+ return resolved.every((item, index) => item === state[index]) ? state : resolved;
607
+ }
276
608
  if (isPlainObject(state)) {
277
- const resolved = Object.create(Object.getPrototypeOf(state));
278
609
  const keys = Reflect.ownKeys(state);
279
- await Promise.all(keys.map(async (key) => {
280
- resolved[key] = await resolveDeferredInStateAsync(state[key], runtime, visited);
610
+ const resolvedEntries = await Promise.all(keys.map(async (key) => {
611
+ return [key, await resolveDeferredInStateAsync(state[key], runtime, visited)];
281
612
  }));
613
+ if (resolvedEntries.every(([key, value]) => value === state[key])) return state;
614
+ const resolved = Object.create(Object.getPrototypeOf(state));
615
+ for (const [key, value] of resolvedEntries) resolved[key] = value;
282
616
  return resolved;
283
617
  }
284
618
  return state;
@@ -289,8 +623,9 @@ async function resolveDeferredInStateAsync(state, runtime, visited = /* @__PURE_
289
623
  */
290
624
  function isMatchedState(fieldState, parser) {
291
625
  if (fieldState === void 0) return false;
292
- if (Array.isArray(fieldState) && fieldState.length === 1 && isPendingDependencySourceState(fieldState[0])) return false;
293
- if (isPendingDependencySourceState(fieldState)) return false;
626
+ const innerState = Array.isArray(fieldState) && fieldState.length === 1 ? fieldState[0] : fieldState;
627
+ if (isPendingDependencySourceState(innerState)) return false;
628
+ if (parser[unmatchedNonCliDependencySourceStateMarker] === true && innerState != null && typeof innerState === "object" && Object.hasOwn(innerState, "hasCliValue") && innerState.hasCliValue === false) return false;
294
629
  if (fieldState === parser.initialState) return false;
295
630
  return true;
296
631
  }
@@ -320,6 +655,33 @@ function buildRuntimeNodesFromPairs(pairs, state, parentPath) {
320
655
  }
321
656
  return nodes;
322
657
  }
658
+ /**
659
+ * Builds {@link RuntimeNode}s from a parser array and a state array.
660
+ *
661
+ * Used by `tuple()` and `concat()` constructs.
662
+ *
663
+ * @param parsers The child parsers.
664
+ * @param stateArray The state array (one element per parser).
665
+ * @param parentPath Optional parent path prefix.
666
+ * @returns An array of runtime nodes.
667
+ * @internal
668
+ * @since 1.0.0
669
+ */
670
+ function buildRuntimeNodesFromArray(parsers, stateArray, parentPath) {
671
+ const prefix = parentPath ?? [];
672
+ const nodes = [];
673
+ for (let i = 0; i < parsers.length; i++) {
674
+ const parser = parsers[i];
675
+ const elemState = i < stateArray.length ? stateArray[i] : void 0;
676
+ nodes.push({
677
+ path: [...prefix, i],
678
+ parser,
679
+ state: elemState,
680
+ matched: isMatchedState(elemState, parser)
681
+ });
682
+ }
683
+ return nodes;
684
+ }
323
685
 
324
686
  //#endregion
325
- export { buildRuntimeNodesFromPairs, collectExplicitSourceValues, collectSourcesFromState, createDependencyRuntimeContext, resolveStateWithRuntime, resolveStateWithRuntimeAsync };
687
+ export { buildRuntimeNodesFromArray, buildRuntimeNodesFromPairs, collectExplicitSourceValues, collectExplicitSourceValuesAsync, collectSourcesFromState, createDependencyRuntimeContext, fillMissingSourceDefaults, fillMissingSourceDefaultsAsync, replayDerivedParser, replayDerivedParserAsync, resolveStateWithRuntime, resolveStateWithRuntimeAsync };