@loomcli/core 0.7.0 → 0.8.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.
@@ -4,8 +4,8 @@ import { escapeControlCharacters } from './controls.js';
4
4
  import { elided, spelled } from './diagnostic-text.js';
5
5
  import { asSentence, DeclarationError, InputError, quoted, reasonOf } from './errors.js';
6
6
  import { callSite, factFault, flagFault, partOf, siteFinding } from './facts.js';
7
- import { booleanOptionValueRule, defaultDepth, defaultLevels, defaultShape, invalidDefault, notAValidator, omissionAlreadyDecided, omissionWithoutValidator, requiredWithDefault, } from './input-rules.js';
8
- import { booleanValue } from './options.js';
7
+ import { booleanOptionValueRule, countOptionValueRule, defaultDepth, defaultLevels, defaultShape, invalidDefault, invalidImplied, notAValidator, omissionAlreadyDecided, omissionWithoutValidator, requiredWithDefault, } from './input-rules.js';
8
+ import { booleanValue, reportedOf, spellingsOf } from './options.js';
9
9
  import { boundedSnapshot, NestedTooDeepError, shallowList } from './plain.js';
10
10
  import { notAnObject } from './plugin-rules.js';
11
11
  import { validatorFailed } from './rules.js';
@@ -44,6 +44,10 @@ class ValidatedInputs {
44
44
  read(input) {
45
45
  return this.#values.get(input);
46
46
  }
47
+ /** Whether this pass validated the declaration's value. */
48
+ has(input) {
49
+ return this.#values.has(input);
50
+ }
47
51
  #field(input) {
48
52
  // Last resort: no typed path exists. The map stores every validated value as `unknown`.
49
53
  // An object literal with a generic computed key does not type as `Record<Name, _>` either.
@@ -56,14 +60,14 @@ class ValidatedInputs {
56
60
  }
57
61
  /**
58
62
  * Authoring's one read of a config, through `captureDeclaration`: the prototype verdict, the copy of
59
- * every own string key, the copy of its `extensions` list, and the snapshot of its default, inside
60
- * one try. Every later check, the registry entry, and every sentence read the copy and never the
61
- * author's object again, so a getter runs once and the caller's later changes reach nothing. The
62
- * default is copied and frozen to `defaultLevels` levels, cycles included, and that copy is the
63
- * value the graph publishes and a run validates; every other property is captured as declared,
64
- * because core clones no library object. A read that throws, from a getter or a proxy trap, is the
65
- * unreadable fault, named by the key it threw in, a default nested deeper is the too-deep fault,
66
- * and a value that is not a plain object is the not-an-object fault.
63
+ * every own string key, the copies of its `extensions` and `aliases` lists, and the snapshot of its
64
+ * default, inside one try. Every later check, the registry entry, and every sentence read the copy
65
+ * and never the author's object again, so a getter runs once and the caller's later changes reach
66
+ * nothing. The default is copied and frozen to `defaultLevels` levels, cycles included, and that
67
+ * copy is the value the graph publishes and a run validates; every other property is captured as
68
+ * declared, because core clones no library object. A read that throws, from a getter or a proxy
69
+ * trap, is the unreadable fault, named by the key it threw in, a default nested deeper is the
70
+ * too-deep fault, and a value that is not a plain object is the not-an-object fault.
67
71
  */
68
72
  export function captureConfig(config, faults) {
69
73
  const { notAnObject: notAnObjectFault, tooDeep, unreadable } = faults;
@@ -71,6 +75,7 @@ export function captureConfig(config, faults) {
71
75
  // Presence is the key, so a declared `default: undefined` stays a default.
72
76
  read.nested(copy, 'default', (value) => boundedSnapshot(value, defaultLevels));
73
77
  read.nested(copy, 'extensions', shallowList);
78
+ read.nested(copy, 'aliases', shallowList);
74
79
  }, {
75
80
  notAnObject: notAnObjectFault,
76
81
  unreadable: (thrown, slot) => thrown instanceof NestedTooDeepError ? tooDeep() : unreadable(thrown, slot),
@@ -125,11 +130,11 @@ export function declarationSubject(input) {
125
130
  : `Option ${quoted(input.name)}`;
126
131
  }
127
132
  /**
128
- * Where one input was declared: a global option at its `globalOption()` call, and every other input
129
- * at its own `argument()` or `option()` call on the Command at `path`.
133
+ * Where one Command's own input was declared: its `argument()` or `option()` call on the Command at
134
+ * `path`. A global option's site is the built table's, under `BuiltGlobals.sites`.
130
135
  */
131
- export function inputPlace(input, scope) {
132
- return scope.global ? { call: 'globalOption', path: [] } : { call: input.kind, path: scope.path };
136
+ export function inputPlace(input, path) {
137
+ return { call: input.kind, path };
133
138
  }
134
139
  /**
135
140
  * The site of the call that declared one input at `place`, rebuilt as `call(name, config)`. Every
@@ -149,23 +154,15 @@ export function configUnread(site) {
149
154
  return { ...site, declaration: { ...site.declaration, arguments: shown } };
150
155
  }
151
156
  /**
152
- * The token an operator would type for one declaration: `--file` for an option, `-F` when the
153
- * option declares `shortOnly`, and the declared name for an argument. Every input diagnostic and
157
+ * The token an operator would type for one declaration: an option's reported spelling, read from
158
+ * the routed Command's table, and the declared name for an argument. Every input diagnostic and
154
159
  * every reported problem names the declaration this way, so an omission and a rejected value read
155
160
  * alike and a `shortOnly` option is never named by a long form it does not accept.
156
161
  */
157
- function spellingOf(input) {
158
- if (input.kind === 'argument') {
159
- return input.name;
160
- }
161
- const { config } = input;
162
- if (config.shortOnly === true && config.short !== undefined) {
163
- return `-${config.short}`;
164
- }
165
- // A negative-only Boolean option accepts its negative form alone.
166
- return config.type === 'boolean' && config.polarity === 'negative'
167
- ? `--no-${input.name}`
168
- : `--${input.name}`;
162
+ function spellingOf(input, table) {
163
+ return input.kind === 'argument'
164
+ ? input.name
165
+ : reportedOf(spellingsOf(table, input.name), input.name);
169
166
  }
170
167
  /**
171
168
  * An input diagnostic names the declaration by kind and by the spelling that reaches it. A value an
@@ -249,7 +246,7 @@ function checkOmissionValidation(input, site, subject) {
249
246
  });
250
247
  }
251
248
  }
252
- /** The value rules a Boolean option may not declare, in the order its diagnostic names them. */
249
+ /** The value rules a Boolean or counted option may not declare, in the order a diagnostic names them. */
253
250
  const valueRules = ['validate', 'default', 'required', 'validateOmitted'];
254
251
  /** Whether a `validate` value is a Standard Schema v1 object that core can call. */
255
252
  function isStandardSchema(validator) {
@@ -263,17 +260,32 @@ function isStandardSchema(validator) {
263
260
  typeof Reflect.get(props, 'vendor') === 'string' &&
264
261
  typeof Reflect.get(props, 'validate') === 'function');
265
262
  }
263
+ /**
264
+ * The value rules a Boolean or counted option declares, which take a value it never consumes. Each
265
+ * kind names its own absent value in the correction.
266
+ */
267
+ function checkValueless(config, site, subject) {
268
+ const declared = valueRules.find((key) => key in config);
269
+ if (declared === undefined) {
270
+ return;
271
+ }
272
+ if (config.type === 'count') {
273
+ throw factFault(countOptionValueRule, site, {
274
+ correction: `Remove ${declared}; a counted option reads 0 when no occurrence supplies it.`,
275
+ fact: declared,
276
+ sentence: `${subject} is a counted option and declares ${declared}.`,
277
+ });
278
+ }
279
+ throw factFault(booleanOptionValueRule, site, {
280
+ correction: `Remove ${declared}; use polarity to control its absent value.`,
281
+ fact: declared,
282
+ sentence: `${subject} is Boolean and declares ${declared}.`,
283
+ });
284
+ }
266
285
  function checkDeclaration(input, site, subject) {
267
286
  const { config } = input;
268
- if (input.kind === 'option' && input.config.type === 'boolean') {
269
- const declared = valueRules.find((key) => key in config);
270
- if (declared !== undefined) {
271
- throw factFault(booleanOptionValueRule, site, {
272
- correction: `Remove ${declared}; use polarity to control its absent value.`,
273
- fact: declared,
274
- sentence: `${subject} is Boolean and declares ${declared}.`,
275
- });
276
- }
287
+ if (input.kind === 'option' && input.config.type !== 'string') {
288
+ checkValueless(input.config, site, subject);
277
289
  return;
278
290
  }
279
291
  if (config.required !== undefined && typeof config.required !== 'boolean') {
@@ -341,7 +353,7 @@ function readIssue(issue) {
341
353
  * belong to the value, so the caller names those.
342
354
  */
343
355
  async function validate(input, raw, call) {
344
- const { context, place } = call;
356
+ const { context, site } = call;
345
357
  const validator = input.config.validate;
346
358
  if (validator === undefined) {
347
359
  return { value: raw };
@@ -362,7 +374,6 @@ async function validate(input, raw, call) {
362
374
  }
363
375
  catch (error) {
364
376
  // The reason is the author's detail: a distributed build shows the generic defect message.
365
- const site = place === undefined ? undefined : declaringSite(input, place);
366
377
  throw new DeclarationError(validatorFailed, {
367
378
  correction: 'Fix the validator.',
368
379
  findings: site === undefined ? [] : [siteFinding(site, partOf(site, 'validate'))],
@@ -370,17 +381,34 @@ async function validate(input, raw, call) {
370
381
  }, { cause: error });
371
382
  }
372
383
  }
384
+ /**
385
+ * Where a bare spelling supplied one option's implied value, with that value's prepared output, or
386
+ * `undefined` when no bare spelling supplied it.
387
+ */
388
+ function impliedPositions(input, read) {
389
+ const positions = input.kind === 'option' ? read.options.implied.get(input.name) : undefined;
390
+ return positions === undefined
391
+ ? undefined
392
+ : { output: read.declaredValues.implied.get(input), positions };
393
+ }
373
394
  /**
374
395
  * One validation of a declared value. A multiple option or a variadic argument passes each of its
375
396
  * values through the validator in order, and each issue reads at its value's position before its
376
397
  * own path, so the action receives the array of outputs. Every other input passes its value once.
377
- * Each call reads a fresh context whose arrays are copies, so a write to them never reaches the
378
- * next call. The host is the one captured object that every call and the action share.
398
+ * A bare spelling's implied value never meets the validator here: its output was prepared before
399
+ * any token, so a scalar that a bare spelling supplied, and each bare occurrence in a multiple
400
+ * option's list, reuse a copy of that output in place. Each call reads a fresh context whose
401
+ * arrays are copies, so a write to them never reaches the next call. The host is the one captured
402
+ * object that every call and the action share.
379
403
  */
380
404
  async function validateDeclared(input, raw, call) {
381
- const { context, place, signal } = call;
405
+ const { context, implied, signal, site } = call;
406
+ if (!collects(input) && implied?.positions.includes(0) === true) {
407
+ // A bare spelling supplied the implied value, whose output was prepared before any token.
408
+ return { value: freshDeclaredValue(implied.output) };
409
+ }
382
410
  if (!collects(input) || input.config.validate === undefined) {
383
- return validate(input, raw, { context: context(), place });
411
+ return validate(input, raw, { context: context(), site });
384
412
  }
385
413
  if (!Array.isArray(raw)) {
386
414
  // The parser, the input sources, and the declaration rules only ever supply an array here.
@@ -393,7 +421,10 @@ async function validateDeclared(input, raw, call) {
393
421
  // A cancelled run starts no further call; the run resolves its cancellation code instead.
394
422
  break;
395
423
  }
396
- const result = await validate(input, value, { context: context(), place });
424
+ // A bare occurrence holds the implied value, whose output was prepared before any token.
425
+ const result = implied?.positions.includes(position) === true
426
+ ? { value: freshDeclaredValue(implied.output) }
427
+ : await validate(input, value, { context: context(), site });
397
428
  if (result.issues === undefined) {
398
429
  outputs.push(result.value);
399
430
  }
@@ -474,44 +505,81 @@ export function checkDeclarations(inputs, named) {
474
505
  }
475
506
  }
476
507
  /**
477
- * Every declared default, validated before any token is read. The host is captured by then, so a
478
- * default's validator reads the same Host its action will, under the `default` phase. `places`
479
- * says where each declaration sits, which a rejected default's finding rebuilds.
508
+ * Every declared default and implied value, validated before any token is read, in declaration
509
+ * order, a declaration's default before its implied value. The host is captured by then, so each
510
+ * validator reads the same Host its action will, under the `default` phase. `places` says where each
511
+ * declaration sits, which a rejected value's finding rebuilds. An implied value is judged whether or
512
+ * not an invocation holds a bare spelling, because the author declared it.
480
513
  */
481
- export async function prepareInputs(inputs, host, places) {
482
- const declarations = scoped(inputs);
514
+ export async function prepareDeclaredValues(inputs, host, places) {
483
515
  const defaults = new Map();
484
- for (const entry of declarations.filter(({ input }) => hasDefault(input))) {
516
+ const implied = new Map();
517
+ for (const entry of scoped(inputs)) {
485
518
  const { input } = entry;
486
- const subject = declarationSubject(input);
487
- const place = places.get(input);
488
- const result = await validateDeclared(input, input.config.default, {
519
+ const call = {
489
520
  context: () => ({ host, input: identityOf(entry), phase: 'default' }),
490
- place,
491
- });
492
- if (result.issues !== undefined) {
493
- const site = place === undefined ? undefined : declaringSite(input, place);
494
- throw new DeclarationError(invalidDefault, {
495
- correction: 'Fix the default or its validator.',
496
- findings: site === undefined ? [] : [siteFinding(site, partOf(site, 'default'))],
497
- sentence: [
498
- `${subject} has an invalid default.`,
499
- ...messages(subject, reported(result.issues)),
500
- ].join('\n'),
501
- });
521
+ site: places.get(input),
522
+ };
523
+ if (hasDefault(input)) {
524
+ const result = await validateDeclared(input, input.config.default, call);
525
+ defaults.set(input, declaredOutput(input, result, { fault: 'default', site: call.site }));
526
+ }
527
+ const value = impliedOf(input);
528
+ if (value !== undefined) {
529
+ // A bare spelling supplies one value, so even a multiple option's validator reads it alone.
530
+ const result = await validate(input, value, { context: call.context(), site: call.site });
531
+ implied.set(input, declaredOutput(input, result, { fault: 'implied', site: call.site }));
502
532
  }
503
- defaults.set(input, result.value);
504
533
  }
505
- return defaults;
534
+ return { defaults, implied };
535
+ }
536
+ /** How each declared value names itself when its validator rejects it, and the fix. */
537
+ const declaredFaults = {
538
+ default: {
539
+ correction: 'Fix the default or its validator.',
540
+ noun: 'default',
541
+ rule: invalidDefault,
542
+ },
543
+ implied: {
544
+ correction: 'Fix the implied value or its validator.',
545
+ noun: 'implied value',
546
+ rule: invalidImplied,
547
+ },
548
+ };
549
+ /**
550
+ * The output of a declared value its validator accepted, or the declaration fault it is when the
551
+ * validator rejected it, one line per issue under the sentence, marking the key that declares it.
552
+ */
553
+ function declaredOutput(input, result, declared) {
554
+ if (result.issues === undefined) {
555
+ return result.value;
556
+ }
557
+ const { fault, site } = declared;
558
+ const { correction, noun, rule } = declaredFaults[fault];
559
+ const subject = declarationSubject(input);
560
+ throw new DeclarationError(rule, {
561
+ correction,
562
+ findings: site === undefined ? [] : [siteFinding(site, partOf(site, fault))],
563
+ sentence: [
564
+ `${subject} has an invalid ${noun}.`,
565
+ ...messages(subject, reported(result.issues)),
566
+ ].join('\n'),
567
+ });
568
+ }
569
+ /** The implied value a string option declares, which a bare spelling supplies, or `undefined`. */
570
+ function impliedOf(input) {
571
+ return input.kind === 'option' && input.config.type === 'string'
572
+ ? input.config.implied
573
+ : undefined;
506
574
  }
507
575
  /**
508
- * An array default reaches the action as its own mutable copy, so an action that mutates its array
509
- * rewrites neither the declaration nor the next invocation. One prepared default serves every
510
- * invocation of a run, and an unvalidated default is the frozen snapshot the declaring call took,
511
- * so each read copies it. The copy keeps a hole where the default has one, as the graph's does.
512
- * Every other output passes through unchanged.
576
+ * An array default or implied output reaches the action as its own mutable copy, so an action that
577
+ * mutates its array rewrites neither the declaration nor the next invocation. One prepared declared
578
+ * value serves every invocation of a run, and an unvalidated default is the frozen snapshot the
579
+ * declaring call took, so each read copies it. The copy keeps a hole where the value has one, as
580
+ * the graph's does. Every other output passes through unchanged.
513
581
  */
514
- function freshDefault(value) {
582
+ function freshDeclaredValue(value) {
515
583
  // A spread reads a hole as `undefined`, and `slice` keeps it.
516
584
  // oxlint-disable-next-line unicorn/prefer-spread
517
585
  return Array.isArray(value) ? value.slice() : value;
@@ -532,6 +600,9 @@ function suppliedInputs(declarations, supplied) {
532
600
  else if (input.config.type === 'boolean') {
533
601
  options[input.name] = supplied.options.booleans.get(input.name);
534
602
  }
603
+ else if (input.config.type === 'count') {
604
+ options[input.name] = supplied.options.counts.get(input.name);
605
+ }
535
606
  else {
536
607
  options[input.name] =
537
608
  copied(suppliedOption(supplied.options, input.name, collected)) ??
@@ -540,22 +611,29 @@ function suppliedInputs(declarations, supplied) {
540
611
  }
541
612
  return { args, options };
542
613
  }
543
- /** The Boolean grammar's one issue, which a variable outside it reports. */
544
- const grammarIssues = [{ message: 'Use true, false, 1, or 0.' }];
545
614
  /**
546
- * Every declaration in the order this phase reports its problems: the globals, then each plugin
547
- * option whose variable is outside the grammar, then the routed Command's own declarations.
615
+ * The one issue a variable outside its option's grammar reports: the Boolean grammar, or the count
616
+ * grammar of ASCII decimal digits.
548
617
  */
549
- function reportingOrder(invocation) {
550
- const declarations = scoped(invocation.inputs);
551
- const globals = declarations.filter((entry) => entry.global);
552
- const rejected = invocation.plugins
553
- .filter((input) => invocation.sources.rejected.has(input.name))
554
- .map((input) => ({ global: true, input }));
555
- return [...globals, ...rejected, ...declarations.filter((entry) => !entry.global)];
618
+ function grammarIssues(input) {
619
+ return input.config.type === 'count'
620
+ ? [{ message: 'Use a whole number of 0 or more.' }]
621
+ : [{ message: 'Use true, false, 1, or 0.' }];
622
+ }
623
+ /**
624
+ * Whether a pass rejected a global option, which leaves no global value a middleware may read. A
625
+ * global option declares no presence rule, so every problem it reports is a rejected value.
626
+ */
627
+ export function rejectsGlobal(validation) {
628
+ return validation.failure?.problems.some((problem) => problem.input.global) ?? false;
556
629
  }
630
+ /**
631
+ * Validates one invocation in reporting order: the global options, the application's and then each
632
+ * plugin's, and then the routed Command's own declarations, each in authoring order.
633
+ */
557
634
  export async function validateValues(invocation) {
558
- const { defaults, sources, supplied } = invocation;
635
+ const { declaredValues, prior, sources, supplied } = invocation;
636
+ const { defaults } = declaredValues;
559
637
  const declarations = scoped(invocation.inputs);
560
638
  /** Where a filled option's value came from, which its diagnostic names; argv names none. */
561
639
  const originOf = (input) => input.kind === 'option' ? sources.labels.get(input.name) : undefined;
@@ -588,24 +666,40 @@ export async function validateValues(invocation) {
588
666
  };
589
667
  };
590
668
  const values = new Map();
591
- const lines = [];
592
- const problems = [];
669
+ // Each input reports at most once, so insertion order is the order this phase reaches them.
670
+ const reports = new Map();
671
+ /**
672
+ * One missing input, in the order this phase reaches it, unless a skipped configuration source
673
+ * would have been asked to fill it.
674
+ */
675
+ const omit = (entry, spelling, line) => {
676
+ if (sources.unanswered?.has(entry.input) === true) {
677
+ return;
678
+ }
679
+ reports.set(entry.input, {
680
+ lines: [line],
681
+ problem: { input: identityOf(entry), reason: 'missing', spelling },
682
+ });
683
+ };
593
684
  /** One rejected input, whatever rejected it, in the order this phase reaches it. */
594
685
  const reject = (entry, issues, subject) => {
595
- problems.push({
596
- input: identityOf(entry),
597
- issues,
598
- reason: 'invalid',
599
- spelling: spellingOf(entry.input),
686
+ reports.set(entry.input, {
687
+ lines: messages(subject, issues),
688
+ problem: {
689
+ input: identityOf(entry),
690
+ issues,
691
+ reason: 'invalid',
692
+ spelling: spellingOf(entry.input, invocation.table),
693
+ },
600
694
  });
601
- lines.push(...messages(subject, issues));
602
695
  };
603
696
  /** One path for every value a validator reads, so a raw shape and its issues meet it once. */
604
697
  const accept = async (entry, raw, spelling) => {
605
698
  const result = await validateDeclared(entry.input, raw, {
606
699
  context: () => contextOf(entry),
607
- place: inputPlace(entry.input, { global: entry.global, path: invocation.command }),
700
+ implied: impliedPositions(entry.input, { declaredValues, options: supplied.options }),
608
701
  signal: invocation.signal,
702
+ site: invocation.places.get(entry.input),
609
703
  });
610
704
  if (result.issues === undefined) {
611
705
  values.set(entry.input, result.value);
@@ -613,7 +707,9 @@ export async function validateValues(invocation) {
613
707
  }
614
708
  reject(entry, reported(result.issues), suppliedName(entry.input, spelling, originOf(entry.input)));
615
709
  };
616
- for (const entry of reportingOrder(invocation)) {
710
+ const { only } = invocation;
711
+ const validated = only ? declarations.filter(({ input }) => only.has(input)) : declarations;
712
+ for (const entry of validated) {
617
713
  if (invocation.signal.aborted) {
618
714
  /**
619
715
  * A cancelled run starts no further validator call. The one already in flight was awaited
@@ -624,16 +720,29 @@ export async function validateValues(invocation) {
624
720
  }
625
721
  const { input } = entry;
626
722
  const variable = sources.rejected.get(input.name);
627
- if (input.kind === 'option' && variable !== undefined) {
628
- // A Boolean variable outside the grammar filled nothing, so it is the option's problem.
629
- reject(entry, grammarIssues, suppliedName(input, spellingOf(input), variable));
723
+ const earlier = prior?.reports.get(input);
724
+ if (prior?.values.has(input) === true) {
725
+ // An earlier pass validated this value once, and one value meets its validator once.
726
+ values.set(input, prior.values.read(input));
727
+ }
728
+ else if (earlier !== undefined) {
729
+ // The earlier pass's problem reports here, in this pass's order.
730
+ reports.set(input, earlier);
731
+ }
732
+ else if (input.kind === 'option' && variable !== undefined) {
733
+ // A variable outside its option's grammar filled nothing, so it is the option's problem.
734
+ reject(entry, grammarIssues(input), suppliedName(input, spellingOf(input, invocation.table), variable));
630
735
  }
631
736
  else if (input.kind === 'option' && input.config.type === 'boolean') {
632
737
  values.set(input, booleanValue(supplied.options, input.name, input.config));
633
738
  }
739
+ else if (input.kind === 'option' && input.config.type === 'count') {
740
+ // A count passes no validator, and no occurrence and no source reads 0.
741
+ values.set(input, supplied.options.counts.get(input.name) ?? 0);
742
+ }
634
743
  else {
635
744
  const collected = collects(input);
636
- const spelling = spellingOf(input);
745
+ const spelling = spellingOf(input, invocation.table);
637
746
  const raw = input.kind === 'argument'
638
747
  ? supplied.args.get(input)
639
748
  : suppliedOption(supplied.options, input.name, collected);
@@ -641,34 +750,37 @@ export async function validateValues(invocation) {
641
750
  if (input.config.required) {
642
751
  // An omitted required argument arrives here too, so omission has one class.
643
752
  // One aggregated diagnostic covers an omitted argument and an omitted option alike.
644
- problems.push({ input: identityOf(entry), reason: 'missing', spelling });
645
- lines.push(missingMessage(input, spelling, { collected }));
753
+ omit(entry, spelling, missingMessage(input, spelling, { collected }));
646
754
  }
647
755
  else if (collected && !defaults.has(input)) {
648
756
  // No occurrence has no value to validate, so the action receives an empty array.
649
757
  values.set(input, []);
650
758
  }
651
- else if (validatesOmission(input)) {
759
+ else if (validatesOmission(input) && sources.unanswered?.has(input) !== true) {
652
760
  // The flag sends the omission itself to the validator.
761
+ // An option a skipped configuration source would have filled is not judged absent.
653
762
  // An absence rule reads the context a supplied value reads, and reports input issues.
654
763
  await accept(entry, undefined, spelling);
655
764
  }
656
765
  else {
657
- values.set(input, freshDefault(defaults.get(input)));
766
+ values.set(input, freshDeclaredValue(defaults.get(input)));
658
767
  }
659
768
  }
660
769
  else if (input.config.required && Array.isArray(raw) && raw.length === 0) {
661
770
  // A filled list satisfies the at-least-one rule by its length, so an empty one is missing.
662
- problems.push({ input: identityOf(entry), reason: 'missing', spelling });
663
- lines.push(missingMessage(input, spelling, { collected, origin: originOf(input) }));
771
+ omit(entry, spelling, missingMessage(input, spelling, { collected, origin: originOf(input) }));
664
772
  }
665
773
  else {
666
774
  await accept(entry, raw, spelling);
667
775
  }
668
776
  }
669
777
  }
670
- if (problems.length > 0) {
671
- throw new InputError(lines.join('\n'), problems);
672
- }
673
- return new ValidatedInputs(values);
778
+ const found = [...reports.values()];
779
+ return {
780
+ failure: found.length > 0
781
+ ? new InputError(found.flatMap((report) => report.lines).join('\n'), found.map((report) => report.problem))
782
+ : undefined,
783
+ reports,
784
+ values: new ValidatedInputs(values),
785
+ };
674
786
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@loomcli/core",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "description": "The Loom CLI core package. Provides the scaffolding for creating new Loom CLI applications.",
5
5
  "license": "MIT AND Unicode-3.0",
6
6
  "repository": {