@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.
package/dist/command.js CHANGED
@@ -3,17 +3,18 @@ import { checkEnvBinding, checkNoArgumentBinding } from './bindings.js';
3
3
  import { captureDeclaration, unreadableArgument } from './capture.js';
4
4
  import { aliasWithoutNames, argumentDeclaredTwice, argumentsBesideChildren, commandAttachedTwice, commandGlobals, commandWithoutAction, declaredAfterAction, declaredName, groupOption, multipleActions, multipleResults, nestingDepth, notACommand, optionalArgumentLast, portableName, repeatedAlias, resultWithoutAction, resultWithoutViews, rowViewOnValue, siblingNameTaken, unknownDefaultView, variadicArgumentLast, viewName, viewShape, viewsWithoutResult, } from './command-rules.js';
5
5
  import { elided, quoteString, spelled } from './diagnostic-text.js';
6
- import { asSentence, commandSentence, commandSubject, DeclarationError, NonCallableCommandError, quoted, ResultError, toFailure, UnexpectedArgumentError, UnknownCommandError, reasonOf, } from './errors.js';
6
+ import { asSentence, commandSentence, commandSubject, DeclarationError, NonCallableCommandError, quoted, ResultError, toFailure, reasonOf, } from './errors.js';
7
7
  import { buildExtensions, extendStore, publishStore, registerDescriptor, storeCommandLayers, validateLayer, } from './extension.js';
8
8
  import { checkDeprecated, checkDescription, checkHidden, checkNoListingFacts, declarerNote, siteFinding, } from './facts.js';
9
- import { buildGlobals, checkLocalOptions } from './globals.js';
9
+ import { buildGlobals, checkLocalOptions, frozenValues } from './globals.js';
10
10
  import { nameSharedAcrossKinds, optionDeclaredTwice, spellingTaken } from './input-rules.js';
11
11
  import { graphMismatch, nodeAt, resultNode } from './inspect.js';
12
- import { checkOptionName, compileOptions, copyValues, emptyValues, extractGlobals, isOptionToken, mergeValues, parseInputs, spellingMark, } from './options.js';
12
+ import { checkOptionName, compileOptions, copyValues, declaredNameCorrection, emptyValues, isDeclaredName, mergeValues, repeatedAliasText, spellingMark, tableEntries, } from './options.js';
13
+ import { argumentSlot, candidatesOf } from './parse.js';
13
14
  import { declaring, isPlainObject, shallowList, snapshot } from './plain.js';
14
15
  import { brokenAttachHook, notAnObject } from './plugin-rules.js';
15
16
  import { fillInputs } from './sources.js';
16
- import { captureInputConfig, configUnread, checkDeclarations, declaringSite, inputPlace, validateValues, } from './validation.js';
17
+ import { captureInputConfig, configUnread, checkDeclarations, declaringSite, inputPlace, rejectsGlobal, validateValues, } from './validation.js';
17
18
  /**
18
19
  * The deepest level below the root a Command may sit at, and the word its diagnostic spells it with.
19
20
  * A child of the root sits at level 1. Raising the cap relaxes a rule and breaks no application.
@@ -105,10 +106,6 @@ function checkCommandOptions(name, options) {
105
106
  });
106
107
  }
107
108
  }
108
- /** One name rule for an argument, option, or view name: a bare token the parser can read. */
109
- function isDeclaredName(name) {
110
- return typeof name === 'string' && Boolean(name) && !name.startsWith('-') && !/[\s=]/u.test(name);
111
- }
112
109
  /**
113
110
  * The portable name rule, for every name an operator types as a command at a shell prompt: the
114
111
  * application name, every Command name, and every alias. The characters are the POSIX portable
@@ -234,11 +231,11 @@ function checkOpen(state, late) {
234
231
  const inputRemedy = 'Declare arguments and options before action().';
235
232
  /** Where one input was declared: the call on the Command at `path` that declared it. */
236
233
  function inputSite(name, path, input) {
237
- return declaringSite(input, inputPlace(input, { global: false, path }), `${commandSentence(name)} ${input.kind} ${quoted(input.name)}`);
234
+ return declaringSite(input, inputPlace(input, path), `${commandSentence(name)} ${input.kind} ${quoted(input.name)}`);
238
235
  }
