@loomcli/core 0.6.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.
Files changed (52) hide show
  1. package/NOTICE +34 -0
  2. package/dist/application.d.ts +11 -6
  3. package/dist/application.js +57 -19
  4. package/dist/capture.d.ts +65 -0
  5. package/dist/capture.js +99 -0
  6. package/dist/chain.d.ts +28 -17
  7. package/dist/chain.js +11 -10
  8. package/dist/command-rules.d.ts +1 -1
  9. package/dist/command-rules.js +2 -2
  10. package/dist/command.d.ts +31 -47
  11. package/dist/command.js +204 -209
  12. package/dist/errors.d.ts +22 -21
  13. package/dist/errors.js +38 -18
  14. package/dist/facts.d.ts +5 -0
  15. package/dist/facts.js +8 -1
  16. package/dist/globals.d.ts +48 -22
  17. package/dist/globals.js +72 -29
  18. package/dist/glyphs.generated.js +1 -1
  19. package/dist/index.d.ts +5 -3
  20. package/dist/index.js +3 -1
  21. package/dist/input-rules.d.ts +20 -2
  22. package/dist/input-rules.js +47 -5
  23. package/dist/inspect.d.ts +33 -17
  24. package/dist/inspect.js +74 -87
  25. package/dist/locate.js +52 -60
  26. package/dist/options.d.ts +101 -83
  27. package/dist/options.js +311 -267
  28. package/dist/parse.d.ts +162 -0
  29. package/dist/parse.js +601 -0
  30. package/dist/plain.d.ts +52 -2
  31. package/dist/plain.js +228 -2
  32. package/dist/plugin-rules.d.ts +8 -4
  33. package/dist/plugin-rules.js +13 -9
  34. package/dist/plugin-settings.d.ts +18 -0
  35. package/dist/plugin-settings.js +38 -0
  36. package/dist/plugin.d.ts +32 -27
  37. package/dist/plugin.js +107 -89
  38. package/dist/sources.d.ts +18 -9
  39. package/dist/sources.js +50 -20
  40. package/dist/style-layout.js +2 -2
  41. package/dist/style-width.d.ts +13 -0
  42. package/dist/style-width.js +170 -0
  43. package/dist/types.d.ts +70 -30
  44. package/dist/unicode.generated.d.ts +27 -0
  45. package/dist/unicode.generated.js +1036 -0
  46. package/dist/validation.d.ts +108 -34
  47. package/dist/validation.js +282 -125
  48. package/dist/view.d.ts +16 -11
  49. package/dist/view.js +13 -4
  50. package/licenses/unicode-LICENSE.txt +41 -0
  51. package/licenses/uucode-LICENSE.md +35 -0
  52. package/package.json +9 -5
@@ -1,10 +1,12 @@
1
+ import { captureDeclaration, elidedRead, unreadableFault } from './capture.js';
1
2
  import { schemaOptions } from './context.js';
2
3
  import { escapeControlCharacters } from './controls.js';
4
+ import { elided, spelled } from './diagnostic-text.js';
3
5
  import { asSentence, DeclarationError, InputError, quoted, reasonOf } from './errors.js';
4
6
  import { callSite, factFault, flagFault, partOf, siteFinding } from './facts.js';
5
- import { booleanOptionValueRule, defaultShape, invalidDefault, notAValidator, omissionAlreadyDecided, omissionWithoutValidator, requiredWithDefault, } from './input-rules.js';
6
- import { booleanValue } from './options.js';
7
- import { isPlainObject } from './plain.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
+ import { boundedSnapshot, NestedTooDeepError, shallowList } from './plain.js';
8
10
  import { notAnObject } from './plugin-rules.js';
9
11
  import { validatorFailed } from './rules.js';
10
12
  /** Every declaration in validation order: the globals first, then the reading Command's own. */