239
236
  /** The finding for the `argument()` or `option()` call that declared one input, marking its name. */
240
237
  function inputFinding(path, input, note) {
241
- const site = declaringSite(input, inputPlace(input, { global: false, path }));
238
+ const site = declaringSite(input, inputPlace(input, path));
242
239
  return siteFinding(site, site.named, note);
243
240
  }
244
241
  /**
@@ -246,7 +243,7 @@ function inputFinding(path, input, note) {
246
243
  * name is judged, so it prints elided.
247
244
  */
248
245
  function nameFinding(path, input) {
249
- const site = configUnread(declaringSite(input, inputPlace(input, { global: false, path })));
246
+ const site = configUnread(declaringSite(input, inputPlace(input, path)));
250
247
  return siteFinding(site, site.named);
251
248
  }
252
249
  /** The scope one Command's own options compile under: its subject, and each option's call. */
@@ -324,7 +321,7 @@ function checkArgumentName(command, declared, input) {
324
321
  const { path } = command;
325
322
  if (!isDeclaredName(input.name)) {
326
323
  throw new DeclarationError(declaredName, {
327
- correction: 'Use a nonempty name without a leading hyphen, whitespace, or "=".',
324
+ correction: declaredNameCorrection,
328
325
  findings: [nameFinding(path, input)],
329
326
  sentence: `${commandSentence(command.name)} declares an argument named ${quoted(input.name)}.`,
330
327
  });
@@ -439,18 +436,17 @@ export function declareAlias(state, names) {
439
436
  const aliases = [...state.aliases];
440
437
  for (const [index, alias] of names.entries()) {
441
438
  const repeated = { arguments: names, call: 'alias', mark: String(index), path };
439
+ const text = repeatedAliasText(commandSentence(name), alias);
442
440
  if (alias === name) {
443
441
  throw new DeclarationError(repeatedAlias, {
444
- correction: 'Remove the alias.',
442
+ ...text.own,
445
443
  findings: [{ ...repeated, note: 'its own name' }],
446
- sentence: `${commandSentence(name)} declares alias "${alias}", which is its own name.`,
447
444
  });
448
445
  }
449
446
  if (aliases.includes(alias)) {
450
447
  throw new DeclarationError(repeatedAlias, {
451
- correction: 'Remove the repeated alias.',
448
+ ...text.twice,
452
449
  findings: [{ ...repeated, note: 'already an alias' }],
453
- sentence: `${commandSentence(name)} declares alias "${alias}" twice.`,
454
450
  });
455
451
  }
456
452
  aliases.push(alias);
@@ -727,7 +723,7 @@ function attachChild(state, child) {
727
723
  /**
728
724
  * Walks one subtree joining an Application, once, for the rules only the Application can judge: one
729
725
  * Command value reached through two paths, two distinct descriptors under one identity, a local
730
- * option that meets a global or plugin option's key, spelling, or variable, and the nesting cap
726
+ * option that meets a global option's key, spelling, or variable, and the nesting cap
731
727
  * measured from the root. A claim is by node identity, and the name serves the diagnostic alone.
732
728
  * At a cap of two a named parent's `command()` already rejects every deeper tree, so the depth check
733
729
  * here fires only once the cap rises: attach reads one level, and only this walk knows each level.
@@ -1293,13 +1289,13 @@ function readSpellings(table, claimed, placeOf) {
1293
1289
  for (const [spelling, option] of table) {
1294
1290
  if (!claimed.has(spelling)) {
1295
1291
  const form = longs.get(option.name) ?? spelling;
1296
- claimed.set(spelling, { finding: placeOf(option.name, option.role), form });
1292
+ claimed.set(spelling, { finding: placeOf(option.name, option), form });
1297
1293
  }
1298
1294
  }
1299
1295
  }
1300
1296
  /** The place one input's spelling sits: the key of its call that yields the spelling. */
1301
- function spellingPlace(site, role, note) {
1302
- return siteFinding(site, spellingMark(site, role), note);
1297
+ function spellingPlace(site, origin, note) {
1298
+ return siteFinding(site, spellingMark(site, origin), note);
1303
1299
  }
1304
1300
  /** One hook-declared option's spellings, against every spelling the table already claims. */
1305
1301
  function checkAttachedSpelling(declared, named) {
@@ -1314,14 +1310,14 @@ function checkAttachedSpelling(declared, named) {
1314
1310
  throw new DeclarationError(spellingTaken, {
1315
1311
  correction: 'Change one of the two spellings or omit the plugin.',
1316
1312
  findings: [
1317
- spellingPlace(site, option.role, declarerNote(identity)),
1313
+ spellingPlace(site, option, declarerNote(identity)),
1318
1314
  ...(used.finding === undefined ? [] : [used.finding]),
1319
1315
  ],
1320
1316
  sentence: `Plugin ${quoted(identity)} declares option ${quoted(input.name)} with spelling ${quoted(spelling)} on ${subject}, which ${quoted(used.form)} already uses.`,
1321
1317
  });
1322
1318
  }
1323
1319
  }
1324
- readSpellings(table, claimed, (_name, role) => spellingPlace(site, role, declarerNote(identity)));
1320
+ readSpellings(table, claimed, (_name, origin) => spellingPlace(site, origin, declarerNote(identity)));
1325
1321
  }
1326
1322
  /** The declarations of one kind, keyed by name, the first of each name winning. */
1327
1323
  function byName(inputs) {
@@ -1335,7 +1331,7 @@ function byName(inputs) {
1335
1331
  }
1336
1332
  /** Where the globals table's options declared their spellings, a global or a plugin's option. */
1337
1333
  function tableSpellings(globals) {
1338
- return (name, role) => {
1334
+ return (name, origin) => {
1339
1335
  const entry = globals.names.get(name);
1340
1336
  if (!entry) {
1341
1337
  return undefined;
@@ -1344,7 +1340,7 @@ function tableSpellings(globals) {
1344
1340
  const note = owner.kind === 'plugin'
1345
1341
  ? `an option of plugin ${quoted(owner.identity)}`
1346
1342
  : `the global option ${quoted(name)}`;
1347
- return spellingPlace(site, role, note);
1343
+ return spellingPlace(site, origin, note);
1348
1344
  };
1349
1345
  }
1350
1346
  /**
@@ -1371,11 +1367,11 @@ function checkAttachedInputs(declared, attached, place) {
1371
1367
  };
1372
1368
  const claimed = new Map();
1373
1369
  readSpellings(globals.options, claimed, tableSpellings(globals));
1374
- readSpellings(compileOptions(options, scope), claimed, (name, role) => {
1370
+ readSpellings(compileOptions(options, scope), claimed, (name, origin) => {
1375
1371
  const local = locals.get(name);
1376
1372
  return local === undefined || local.kind !== 'option'
1377
1373
  ? undefined
1378
- : spellingPlace(scope.siteOf(local), role, `the local option ${quoted(name)}`);
1374
+ : spellingPlace(scope.siteOf(local), origin, `the local option ${quoted(name)}`);
1379
1375
  });
1380
1376
  for (const entry of attached) {
1381
1377
  const { identity, input } = entry;
@@ -1404,7 +1400,7 @@ function bindDispatch(state, action, globals) {
1404
1400
  // The graph erases the binder's generic relationship.
1405
1401
  // It holds because attachment checks the global output requirement.
1406
1402
  // The call or the attach that met both options rejected a collision between a global and a local.
1407
- // This binder returns the Application's validated globals alone.
1403
+ // This binder returns the validated value of every global option, the plugins' included.
1408
1404
  // oxlint-disable-next-line typescript/no-unsafe-type-assertion
1409
1405
  const globalOptions = globals.bind(values);
1410
1406
  return action({
@@ -1499,6 +1495,7 @@ export function buildCommand(state, context) {
1499
1495
  checkArgumentPlacement(place, slots[0], state.children[0]);
1500
1496
  const result = checkFinished(hooked, hasAction, { path: context.path });
1501
1497
  const options = checkLocalOptions(optionsOf(hooked.inputs), globals, localScope(name, context.path));
1498
+ const table = new Map([...tableEntries(options, false), ...globals.table]);
1502
1499
  // A hook's erased calls answer the declaration rules an authored call answers at the call.
1503
1500
  checkDeclarations(hookInputs.map(({ input }) => ({ input, site: inputSite(name, context.path, input) })));
1504
1501
  const children = new Map();
@@ -1525,9 +1522,9 @@ export function buildCommand(state, context) {
1525
1522
  hidden: facts.hidden,
1526
1523
  inputs: hooked.inputs,
1527
1524
  name,
1528
- options,
1529
1525
  result,
1530
1526
  routes,
1527
+ table,
1531
1528
  };
1532
1529
  }
1533
1530
  /**
@@ -1637,17 +1634,15 @@ export function collectInputs(command) {
1637
1634
  ];
1638
1635
  }
1639
1636
  /**
1640
- * Where every input of one graph was declared: each global option at its `globalOption()` call,
1641
- * and each Command's own inputs at their calls on the Command, under its path from the root.
1637
+ * Where every input of one graph was declared: each global option at its `globalOption()` call or
1638
+ * in its plugin's `options` record, and each Command's own inputs at their calls on the Command,
1639
+ * under its path from the root.
1642
1640
  */
1643
1641
  export function inputPlaces(graph) {
1644
- const places = new Map();
1645
- for (const input of graph.globals.inputs) {
1646
- places.set(input, inputPlace(input, { global: true, path: [] }));
1647
- }
1642
+ const places = new Map(graph.globals.sites);
1648
1643
  const walk = (command, path) => {
1649
1644
  for (const input of command.inputs) {
1650
- places.set(input, inputPlace(input, { global: false, path }));
1645
+ places.set(input, declaringSite(input, inputPlace(input, path)));
1651
1646
  }
1652
1647
  for (const [name, child] of command.children) {
1653
1648
  walk(child, [...path, name]);
@@ -1656,97 +1651,36 @@ export function inputPlaces(graph) {
1656
1651
  walk(graph.root, []);
1657
1652
  return places;
1658
1653
  }
1659
- /**
1660
- * The names a routing failure offers: the canonical names of the visible, current children, in
1661
- * authoring order. A candidate list is a listing, so a hidden or a deprecated child is absent from
1662
- * it, as completion leaves them out, and a parent whose children are all hidden or deprecated
1663
- * offers none. A deprecated child typed in full still routes.
1664
- */
1665
- function candidatesOf(command) {
1666
- return [...command.children]
1667
- .filter(([, child]) => !child.hidden && child.deprecated === undefined)
1668
- .map(([name]) => name);
1669
- }
1670
- /**
1671
- * Whether a token names one of the Command's children: the Command has children and the token is
1672
- * no option token. Routing and `locate` read a bare word through this rule.
1673
- */
1674
- export function readsAsChild(command, token) {
1675
- return command.children.size > 0 && !isOptionToken(token);
1676
- }
1677
- /**
1678
- * Bare tokens, names or aliases, select children until a Command has none; a hyphen commits.
1679
- * `walked` receives the path after each name routes, so an unknown Command leaves its caller
1680
- * holding the partial path walked before it.
1681
- */
1682
- export function route(root, tokens, walked) {
1683
- let command = root;
1684
- const path = [];
1685
- let index = 0;
1686
- for (let token = tokens[index]; token !== undefined; token = tokens[index]) {
1687
- if (!readsAsChild(command, token)) {
1688
- break;
1689
- }
1690
- const child = command.routes.get(token);
1691
- if (!child) {
1692
- throw new UnknownCommandError(token, candidatesOf(command));
1693
- }
1694
- // An alias routes like the canonical name, and the path it walks reports that name alone.
1695
- command = child.command;
1696
- path.push(child.name);
1697
- walked?.(Object.freeze([...path]));
1698
- index += 1;
1654
+ /** One positional word bound to its slot: a variadic slot collects it, any other holds it. */
1655
+ function bindWord(values, slot, word) {
1656
+ const bound = values.get(slot.input);
1657
+ if (!slot.variadic) {
1658
+ values.set(slot.input, word);
1659
+ }
1660
+ else if (Array.isArray(bound)) {
1661
+ bound.push(word);
1662
+ }
1663
+ else {
1664
+ values.set(slot.input, [word]);
1699
1665
  }
1700
- return { command, path, tokens: tokens.slice(index) };
1701
1666
  }
1702
1667
  /**
1703
- * The tokens each positional slot received. An omitted required argument binds nothing here and
1668
+ * The words each positional slot received. Parsing already held the first positional no slot
1669
+ * accepts, so every positional here has a slot. An omitted required argument binds nothing and
1704
1670
  * reports as a missing input in the validation phase, so omission has one class whether the input
1705
- * is an argument or an option. Extra tokens are a token fault, so this phase still reports them.
1671
+ * is an argument or an option. An empty variadic tail binds nothing, so validation reads it as
1672
+ * `[]` or reports the omission.
1706
1673
  */
1707
- function bindArguments(command, path, positionals) {
1674
+ function bindArguments(command, positionals) {
1708
1675
  const values = new Map();
1709
- for (const [position, token] of positionals.entries()) {
1676
+ for (const [position, word] of positionals.entries()) {
1710
1677
  const slot = argumentSlot(command.arguments, position);
1711
- if (!slot) {
1712
- throw new UnexpectedArgumentError(path, command.arguments.length, positionals.slice(position));
1713
- }
1714
- // An empty variadic tail binds nothing, so validation reads it as `[]` or reports the omission.
1715
- const bound = values.get(slot.input);
1716
- if (!slot.variadic) {
1717
- values.set(slot.input, token);
1718
- }
1719
- else if (Array.isArray(bound)) {
1720
- bound.push(token);
1721
- }
1722
- else {
1723
- values.set(slot.input, [token]);
1678
+ if (slot) {
1679
+ bindWord(values, slot, word);
1724
1680
  }
1725
1681
  }
1726
1682
  return values;
1727
1683
  }
1728
- /**
1729
- * The slot the positional at one index fills: the slot at that index, else a variadic last slot,
1730
- * which accepts every later positional, else none. Binding and `locate` read positions through it.
1731
- */
1732
- export function argumentSlot(slots, position) {
1733
- const last = slots.at(-1);
1734
- return slots[position] ?? (last?.variadic ? last : undefined);
1735
- }
1736
- /**
1737
- * Consumes the globals table, then routes the remaining bare tokens to a Command. `walked`
1738
- * receives the path as routing extends it, the partial path of an unknown Command included.
1739
- */
1740
- export function routeInvocation(graph, argv, walked) {
1741
- const scan = extractGlobals(graph.globals.options, [...argv]);
1742
- const routed = route(graph.root, scan.rest, walked);
1743
- return {
1744
- command: routed.command,
1745
- path: routed.path,
1746
- scan: scan.values,
1747
- tokens: routed.tokens,
1748
- };
1749
- }
1750
1684
  /**
1751
1685
  * The frozen records one middleware reads: the routed Command's own inputs, and the tail. Every
1752
1686
  * value is a plain-data copy frozen to every depth, so a middleware that reaches into a list or a
@@ -1768,30 +1702,25 @@ function requestOf(command, values, passthrough) {
1768
1702
  });
1769
1703
  }
1770
1704
  /**
1771
- * The callable check and local parsing. A group answers no invocation of its own, so it holds the
1772
- * missing-subcommand error with the rank the routing errors have and parses no token; a token
1773
- * fault holds the same way.
1705
+ * The held structural fault, then the callable check. A group answers no invocation of its own, so
1706
+ * it holds the missing-subcommand error, which ranks after every structural fault.
1774
1707
  */
1775
1708
  function parseLocal(routed) {
1776
- const { command, path } = routed;
1709
+ const { command, fault, path } = routed;
1777
1710
  const { dispatch } = command;
1778
- try {
1779
- if (!dispatch) {
1780
- throw new NonCallableCommandError(path, candidatesOf(command));
1781
- }
1782
- const parsed = parseInputs(command.options, routed.tokens);
1783
- const args = bindArguments(command, path, parsed.positionals);
1784
- return {
1785
- args,
1786
- dispatch,
1787
- kind: 'parsed',
1788
- options: parsed.options,
1789
- passthrough: parsed.passthrough,
1790
- };
1711
+ if (fault) {
1712
+ return { fault, kind: 'held' };
1791
1713
  }
1792
- catch (error) {
1793
- return { fault: error, kind: 'held' };
1714
+ if (!dispatch) {
1715
+ return { fault: new NonCallableCommandError(path, candidatesOf(command)), kind: 'held' };
1794
1716
  }
1717
+ return {
1718
+ args: bindArguments(command, routed.positionals),
1719
+ dispatch,
1720
+ kind: 'parsed',
1721
+ options: routed.values.locals,
1722
+ passthrough: routed.passthrough,
1723
+ };
1795
1724
  }
1796
1725
  /**
1797
1726
  * The node of one option a configuration source is asked about: in the graph's globals, or among
@@ -1806,21 +1735,24 @@ function requestNode(graph, path, place) {
1806
1735
  }
1807
1736
  return node;
1808
1737
  }
1738
+ /** The passthrough tail a validator reads: the routed Command's, or none while a fault is held. */
1739
+ function tailOf(local) {
1740
+ return local.kind === 'parsed' ? local.passthrough : [];
1741
+ }
1809
1742
  /**
1810
- * The input-source stage over this run's own copies of the parsed values. The global and plugin
1811
- * options fill whatever local parsing held; the routed Command's own options fill only when local
1812
- * parsing held no fault, because the request is `null` otherwise.
1743
+ * The input-source stage over this run's own copies of the parsed values. The global options fill
1744
+ * whatever was held, and a faulted occurrence supplied nothing, so a source may fill its option;
1745
+ * the routed Command's own options fill only when nothing is held, because the request is `null`
1746
+ * otherwise. A configuration source's own options pass their validators before the source is
1747
+ * called, as a pass over those options alone.
1813
1748
  */
1814
- async function fillScope({ graph, invocation, routed }, local) {
1815
- const globals = copyValues(routed.scan);
1749
+ async function fillScope(preparation, local) {
1750
+ const { graph, invocation, routed } = preparation;
1751
+ const globals = copyValues(routed.values.globals);
1816
1752
  const locals = copyValues(local.kind === 'parsed' ? local.options : emptyValues());
1817
1753
  const sources = await fillInputs({
1818
1754
  extensions: graph.extensions,
1819
- globals: {
1820
- global: true,
1821
- inputs: [...graph.globals.inputs, ...graph.globals.plugins.flatMap((entry) => entry.inputs)],
1822
- values: globals,
1823
- },
1755
+ globals: { global: true, inputs: graph.globals.inputs, values: globals },
1824
1756
  host: invocation.host,
1825
1757
  inspected: invocation.inspected,
1826
1758
  locals: local.kind === 'parsed'
@@ -1832,28 +1764,52 @@ async function fillScope({ graph, invocation, routed }, local) {
1832
1764
  request: (input, global) => requestNode(invocation.inspected(), routed.path, { global, name: input.name }),
1833
1765
  signal: invocation.signal,
1834
1766
  style: invocation.style,
1767
+ validate: (inputs, provenance) => validateInvocation(preparation, {
1768
+ local,
1769
+ locals,
1770
+ only: new Set(inputs),
1771
+ sources: provenance,
1772
+ values: globals,
1773
+ }),
1835
1774
  });
1836
1775
  return { globals, locals, sources };
1837
1776
  }
1838
1777
  /**
1839
- * Validation and the dispatch it prepares, over the values the input-source stage left. It answers
1840
- * with the call that dispatches, so the caller records that the action was invoked at the moment
1841
- * it invokes it and no earlier failure reads as a dispatch.
1778
+ * A validation pass over the values the input-source stage has filled: every global option
1779
+ * whatever was held, and the routed Command's own declarations only when nothing is held and the
1780
+ * Command is not a group. `only` narrows the declarations a pass validates, as the pass over a
1781
+ * configuration source's own options does ahead of its call, while every validator reads the same
1782
+ * context. The run's one pass reads that earlier pass's values and problems rather than validating
1783
+ * them again.
1842
1784
  */
1843
- async function readyDispatch({ graph, invocation, routed }, filled) {
1844
- const { command, path } = routed;
1845
- const { local } = filled;
1846
- const values = await validateValues({
1847
- command: path,
1848
- defaults: invocation.defaults,
1785
+ function validateInvocation({ graph, invocation, routed }, filled) {
1786
+ const { local, only, sources } = filled;
1787
+ const parsed = local.kind === 'parsed';
1788
+ return validateValues({
1789
+ command: routed.path,
1790
+ declaredValues: invocation.declaredValues,
1849
1791
  host: invocation.host,
1850
- inputs: { globals: graph.globals.inputs, locals: command.inputs },
1851
- passthrough: local.passthrough,
1852
- plugins: graph.globals.plugins.flatMap((entry) => entry.inputs),
1792
+ inputs: { globals: graph.globals.inputs, locals: parsed ? routed.command.inputs : [] },
1793
+ passthrough: tailOf(local),
1794
+ places: invocation.places,
1853
1795
  signal: invocation.signal,
1854
- sources: filled.sources,
1855
- supplied: { args: local.args, options: filled.values },
1796
+ sources,
1797
+ supplied: {
1798
+ args: parsed ? local.args : new Map(),
1799
+ options: parsed ? mergeValues(filled.values, filled.locals) : filled.values,
1800
+ },
1801
+ table: routed.command.table,
1802
+ ...(only ? { only } : {}),
1803
+ ...(sources.validated ? { prior: sources.validated } : {}),
1856
1804
  });
1805
+ }
1806
+ /**
1807
+ * The dispatch a clean validation prepares. It answers with the call that dispatches, so the caller
1808
+ * records that the action was invoked at the moment it invokes it and no earlier failure reads as a
1809
+ * dispatch.
1810
+ */
1811
+ function readyDispatch({ invocation, routed }, local, values) {
1812
+ const { command, path } = routed;
1857
1813
  return {
1858
1814
  dispatch: async (view) => {
1859
1815
  // The channel is built at the boundary, because the view a middleware selected is read there.
@@ -1893,37 +1849,47 @@ async function readyDispatch({ graph, invocation, routed }, filled) {
1893
1849
  /**
1894
1850
  * Prepares one dispatch ahead of the middleware chain and holds whatever fault it found, so a
1895
1851
  * middleware reads the request before the action runs and a takeover never observes the fault.
1896
- * The phases run in order: local parsing, the input-source stage, and validation. A local fault
1897
- * outranks a configuration source's fault, and after either one core runs no validation.
1852
+ * The phases run in order: the structural fault parsing held, a group's missing subcommand, the
1853
+ * input-source stage, and validation. Either of the first two outranks a configuration source's
1854
+ * fault, and a source's fault takes the place of every validation problem. Validation checks the
1855
+ * global options whatever was held, so a middleware reads them after a local fault; a structural
1856
+ * fault on a global option, a rejected global option, a source's fault, or a validator's own fault
1857
+ * leaves `options` `null`.
1898
1858
  */
1899
1859
  export async function prepareDispatch(graph, routed, invocation) {
1900
1860
  const result = routed.command.result;
1901
1861
  const local = parseLocal(routed);
1902
1862
  const preparation = { graph, invocation, routed };
1903
1863
  const { globals, locals, sources } = await fillScope(preparation, local);
1904
- const held = (fault) => ({
1905
- fault,
1864
+ const held = (fault, options) => ({
1865
+ fault: local.kind === 'held' ? local.fault : fault,
1906
1866
  globals,
1907
1867
  kind: 'held',
1868
+ options,
1908
1869
  request: null,
1909
1870
  result,
1910
1871
  });
1911
- if (local.kind === 'held') {
1912
- return held(local.fault);
1913
- }
1914
1872
  if (sources.fault) {
1915
- return held(sources.fault);
1873
+ return held(sources.fault, null);
1916
1874
  }
1917
1875
  try {
1918
- const ready = await readyDispatch(preparation, {
1876
+ const validation = await validateInvocation(preparation, {
1919
1877
  local,
1878
+ locals,
1920
1879
  sources,
1921
- values: mergeValues(globals, locals),
1880
+ values: globals,
1922
1881
  });
1923
- return { ...ready, globals, kind: 'ready', result };
1882
+ const options = routed.globalFault || rejectsGlobal(validation)
1883
+ ? null
1884
+ : frozenValues(graph.globals.inputs, validation.values);
1885
+ if (local.kind === 'held' || validation.failure) {
1886
+ return held(validation.failure, options);
1887
+ }
1888
+ const ready = readyDispatch(preparation, local, validation.values);
1889
+ return { ...ready, globals, kind: 'ready', options, result };
1924
1890
  }
1925
1891
  catch (error) {
1926
1892
  // The fault is held as a failure, so a throw from reading a validator's output is never offered.
1927
- return held(toFailure(error));
1893
+ return held(toFailure(error), null);
1928
1894
  }
1929
1895
  }
package/dist/errors.d.ts CHANGED
@@ -10,20 +10,6 @@ import type { InputIdentity } from './types.js';
10
10
  * keeps the raw value.
11
11
  */
12
12
  export declare function quoted(value: unknown): string;
13
- /**
14
- * The two short-group faults. A value option that is not last in its group names that option's
15
- * spelling; a group that mixes scopes names only the two letters that disagree, because the rest
16
- * of the group may hold an inline value. `token` keeps the whole group as the reported fact.
17
- */
18
- type ShortGroupFault = {
19
- reason: 'value-position';
20
- token: string;
21
- } | {
22
- reason: 'mixed-scope';
23
- token: string;
24
- global: string;
25
- other: string;
26
- };
27
13
  /**
28
14
  * The one message a distributed build shows an operator for a defect or a declaration fault: the
29
15
  * application name and a fixed phrase, with no reason, class name, code, or path.
@@ -123,24 +109,39 @@ export declare class UnknownOptionError extends UsageError {
123
109
  readonly spelling: string;
124
110
  constructor(spelling: string);
125
111
  }
112
+ /**
113
+ * Where a missing value should have been: the words ran out, or the next word was an option word or
114
+ * the bare `--`, which is never a separate value, so the sentence names the attached form too.
115
+ */
116
+ type MissingValueForm = 'attached' | 'separate';
126
117
  export declare class MissingValueError extends UsageError {
127
118
  readonly spelling: string;
128
- constructor(spelling: string);
119
+ constructor(spelling: string, form?: MissingValueForm);
129
120
  }
130
- /** A Boolean spelling takes no value, so the token carried one the declaration cannot accept. */
121
+ /**
122
+ * A Boolean or counted spelling takes no value, so the token carried one the declaration cannot
123
+ * accept. A counted option's sentence tells the operator to repeat the spelling instead, and `kind`
124
+ * chooses the sentence alone.
125
+ */
131
126
  export declare class UnexpectedValueError extends UsageError {
132
127
  readonly spelling: string;
133
128
  readonly value: string;
134
- constructor(spelling: string, value: string);
129
+ constructor(spelling: string, value: string, kind?: 'boolean' | 'count');
135
130
  }
136
131
  export declare class RepeatedOptionError extends UsageError {
137
132
  readonly spelling: string;
138
133
  constructor(spelling: string);
139
134
  }
140
- export declare class ShortGroupError extends UsageError {
141
- readonly token: string;
142
- readonly reason: 'value-position' | 'mixed-scope';
143
- constructor(fault: ShortGroupFault);
135
+ /**
136
+ * An option word the Command routing reached does not declare, while a visible Command below it
137
+ * does, such as a Command's own option typed before its name, or a parent's own option the routed
138
+ * Command declares with another value class. `commands` holds each such Command's path from the
139
+ * root, in authoring order.
140
+ */
141
+ export declare class MisplacedOptionError extends UsageError {
142
+ readonly spelling: string;
143
+ readonly commands: readonly (readonly string[])[];
144
+ constructor(spelling: string, commands: readonly (readonly string[])[]);
144
145
  }
145
146
  /**
146
147
  * Exit 1: the declaration is wrong, so the author reads its Developer Diagnostic. A rule and the