@@ -42,6 +44,10 @@ class ValidatedInputs {
42
44
  read(input) {
43
45
  return this.#values.get(input);
44
46
  }
47
+ /** Whether this pass validated the declaration's value. */
48
+ has(input) {
49
+ return this.#values.has(input);
50
+ }
45
51
  #field(input) {
46
52
  // Last resort: no typed path exists. The map stores every validated value as `unknown`.
47
53
  // An object literal with a generic computed key does not type as `Record<Name, _>` either.
@@ -53,34 +59,66 @@ class ValidatedInputs {
53
59
  }
54
60
  }
55
61
  /**
56
- * The one check every `argument()`, `option()`, and `globalOption()` call makes before it reads its
57
- * config, so a call in JavaScript that supplies none, or a value of another kind, reports a
58
- * declaration fault and not a TypeError. `place` is where the call sits.
62
+ * Authoring's one read of a config, through `captureDeclaration`: the prototype verdict, the copy of
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.
59
71
  */
60
- export function checkInputConfig(declared, place) {
61
- const { config, kind, name } = declared;
62
- if (isPlainObject(config)) {
63
- return;
64
- }
65
- const subject = kind === 'argument' ? 'Argument' : 'Option';
66
- const site = callSite(`${subject} ${quoted(name)}`, {
67
- arguments: [name, config],
68
- ...place,
72
+ export function captureConfig(config, faults) {
73
+ const { notAnObject: notAnObjectFault, tooDeep, unreadable } = faults;
74
+ return captureDeclaration(config, (copy, read) => {
75
+ // Presence is the key, so a declared `default: undefined` stays a default.
76
+ read.nested(copy, 'default', (value) => boundedSnapshot(value, defaultLevels));
77
+ read.nested(copy, 'extensions', shallowList);
78
+ read.nested(copy, 'aliases', shallowList);
79
+ }, {
80
+ notAnObject: notAnObjectFault,
81
+ unreadable: (thrown, slot) => thrown instanceof NestedTooDeepError ? tooDeep() : unreadable(thrown, slot),
69
82
  });
70
- throw new DeclarationError(notAnObject, {
71
- correction: `Supply ${kind === 'argument' ? 'an argument' : 'an option'} config object, such as ${kind === 'argument' ? '{}' : "{ type: 'string' }"}.`,
72
- findings: [siteFinding(site, '1')],
73
- sentence: `${site.subject} declares a config that is not an object.`,
83
+ }
84
+ /** The fault of a default nested deeper than `defaultLevels`, declared by `subject`. */
85
+ export function defaultDepthFault(subject, findings) {
86
+ return new DeclarationError(defaultDepth, {
87
+ correction: `Nest a default at most ${String(defaultLevels)} levels deep.`,
88
+ findings,
89
+ sentence: `${subject} default nests deeper than ${String(defaultLevels)} levels.`,
74
90
  });
75
91
  }
76
92
  /**
77
- * Authoring's snapshot of one config. An array default is the one declared value core hands to an
78
- * action as its own value, so the declaration keeps a copy and the caller keeps its array. Every
79
- * other property is captured as declared, because core clones no library object.
93
+ * The config every `argument()`, `option()`, and `globalOption()` call, and every input a lifecycle
94
+ * hook declares, reads through `captureConfig`. A call in JavaScript that supplies none, or a value
95
+ * of another kind, reports a declaration fault and not a TypeError, and so does a config whose read
96
+ * throws. Each fault marks the config on the call at `place`.
80
97
  */
81
- export function captureConfig(config) {
82
- const value = config.default;
83
- return Array.isArray(value) ? { ...config, default: [...value] } : { ...config };
98
+ export function captureInputConfig(declared, place) {
99
+ const { config, kind, name } = declared;
100
+ const subject = `${kind === 'argument' ? 'Argument' : 'Option'} ${quoted(name)}`;
101
+ // The finding marks the config, or one key inside it, which `shown` stands for when the call prints.
102
+ const findings = (shown, keys = []) => [
103
+ siteFinding(callSite(subject, { arguments: [name, shown], ...place }), ['1', ...keys].join('.')),
104
+ ];
105
+ return captureConfig(config, {
106
+ notAnObject: () => new DeclarationError(notAnObject, {
107
+ correction: `Supply ${kind === 'argument' ? 'an argument' : 'an option'} config object, such as ${kind === 'argument' ? '{}' : "{ type: 'string' }"}.`,
108
+ findings: findings(config),
109
+ sentence: `${subject} declares a config that is not an object.`,
110
+ }),
111
+ // The default's walk stopped at the limit, so the finding prints it elided.
112
+ tooDeep: () => {
113
+ const { keys, shown } = elidedRead('default');
114
+ return defaultDepthFault(subject, findings(shown, keys));
115
+ },
116
+ // A part whose read threw is never read again, so the finding prints it elided.
117
+ unreadable: (thrown, slot) => {
118
+ const { keys, shown } = elidedRead(slot);
119
+ return unreadableFault({ declared: 'config', findings: findings(shown, keys), subject }, thrown);
120
+ },
121
+ });
84
122
  }
85
123
  /**
86
124
  * A declaration error names the declaration, because the author reads the declaration to fix it.
@@ -92,11 +130,11 @@ export function declarationSubject(input) {
92
130
  : `Option ${quoted(input.name)}`;
93
131
  }
94
132
  /**
95
- * Where one input was declared: a global option at its `globalOption()` call, and every other input
96
- * 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`.
97
135
  */
98
- export function inputPlace(input, scope) {
99
- return scope.global ? { call: 'globalOption', path: [] } : { call: input.kind, path: scope.path };
136
+ export function inputPlace(input, path) {
137
+ return { call: input.kind, path };
100
138
  }
101
139
  /**
102
140
  * The site of the call that declared one input at `place`, rebuilt as `call(name, config)`. Every
@@ -107,23 +145,24 @@ export function declaringSite(input, place, subject = declarationSubject(input))
107
145
  return callSite(subject, { arguments: [input.name, input.config], ...place });
108
146
  }
109
147
  /**
110
- * The token an operator would type for one declaration: `--file` for an option, `-F` when the
111
- * option declares `shortOnly`, and the declared name for an argument. Every input diagnostic and
148
+ * One input's site with its config printed elided, for a fault judged before the config is read,
149
+ * such as an invalid name, so the fault reads none of the config.
150
+ */
151
+ export function configUnread(site) {
152
+ const [name, config] = site.declaration.arguments;
153
+ const shown = config === undefined ? [name] : [name, spelled(elided)];
154
+ return { ...site, declaration: { ...site.declaration, arguments: shown } };
155
+ }
156
+ /**
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
112
159
  * every reported problem names the declaration this way, so an omission and a rejected value read
113
160
  * alike and a `shortOnly` option is never named by a long form it does not accept.
114
161
  */
115
- function spellingOf(input) {
116
- if (input.kind === 'argument') {
117
- return input.name;
118
- }
119
- const { config } = input;
120
- if (config.shortOnly === true && config.short !== undefined) {
121
- return `-${config.short}`;
122
- }
123
- // A negative-only Boolean option accepts its negative form alone.
124
- return config.type === 'boolean' && config.polarity === 'negative'
125
- ? `--no-${input.name}`
126
- : `--${input.name}`;
162
+ function spellingOf(input, table) {
163
+ return input.kind === 'argument'
164
+ ? input.name
165
+ : reportedOf(spellingsOf(table, input.name), input.name);
127
166
  }
128
167
  /**
129
168
  * An input diagnostic names the declaration by kind and by the spelling that reaches it. A value an
@@ -207,7 +246,7 @@ function checkOmissionValidation(input, site, subject) {
207
246
  });
208
247
  }
209
248
  }
210
- /** 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. */
211
250
  const valueRules = ['validate', 'default', 'required', 'validateOmitted'];
212
251
  /** Whether a `validate` value is a Standard Schema v1 object that core can call. */
213
252
  function isStandardSchema(validator) {
@@ -221,17 +260,32 @@ function isStandardSchema(validator) {
221
260
  typeof Reflect.get(props, 'vendor') === 'string' &&
222
261
  typeof Reflect.get(props, 'validate') === 'function');
223
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
+ }
224
285
  function checkDeclaration(input, site, subject) {
225
286
  const { config } = input;
226
- if (input.kind === 'option' && input.config.type === 'boolean') {
227
- const declared = valueRules.find((key) => key in config);
228
- if (declared !== undefined) {
229
- throw factFault(booleanOptionValueRule, site, {
230
- correction: `Remove ${declared}; use polarity to control its absent value.`,
231
- fact: declared,
232
- sentence: `${subject} is Boolean and declares ${declared}.`,
233
- });
234
- }
287
+ if (input.kind === 'option' && input.config.type !== 'string') {
288
+ checkValueless(input.config, site, subject);
235
289
  return;
236
290
  }
237
291
  if (config.required !== undefined && typeof config.required !== 'boolean') {
@@ -299,7 +353,7 @@ function readIssue(issue) {
299
353
  * belong to the value, so the caller names those.
300
354
  */
301
355
  async function validate(input, raw, call) {
302
- const { context, place } = call;
356
+ const { context, site } = call;
303
357
  const validator = input.config.validate;
304
358
  if (validator === undefined) {
305
359
  return { value: raw };
@@ -320,7 +374,6 @@ async function validate(input, raw, call) {
320
374
  }
321
375
  catch (error) {
322
376
  // The reason is the author's detail: a distributed build shows the generic defect message.
323
- const site = place === undefined ? undefined : declaringSite(input, place);
324
377
  throw new DeclarationError(validatorFailed, {
325
378
  correction: 'Fix the validator.',
326
379
  findings: site === undefined ? [] : [siteFinding(site, partOf(site, 'validate'))],
@@ -328,17 +381,34 @@ async function validate(input, raw, call) {
328
381
  }, { cause: error });
329
382
  }
330
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
+ }
331
394
  /**
332
395
  * One validation of a declared value. A multiple option or a variadic argument passes each of its
333
396
  * values through the validator in order, and each issue reads at its value's position before its
334
397
  * own path, so the action receives the array of outputs. Every other input passes its value once.
335
- * Each call reads a fresh context whose arrays are copies, so a write to them never reaches the
336
- * 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.
337
403
  */
338
404
  async function validateDeclared(input, raw, call) {
339
- 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
+ }
340
410
  if (!collects(input) || input.config.validate === undefined) {
341
- return validate(input, raw, { context: context(), place });
411
+ return validate(input, raw, { context: context(), site });
342
412
  }
343
413
  if (!Array.isArray(raw)) {
344
414
  // The parser, the input sources, and the declaration rules only ever supply an array here.
@@ -351,7 +421,10 @@ async function validateDeclared(input, raw, call) {
351
421
  // A cancelled run starts no further call; the run resolves its cancellation code instead.
352
422
  break;
353
423
  }
354
- 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 });
355
428
  if (result.issues === undefined) {
356
429
  outputs.push(result.value);
357
430
  }
@@ -432,44 +505,84 @@ export function checkDeclarations(inputs, named) {
432
505
  }
433
506
  }
434
507
  /**
435
- * Every declared default, validated before any token is read. The host is captured by then, so a
436
- * default's validator reads the same Host its action will, under the `default` phase. `places`
437
- * 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.
438
513
  */
439
- export async function prepareInputs(inputs, host, places) {
440
- const declarations = scoped(inputs);
514
+ export async function prepareDeclaredValues(inputs, host, places) {
441
515
  const defaults = new Map();
442
- for (const entry of declarations.filter(({ input }) => hasDefault(input))) {
516
+ const implied = new Map();
517
+ for (const entry of scoped(inputs)) {
443
518
  const { input } = entry;
444
- const subject = declarationSubject(input);
445
- const place = places.get(input);
446
- const result = await validateDeclared(input, input.config.default, {
519
+ const call = {
447
520
  context: () => ({ host, input: identityOf(entry), phase: 'default' }),
448
- place,
449
- });
450
- if (result.issues !== undefined) {
451
- const site = place === undefined ? undefined : declaringSite(input, place);
452
- throw new DeclarationError(invalidDefault, {
453
- correction: 'Fix the default or its validator.',
454
- findings: site === undefined ? [] : [siteFinding(site, partOf(site, 'default'))],
455
- sentence: [
456
- `${subject} has an invalid default.`,
457
- ...messages(subject, reported(result.issues)),
458
- ].join('\n'),
459
- });
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 }));
460
532
  }
461
- defaults.set(input, result.value);
462
533
  }
463
- 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;
464
574
  }
465
575
  /**
466
- * An array default reaches the action as its own copy, so an action that mutates its array
467
- * rewrites neither the declaration nor the next invocation. One prepared default serves every
468
- * invocation of a run, and an unvalidated default is the declared array itself, so each read
469
- * copies it. 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.
470
581
  */
471
- function freshDefault(value) {
472
- return Array.isArray(value) ? [...value] : value;
582
+ function freshDeclaredValue(value) {
583
+ // A spread reads a hole as `undefined`, and `slice` keeps it.
584
+ // oxlint-disable-next-line unicorn/prefer-spread
585
+ return Array.isArray(value) ? value.slice() : value;
473
586
  }
474
587
  /**
475
588
  * The raw tokens of one invocation, keyed by declared name. Every declared input of the routed
@@ -487,6 +600,9 @@ function suppliedInputs(declarations, supplied) {
487
600
  else if (input.config.type === 'boolean') {
488
601
  options[input.name] = supplied.options.booleans.get(input.name);
489
602
  }
603
+ else if (input.config.type === 'count') {
604
+ options[input.name] = supplied.options.counts.get(input.name);
605
+ }
490
606
  else {
491
607
  options[input.name] =
492
608
  copied(suppliedOption(supplied.options, input.name, collected)) ??
@@ -495,22 +611,29 @@ function suppliedInputs(declarations, supplied) {
495
611
  }
496
612
  return { args, options };
497
613
  }
498
- /** The Boolean grammar's one issue, which a variable outside it reports. */
499
- const grammarIssues = [{ message: 'Use true, false, 1, or 0.' }];
500
614
  /**
501
- * Every declaration in the order this phase reports its problems: the globals, then each plugin
502
- * 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.
503
617
  */
504
- function reportingOrder(invocation) {
505
- const declarations = scoped(invocation.inputs);
506
- const globals = declarations.filter((entry) => entry.global);
507
- const rejected = invocation.plugins
508
- .filter((input) => invocation.sources.rejected.has(input.name))
509
- .map((input) => ({ global: true, input }));
510
- 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.' }];
511
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;
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
+ */
512
634
  export async function validateValues(invocation) {
513
- const { defaults, sources, supplied } = invocation;
635
+ const { declaredValues, prior, sources, supplied } = invocation;
636
+ const { defaults } = declaredValues;
514
637
  const declarations = scoped(invocation.inputs);
515
638
  /** Where a filled option's value came from, which its diagnostic names; argv names none. */
516
639
  const originOf = (input) => input.kind === 'option' ? sources.labels.get(input.name) : undefined;
@@ -543,24 +666,40 @@ export async function validateValues(invocation) {
543
666
  };
544
667
  };
545
668
  const values = new Map();
546
- const lines = [];
547
- 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
+ };
548
684
  /** One rejected input, whatever rejected it, in the order this phase reaches it. */
549
685
  const reject = (entry, issues, subject) => {
550
- problems.push({
551
- input: identityOf(entry),
552
- issues,
553
- reason: 'invalid',
554
- 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
+ },
555
694
  });
556
- lines.push(...messages(subject, issues));
557
695
  };
558
696
  /** One path for every value a validator reads, so a raw shape and its issues meet it once. */
559
697
  const accept = async (entry, raw, spelling) => {
560
698
  const result = await validateDeclared(entry.input, raw, {
561
699
  context: () => contextOf(entry),
562
- place: inputPlace(entry.input, { global: entry.global, path: invocation.command }),
700
+ implied: impliedPositions(entry.input, { declaredValues, options: supplied.options }),
563
701
  signal: invocation.signal,
702
+ site: invocation.places.get(entry.input),
564
703
  });
565
704
  if (result.issues === undefined) {
566
705
  values.set(entry.input, result.value);
@@ -568,7 +707,9 @@ export async function validateValues(invocation) {
568
707
  }
569
708
  reject(entry, reported(result.issues), suppliedName(entry.input, spelling, originOf(entry.input)));
570
709
  };
571
- 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) {
572
713
  if (invocation.signal.aborted) {
573
714
  /**
574
715
  * A cancelled run starts no further validator call. The one already in flight was awaited
@@ -579,16 +720,29 @@ export async function validateValues(invocation) {
579
720
  }
580
721
  const { input } = entry;
581
722
  const variable = sources.rejected.get(input.name);
582
- if (input.kind === 'option' && variable !== undefined) {
583
- // A Boolean variable outside the grammar filled nothing, so it is the option's problem.
584
- 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));
585
735
  }
586
736
  else if (input.kind === 'option' && input.config.type === 'boolean') {
587
737
  values.set(input, booleanValue(supplied.options, input.name, input.config));
588
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
+ }
589
743
  else {
590
744
  const collected = collects(input);
591
- const spelling = spellingOf(input);
745
+ const spelling = spellingOf(input, invocation.table);
592
746
  const raw = input.kind === 'argument'
593
747
  ? supplied.args.get(input)
594
748
  : suppliedOption(supplied.options, input.name, collected);
@@ -596,34 +750,37 @@ export async function validateValues(invocation) {
596
750
  if (input.config.required) {
597
751
  // An omitted required argument arrives here too, so omission has one class.
598
752
  // One aggregated diagnostic covers an omitted argument and an omitted option alike.
599
- problems.push({ input: identityOf(entry), reason: 'missing', spelling });
600
- lines.push(missingMessage(input, spelling, { collected }));
753
+ omit(entry, spelling, missingMessage(input, spelling, { collected }));
601
754
  }
602
755
  else if (collected && !defaults.has(input)) {
603
756
  // No occurrence has no value to validate, so the action receives an empty array.
604
757
  values.set(input, []);
605
758
  }
606
- else if (validatesOmission(input)) {
759
+ else if (validatesOmission(input) && sources.unanswered?.has(input) !== true) {
607
760
  // The flag sends the omission itself to the validator.
761
+ // An option a skipped configuration source would have filled is not judged absent.
608
762
  // An absence rule reads the context a supplied value reads, and reports input issues.
609
763
  await accept(entry, undefined, spelling);
610
764
  }
611
765
  else {
612
- values.set(input, freshDefault(defaults.get(input)));
766
+ values.set(input, freshDeclaredValue(defaults.get(input)));
613
767
  }
614
768
  }
615
769
  else if (input.config.required && Array.isArray(raw) && raw.length === 0) {
616
770
  // A filled list satisfies the at-least-one rule by its length, so an empty one is missing.
617
- problems.push({ input: identityOf(entry), reason: 'missing', spelling });
618
- lines.push(missingMessage(input, spelling, { collected, origin: originOf(input) }));
771
+ omit(entry, spelling, missingMessage(input, spelling, { collected, origin: originOf(input) }));
619
772
  }
620
773
  else {
621
774
  await accept(entry, raw, spelling);
622
775
  }
623
776
  }
624
777
  }
625
- if (problems.length > 0) {
626
- throw new InputError(lines.join('\n'), problems);
627
- }
628
- 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
+ };
629
786
  }