@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
package/dist/command.js CHANGED
@@ -1,18 +1,20 @@
1
1
  var _a, _b;
2
2
  import { checkEnvBinding, checkNoArgumentBinding } from './bindings.js';
3
+ import { captureDeclaration, unreadableArgument } from './capture.js';
3
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';
4
- import { quoteString, spelled } from './diagnostic-text.js';
5
- import { asSentence, commandSentence, commandSubject, DeclarationError, NonCallableCommandError, quoted, ResultError, toFailure, UnexpectedArgumentError, UnknownCommandError, reasonOf, } from './errors.js';
5
+ import { elided, quoteString, spelled } from './diagnostic-text.js';
6
+ import { asSentence, commandSentence, commandSubject, DeclarationError, NonCallableCommandError, quoted, ResultError, toFailure, reasonOf, } from './errors.js';
6
7
  import { buildExtensions, extendStore, publishStore, registerDescriptor, storeCommandLayers, validateLayer, } from './extension.js';
7
- import { checkDeprecated, checkDescription, checkHidden, checkNoListingFacts, siteFinding, } from './facts.js';
8
- import { buildGlobals, checkLocalOptions } from './globals.js';
8
+ import { checkDeprecated, checkDescription, checkHidden, checkNoListingFacts, declarerNote, siteFinding, } from './facts.js';
9
+ import { buildGlobals, checkLocalOptions, frozenValues } from './globals.js';
9
10
  import { nameSharedAcrossKinds, optionDeclaredTwice, spellingTaken } from './input-rules.js';
10
- import { graphMismatch, nodeAt, resultNode, snapshot } from './inspect.js';
11
- import { checkOptionName, compileOptions, copyValues, emptyValues, extractGlobals, isOptionToken, mergeValues, parseInputs, spellingMark, } from './options.js';
12
- import { isPlainObject } from './plain.js';
11
+ import { graphMismatch, nodeAt, resultNode } from './inspect.js';
12
+ import { checkOptionName, compileOptions, copyValues, declaredNameCorrection, emptyValues, isDeclaredName, mergeValues, repeatedAliasText, spellingMark, tableEntries, } from './options.js';
13
+ import { argumentSlot, candidatesOf } from './parse.js';
14
+ import { declaring, isPlainObject, shallowList, snapshot } from './plain.js';
13
15
  import { brokenAttachHook, notAnObject } from './plugin-rules.js';
14
16
  import { fillInputs } from './sources.js';
15
- import { captureConfig, checkInputConfig, checkDeclarations, declaringSite, inputPlace, validateValues, } from './validation.js';
17
+ import { captureInputConfig, configUnread, checkDeclarations, declaringSite, inputPlace, rejectsGlobal, validateValues, } from './validation.js';
16
18
  /**
17
19
  * The deepest level below the root a Command may sit at, and the word its diagnostic spells it with.
18
20
  * A child of the root sits at level 1. Raising the cap relaxes a rule and breaks no application.
@@ -73,16 +75,30 @@ export function commandPlacement(path, child) {
73
75
  function constructorFinding(name, options, mark) {
74
76
  return { arguments: callArguments(name, options), call: 'new Command', mark };
75
77
  }
76
- /** A named Command's options slot holds a plain options object, and never retired globals wiring. */
77
- function checkCommandOptions(name, options) {
78
- if (options !== undefined && !isPlainObject(options)) {
79
- throw new DeclarationError(notAnObject, {
78
+ /**
79
+ * The one copy of the options that `new Command(name, options)` reads, taken once the name is
80
+ * judged: the options object and its `extensions` list, whose entries are what their factory built
81
+ * and are not copied. A read that throws is the unreadable fault of the options, a value that is not
82
+ * a plain object is the not-an-object fault, and a Command declared without options has none.
83
+ */
84
+ function captureCommandOptions(name, options) {
85
+ if (options === undefined) {
86
+ return undefined;
87
+ }
88
+ return captureDeclaration(options, (copy, read) => {
89
+ read.nested(copy, 'extensions', shallowList);
90
+ }, {
91
+ notAnObject: () => new DeclarationError(notAnObject, {
80
92
  correction: 'Supply a Command options object.',
81
93
  findings: [constructorFinding(name, options, '1')],
82
94
  sentence: `${commandSentence(name)} declares options that are not an object.`,
83
- });
84
- }
85
- if (isPlainObject(options) && 'globals' in options) {
95
+ }),
96
+ unreadable: unreadableArgument({ call: 'new Command', named: name, subject: commandSentence(name) }, 'options'),
97
+ });
98
+ }
99
+ /** A named Command's options never carry the retired globals wiring. */
100
+ function checkCommandOptions(name, options) {
101
+ if ('globals' in options) {
86
102
  throw new DeclarationError(commandGlobals, {
87
103
  correction: 'Declare globals on the Application and register its environment.',
88
104
  findings: [constructorFinding(name, options, '1.globals')],
@@ -90,10 +106,6 @@ function checkCommandOptions(name, options) {
90
106
  });
91
107
  }
92
108
  }
93
- /** One name rule for an argument, option, or view name: a bare token the parser can read. */
94
- function isDeclaredName(name) {
95
- return typeof name === 'string' && Boolean(name) && !name.startsWith('-') && !/[\s=]/u.test(name);
96
- }
97
109
  /**
98
110
  * The portable name rule, for every name an operator types as a command at a shell prompt: the
99
111
  * application name, every Command name, and every alias. The characters are the POSIX portable
@@ -149,23 +161,28 @@ export function freshState(declaration) {
149
161
  };
150
162
  }
151
163
  /**
152
- * A named Command's own declaration, checked before the value exists: the name, the options slot,
153
- * the core facts, and the extension values the slot carries. A later change to the options object
154
- * the author passed changes nothing the declaration holds.
164
+ * A named Command's own declaration, checked before the value exists: the name, then the one copy
165
+ * of the options slot, its core facts, and the extension values it carries. A later change to the
166
+ * options object the author passed changes nothing the declaration holds.
155
167
  */
156
168
  function namedState(name, options) {
157
169
  if (!isPortableName(name)) {
170
+ // The options are read after the name is judged, so the name's finding prints them elided.
158
171
  throw new DeclarationError(portableName, {
159
172
  correction: portableNameCorrection,
160
- findings: [constructorFinding(name, options, '0')],
173
+ findings: [
174
+ constructorFinding(name, options === undefined ? undefined : spelled(elided), '0'),
175
+ ],
161
176
  sentence: `Command name ${quoted(name)} is invalid.`,
162
177
  });
163
178
  }
164
- checkCommandOptions(name, options);
165
- const slot = isPlainObject(options) ? options : undefined;
179
+ const slot = captureCommandOptions(name, options);
180
+ if (slot !== undefined) {
181
+ checkCommandOptions(name, slot);
182
+ }
166
183
  const site = {
167
184
  at: '1',
168
- declaration: { arguments: [name, options], call: 'new Command' },
185
+ declaration: { arguments: [name, slot], call: 'new Command' },
169
186
  subject: commandSentence(name),
170
187
  };
171
188
  // The facts are read in the order their diagnostics have always ranked.
@@ -214,13 +231,21 @@ function checkOpen(state, late) {
214
231
  const inputRemedy = 'Declare arguments and options before action().';
215
232
  /** Where one input was declared: the call on the Command at `path` that declared it. */
216
233
  function inputSite(name, path, input) {
217
- 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)}`);
218
235
  }
219
236
  /** The finding for the `argument()` or `option()` call that declared one input, marking its name. */
220
237
  function inputFinding(path, input, note) {
221
- const site = declaringSite(input, inputPlace(input, { global: false, path }));
238
+ const site = declaringSite(input, inputPlace(input, path));
222
239
  return siteFinding(site, site.named, note);
223
240
  }
241
+ /**
242
+ * The finding for an input whose name is invalid, marking the name. The config is read after the
243
+ * name is judged, so it prints elided.
244
+ */
245
+ function nameFinding(path, input) {
246
+ const site = configUnread(declaringSite(input, inputPlace(input, path)));
247
+ return siteFinding(site, site.named);
248
+ }
224
249
  /** The scope one Command's own options compile under: its subject, and each option's call. */
225
250
  function localScope(name, path) {
226
251
  return { siteOf: (input) => inputSite(name, path, input), subject: commandSubject(name) };
@@ -296,8 +321,8 @@ function checkArgumentName(command, declared, input) {
296
321
  const { path } = command;
297
322
  if (!isDeclaredName(input.name)) {
298
323
  throw new DeclarationError(declaredName, {
299
- correction: 'Use a nonempty name without a leading hyphen, whitespace, or "=".',
300
- findings: [inputFinding(path, input)],
324
+ correction: declaredNameCorrection,
325
+ findings: [nameFinding(path, input)],
301
326
  sentence: `${commandSentence(command.name)} declares an argument named ${quoted(input.name)}.`,
302
327
  });
303
328
  }
@@ -317,11 +342,13 @@ function checkArgumentName(command, declared, input) {
317
342
  export function declareArgument(state, declared) {
318
343
  // The call's own input is judged before the receiver's state, as alias() judges its names.
319
344
  // A name of another kind then reports as a declared name instead of failing to print in the order diagnostic.
320
- // The config is judged right after its name, and only then captured.
345
+ // The config is read once right after its name is judged, and every later check reads that copy.
321
346
  const { name } = state;
322
347
  checkArgumentName({ name, path: pathOf(name), subject: commandSubject(name) }, [], declared);
323
- checkInputConfig(declared, { call: 'argument', path: pathOf(name) });
324
- const input = { ...declared, config: captureConfig(declared.config) };
348
+ const input = {
349
+ ...declared,
350
+ config: captureInputConfig(declared, { call: 'argument', path: pathOf(name) }),
351
+ };
325
352
  checkOpen(state, {
326
353
  arguments: [input.name, input.config],
327
354
  call: 'argument',
@@ -350,11 +377,14 @@ const noGlobals = { names: new Map(), options: new Map(), variables: new Map() }
350
377
  */
351
378
  export function declareOption(state, declared, table = noGlobals) {
352
379
  // The call's own input is judged before the receiver's state, as alias() judges its names.
353
- // The config is judged right after its name, and only then captured.
354
- checkOptionName(declared.name, inputSite(state.name, pathOf(state.name), declared));
355
- checkInputConfig(declared, { call: 'option', path: pathOf(state.name) });
356
- const input = { ...declared, config: captureConfig(declared.config) };
357
- const site = inputSite(state.name, pathOf(state.name), input);
380
+ // The config is read once right after its name is judged, and every later check reads that copy.
381
+ const path = pathOf(state.name);
382
+ checkOptionName(declared.name, configUnread(inputSite(state.name, path, declared)));
383
+ const input = {
384
+ ...declared,
385
+ config: captureInputConfig(declared, { call: 'option', path }),
386
+ };
387
+ const site = inputSite(state.name, path, input);
358
388
  checkOpen(state, {
359
389
  arguments: [input.name, input.config],
360
390
  call: 'option',
@@ -406,18 +436,17 @@ export function declareAlias(state, names) {
406
436
  const aliases = [...state.aliases];
407
437
  for (const [index, alias] of names.entries()) {
408
438
  const repeated = { arguments: names, call: 'alias', mark: String(index), path };
439
+ const text = repeatedAliasText(commandSentence(name), alias);
409
440
  if (alias === name) {
410
441
  throw new DeclarationError(repeatedAlias, {
411
- correction: 'Remove the alias.',
442
+ ...text.own,
412
443
  findings: [{ ...repeated, note: 'its own name' }],
413
- sentence: `${commandSentence(name)} declares alias "${alias}", which is its own name.`,
414
444
  });
415
445
  }
416
446
  if (aliases.includes(alias)) {
417
447
  throw new DeclarationError(repeatedAlias, {
418
- correction: 'Remove the repeated alias.',
448
+ ...text.twice,
419
449
  findings: [{ ...repeated, note: 'already an alias' }],
420
- sentence: `${commandSentence(name)} declares alias "${alias}" twice.`,
421
450
  });
422
451
  }
423
452
  aliases.push(alias);
@@ -632,7 +661,7 @@ function checkGroup(state, place) {
632
661
  throw new DeclarationError(groupOption, {
633
662
  correction: 'Register an action or remove the option.',
634
663
  findings: [inputFinding(place.path, option, 'no action reads it')],
635
- sentence: `${commandSentence(name)} declares option "${option.name}" but registers no action to receive it.`,
664
+ sentence: `${commandSentence(name)} declares option ${quoted(option.name)} but registers no action to receive it.`,
636
665
  });
637
666
  }
638
667
  }
@@ -694,7 +723,7 @@ function attachChild(state, child) {
694
723
  /**
695
724
  * Walks one subtree joining an Application, once, for the rules only the Application can judge: one
696
725
  * Command value reached through two paths, two distinct descriptors under one identity, a local
697
- * 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
698
727
  * measured from the root. A claim is by node identity, and the name serves the diagnostic alone.
699
728
  * At a cap of two a named parent's `command()` already rejects every deeper tree, so the depth check
700
729
  * here fires only once the cap rises: attach reads one level, and only this walk knows each level.
@@ -1045,13 +1074,13 @@ class AttachedCommandValue {
1045
1074
  checkArgumentName({ name, path, subject: commandSubject(name) }, [], raw);
1046
1075
  }
1047
1076
  else {
1048
- checkOptionName(raw.name, inputSite(declared.name, path, raw));
1077
+ checkOptionName(raw.name, configUnread(inputSite(declared.name, path, raw)));
1049
1078
  }
1050
- checkInputConfig(raw, { call: raw.kind, path });
1051
1079
  // Each kind captures its own config, so the input keeps the pairing its kind declares.
1080
+ const place = { call: raw.kind, path };
1052
1081
  const input = raw.kind === 'argument'
1053
- ? { ...raw, config: captureConfig(raw.config) }
1054
- : { ...raw, config: captureConfig(raw.config) };
1082
+ ? { ...raw, config: captureInputConfig(raw, place) }
1083
+ : { ...raw, config: captureInputConfig(raw, place) };
1055
1084
  return this.#derive({ identity, input, kind: 'input' }, { ...declared, inputs: [...declared.inputs, input] });
1056
1085
  }
1057
1086
  #derive(call, declared) {
@@ -1166,16 +1195,12 @@ function nameCollisionRule(declared, held) {
1166
1195
  }
1167
1196
  return declared === 'option' ? optionDeclaredTwice : argumentDeclaredTwice;
1168
1197
  }
1169
- /** The note a finding for one hook-declared input carries. */
1170
- function hookNote(identity) {
1171
- return `declared by plugin ${quoted(identity)}`;
1172
- }
1173
1198
  /** The clause and the remedy an input another plugin's hook already declared earns. */
1174
1199
  function hookClause(earlier, path) {
1175
1200
  const { identity, input } = earlier;
1176
1201
  return {
1177
1202
  clause: `an ${input.kind} plugin ${quoted(identity)} declared through onCommandAttach`,
1178
- held: inputFinding(path, input, hookNote(identity)),
1203
+ held: inputFinding(path, input, declarerNote(identity)),
1179
1204
  kind: input.kind,
1180
1205
  remedy: 'Install one of them.',
1181
1206
  };
@@ -1264,13 +1289,13 @@ function readSpellings(table, claimed, placeOf) {
1264
1289
  for (const [spelling, option] of table) {
1265
1290
  if (!claimed.has(spelling)) {
1266
1291
  const form = longs.get(option.name) ?? spelling;
1267
- claimed.set(spelling, { finding: placeOf(option.name, option.role), form });
1292
+ claimed.set(spelling, { finding: placeOf(option.name, option), form });
1268
1293
  }
1269
1294
  }
1270
1295
  }
1271
1296
  /** The place one input's spelling sits: the key of its call that yields the spelling. */
1272
- function spellingPlace(site, role, note) {
1273
- return siteFinding(site, spellingMark(site, role), note);
1297
+ function spellingPlace(site, origin, note) {
1298
+ return siteFinding(site, spellingMark(site, origin), note);
1274
1299
  }
1275
1300
  /** One hook-declared option's spellings, against every spelling the table already claims. */
1276
1301
  function checkAttachedSpelling(declared, named) {
@@ -1285,14 +1310,14 @@ function checkAttachedSpelling(declared, named) {
1285
1310
  throw new DeclarationError(spellingTaken, {
1286
1311
  correction: 'Change one of the two spellings or omit the plugin.',
1287
1312
  findings: [
1288
- spellingPlace(site, option.role, hookNote(identity)),
1313
+ spellingPlace(site, option, declarerNote(identity)),
1289
1314
  ...(used.finding === undefined ? [] : [used.finding]),
1290
1315
  ],
1291
1316
  sentence: `Plugin ${quoted(identity)} declares option ${quoted(input.name)} with spelling ${quoted(spelling)} on ${subject}, which ${quoted(used.form)} already uses.`,
1292
1317
  });
1293
1318
  }
1294
1319
  }
1295
- readSpellings(table, claimed, (_name, role) => spellingPlace(site, role, hookNote(identity)));
1320
+ readSpellings(table, claimed, (_name, origin) => spellingPlace(site, origin, declarerNote(identity)));
1296
1321
  }
1297
1322
  /** The declarations of one kind, keyed by name, the first of each name winning. */
1298
1323
  function byName(inputs) {
@@ -1306,7 +1331,7 @@ function byName(inputs) {
1306
1331
  }
1307
1332
  /** Where the globals table's options declared their spellings, a global or a plugin's option. */
1308
1333
  function tableSpellings(globals) {
1309
- return (name, role) => {
1334
+ return (name, origin) => {
1310
1335
  const entry = globals.names.get(name);
1311
1336
  if (!entry) {
1312
1337
  return undefined;
@@ -1314,8 +1339,8 @@ function tableSpellings(globals) {
1314
1339
  const { owner, site } = entry;
1315
1340
  const note = owner.kind === 'plugin'
1316
1341
  ? `an option of plugin ${quoted(owner.identity)}`
1317
- : `the global option "${name}"`;
1318
- return spellingPlace(site, role, note);
1342
+ : `the global option ${quoted(name)}`;
1343
+ return spellingPlace(site, origin, note);
1319
1344
  };
1320
1345
  }
1321
1346
  /**
@@ -1342,11 +1367,11 @@ function checkAttachedInputs(declared, attached, place) {
1342
1367
  };
1343
1368
  const claimed = new Map();
1344
1369
  readSpellings(globals.options, claimed, tableSpellings(globals));
1345
- readSpellings(compileOptions(options, scope), claimed, (name, role) => {
1370
+ readSpellings(compileOptions(options, scope), claimed, (name, origin) => {
1346
1371
  const local = locals.get(name);
1347
1372
  return local === undefined || local.kind !== 'option'
1348
1373
  ? undefined
1349
- : spellingPlace(scope.siteOf(local), role, `the local option "${name}"`);
1374
+ : spellingPlace(scope.siteOf(local), origin, `the local option ${quoted(name)}`);
1350
1375
  });
1351
1376
  for (const entry of attached) {
1352
1377
  const { identity, input } = entry;
@@ -1354,7 +1379,7 @@ function checkAttachedInputs(declared, attached, place) {
1354
1379
  if (collision) {
1355
1380
  throw new DeclarationError(nameCollisionRule(input.kind, collision.kind), {
1356
1381
  correction: collision.remedy,
1357
- findings: [inputFinding(path, input, hookNote(identity)), collision.held],
1382
+ findings: [inputFinding(path, input, declarerNote(identity)), collision.held],
1358
1383
  sentence: `Plugin ${quoted(identity)} declares ${input.kind} ${quoted(input.name)} on ${subject}, which is already declared as ${collision.clause}.`,
1359
1384
  });
1360
1385
  }
@@ -1375,7 +1400,7 @@ function bindDispatch(state, action, globals) {
1375
1400
  // The graph erases the binder's generic relationship.
1376
1401
  // It holds because attachment checks the global output requirement.
1377
1402
  // The call or the attach that met both options rejected a collision between a global and a local.
1378
- // This binder returns the Application's validated globals alone.
1403
+ // This binder returns the validated value of every global option, the plugins' included.
1379
1404
  // oxlint-disable-next-line typescript/no-unsafe-type-assertion
1380
1405
  const globalOptions = globals.bind(values);
1381
1406
  return action({
@@ -1470,6 +1495,7 @@ export function buildCommand(state, context) {
1470
1495
  checkArgumentPlacement(place, slots[0], state.children[0]);
1471
1496
  const result = checkFinished(hooked, hasAction, { path: context.path });
1472
1497
  const options = checkLocalOptions(optionsOf(hooked.inputs), globals, localScope(name, context.path));
1498
+ const table = new Map([...tableEntries(options, false), ...globals.table]);
1473
1499
  // A hook's erased calls answer the declaration rules an authored call answers at the call.
1474
1500
  checkDeclarations(hookInputs.map(({ input }) => ({ input, site: inputSite(name, context.path, input) })));
1475
1501
  const children = new Map();
@@ -1496,9 +1522,9 @@ export function buildCommand(state, context) {
1496
1522
  hidden: facts.hidden,
1497
1523
  inputs: hooked.inputs,
1498
1524
  name,
1499
- options,
1500
1525
  result,
1501
1526
  routes,
1527
+ table,
1502
1528
  };
1503
1529
  }
1504
1530
  /**
@@ -1594,7 +1620,7 @@ _b = CommandBuilder;
1594
1620
  */
1595
1621
  class CommandDeclaration extends CommandBuilder {
1596
1622
  constructor(name, options) {
1597
- const declared = namedState(name, options);
1623
+ const declared = declaring(() => namedState(name, options));
1598
1624
  super(declared.name, declared.state);
1599
1625
  }
1600
1626
  }
@@ -1608,17 +1634,15 @@ export function collectInputs(command) {
1608
1634
  ];
1609
1635
  }
1610
1636
  /**
1611
- * Where every input of one graph was declared: each global option at its `globalOption()` call,
1612
- * 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.
1613
1640
  */
1614
1641
  export function inputPlaces(graph) {
1615
- const places = new Map();
1616
- for (const input of graph.globals.inputs) {
1617
- places.set(input, inputPlace(input, { global: true, path: [] }));
1618
- }
1642
+ const places = new Map(graph.globals.sites);
1619
1643
  const walk = (command, path) => {
1620
1644
  for (const input of command.inputs) {
1621
- places.set(input, inputPlace(input, { global: false, path }));
1645
+ places.set(input, declaringSite(input, inputPlace(input, path)));
1622
1646
  }
1623
1647
  for (const [name, child] of command.children) {
1624
1648
  walk(child, [...path, name]);
@@ -1627,97 +1651,36 @@ export function inputPlaces(graph) {
1627
1651
  walk(graph.root, []);
1628
1652
  return places;
1629
1653
  }
1630
- /**
1631
- * The names a routing failure offers: the canonical names of the visible, current children, in
1632
- * authoring order. A candidate list is a listing, so a hidden or a deprecated child is absent from
1633
- * it, as completion leaves them out, and a parent whose children are all hidden or deprecated
1634
- * offers none. A deprecated child typed in full still routes.
1635
- */
1636
- function candidatesOf(command) {
1637
- return [...command.children]
1638
- .filter(([, child]) => !child.hidden && child.deprecated === undefined)
1639
- .map(([name]) => name);
1640
- }
1641
- /**
1642
- * Whether a token names one of the Command's children: the Command has children and the token is
1643
- * no option token. Routing and `locate` read a bare word through this rule.
1644
- */
1645
- export function readsAsChild(command, token) {
1646
- return command.children.size > 0 && !isOptionToken(token);
1647
- }
1648
- /**
1649
- * Bare tokens, names or aliases, select children until a Command has none; a hyphen commits.
1650
- * `walked` receives the path after each name routes, so an unknown Command leaves its caller
1651
- * holding the partial path walked before it.
1652
- */
1653
- export function route(root, tokens, walked) {
1654
- let command = root;
1655
- const path = [];
1656
- let index = 0;
1657
- for (let token = tokens[index]; token !== undefined; token = tokens[index]) {
1658
- if (!readsAsChild(command, token)) {
1659
- break;
1660
- }
1661
- const child = command.routes.get(token);
1662
- if (!child) {
1663
- throw new UnknownCommandError(token, candidatesOf(command));
1664
- }
1665
- // An alias routes like the canonical name, and the path it walks reports that name alone.
1666
- command = child.command;
1667
- path.push(child.name);
1668
- walked?.(Object.freeze([...path]));
1669
- 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]);
1670
1665
  }
1671
- return { command, path, tokens: tokens.slice(index) };
1672
1666
  }
1673
1667
  /**
1674
- * 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
1675
1670
  * reports as a missing input in the validation phase, so omission has one class whether the input
1676
- * 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.
1677
1673
  */
1678
- function bindArguments(command, path, positionals) {
1674
+ function bindArguments(command, positionals) {
1679
1675
  const values = new Map();
1680
- for (const [position, token] of positionals.entries()) {
1676
+ for (const [position, word] of positionals.entries()) {
1681
1677
  const slot = argumentSlot(command.arguments, position);
1682
- if (!slot) {
1683
- throw new UnexpectedArgumentError(path, command.arguments.length, positionals.slice(position));
1684
- }
1685
- // An empty variadic tail binds nothing, so validation reads it as `[]` or reports the omission.
1686
- const bound = values.get(slot.input);
1687
- if (!slot.variadic) {
1688
- values.set(slot.input, token);
1689
- }
1690
- else if (Array.isArray(bound)) {
1691
- bound.push(token);
1692
- }
1693
- else {
1694
- values.set(slot.input, [token]);
1678
+ if (slot) {
1679
+ bindWord(values, slot, word);
1695
1680
  }
1696
1681
  }
1697
1682
  return values;
1698
1683
  }
1699
- /**
1700
- * The slot the positional at one index fills: the slot at that index, else a variadic last slot,
1701
- * which accepts every later positional, else none. Binding and `locate` read positions through it.
1702
- */
1703
- export function argumentSlot(slots, position) {
1704
- const last = slots.at(-1);
1705
- return slots[position] ?? (last?.variadic ? last : undefined);
1706
- }
1707
- /**
1708
- * Consumes the globals table, then routes the remaining bare tokens to a Command. `walked`
1709
- * receives the path as routing extends it, the partial path of an unknown Command included.
1710
- */
1711
- export function routeInvocation(graph, argv, walked) {
1712
- const scan = extractGlobals(graph.globals.options, [...argv]);
1713
- const routed = route(graph.root, scan.rest, walked);
1714
- return {
1715
- command: routed.command,
1716
- path: routed.path,
1717
- scan: scan.values,
1718
- tokens: routed.tokens,
1719
- };
1720
- }
1721
1684
  /**
1722
1685
  * The frozen records one middleware reads: the routed Command's own inputs, and the tail. Every
1723
1686
  * value is a plain-data copy frozen to every depth, so a middleware that reaches into a list or a
@@ -1739,30 +1702,25 @@ function requestOf(command, values, passthrough) {
1739
1702
  });
1740
1703
  }
1741
1704
  /**
1742
- * The callable check and local parsing. A group answers no invocation of its own, so it holds the
1743
- * missing-subcommand error with the rank the routing errors have and parses no token; a token
1744
- * 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.
1745
1707
  */
1746
1708
  function parseLocal(routed) {
1747
- const { command, path } = routed;
1709
+ const { command, fault, path } = routed;
1748
1710
  const { dispatch } = command;
1749
- try {
1750
- if (!dispatch) {
1751
- throw new NonCallableCommandError(path, candidatesOf(command));
1752
- }
1753
- const parsed = parseInputs(command.options, routed.tokens);
1754
- const args = bindArguments(command, path, parsed.positionals);
1755
- return {
1756
- args,
1757
- dispatch,
1758
- kind: 'parsed',
1759
- options: parsed.options,
1760
- passthrough: parsed.passthrough,
1761
- };
1711
+ if (fault) {
1712
+ return { fault, kind: 'held' };
1762
1713
  }
1763
- catch (error) {
1764
- return { fault: error, kind: 'held' };
1714
+ if (!dispatch) {
1715
+ return { fault: new NonCallableCommandError(path, candidatesOf(command)), kind: 'held' };
1765
1716
  }
1717
+ return {
1718
+ args: bindArguments(command, routed.positionals),
1719
+ dispatch,
1720
+ kind: 'parsed',
1721
+ options: routed.values.locals,
1722
+ passthrough: routed.passthrough,
1723
+ };
1766
1724
  }
1767
1725
  /**
1768
1726
  * The node of one option a configuration source is asked about: in the graph's globals, or among
@@ -1777,21 +1735,24 @@ function requestNode(graph, path, place) {
1777
1735
  }
1778
1736
  return node;
1779
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
+ }
1780
1742
  /**
1781
- * The input-source stage over this run's own copies of the parsed values. The global and plugin
1782
- * options fill whatever local parsing held; the routed Command's own options fill only when local
1783
- * 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.
1784
1748
  */
1785
- async function fillScope({ graph, invocation, routed }, local) {
1786
- const globals = copyValues(routed.scan);
1749
+ async function fillScope(preparation, local) {
1750
+ const { graph, invocation, routed } = preparation;
1751
+ const globals = copyValues(routed.values.globals);
1787
1752
  const locals = copyValues(local.kind === 'parsed' ? local.options : emptyValues());
1788
1753
  const sources = await fillInputs({
1789
1754
  extensions: graph.extensions,
1790
- globals: {
1791
- global: true,
1792
- inputs: [...graph.globals.inputs, ...graph.globals.plugins.flatMap((entry) => entry.inputs)],
1793
- values: globals,
1794
- },
1755
+ globals: { global: true, inputs: graph.globals.inputs, values: globals },
1795
1756
  host: invocation.host,
1796
1757
  inspected: invocation.inspected,
1797
1758
  locals: local.kind === 'parsed'
@@ -1803,28 +1764,52 @@ async function fillScope({ graph, invocation, routed }, local) {
1803
1764
  request: (input, global) => requestNode(invocation.inspected(), routed.path, { global, name: input.name }),
1804
1765
  signal: invocation.signal,
1805
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
+ }),
1806
1774
  });
1807
1775
  return { globals, locals, sources };
1808
1776
  }
1809
1777
  /**
1810
- * Validation and the dispatch it prepares, over the values the input-source stage left. It answers
1811
- * with the call that dispatches, so the caller records that the action was invoked at the moment
1812
- * 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.
1813
1784
  */
1814
- async function readyDispatch({ graph, invocation, routed }, filled) {
1815
- const { command, path } = routed;
1816
- const { local } = filled;
1817
- const values = await validateValues({
1818
- command: path,
1819
- 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,
1820
1791
  host: invocation.host,
1821
- inputs: { globals: graph.globals.inputs, locals: command.inputs },
1822
- passthrough: local.passthrough,
1823
- 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,
1824
1795
  signal: invocation.signal,
1825
- sources: filled.sources,
1826
- 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 } : {}),
1827
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;
1828
1813
  return {
1829
1814
  dispatch: async (view) => {
1830
1815
  // The channel is built at the boundary, because the view a middleware selected is read there.
@@ -1864,37 +1849,47 @@ async function readyDispatch({ graph, invocation, routed }, filled) {
1864
1849
  /**
1865
1850
  * Prepares one dispatch ahead of the middleware chain and holds whatever fault it found, so a
1866
1851
  * middleware reads the request before the action runs and a takeover never observes the fault.
1867
- * The phases run in order: local parsing, the input-source stage, and validation. A local fault
1868
- * 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`.
1869
1858
  */
1870
1859
  export async function prepareDispatch(graph, routed, invocation) {
1871
1860
  const result = routed.command.result;
1872
1861
  const local = parseLocal(routed);
1873
1862
  const preparation = { graph, invocation, routed };
1874
1863
  const { globals, locals, sources } = await fillScope(preparation, local);
1875
- const held = (fault) => ({
1876
- fault,
1864
+ const held = (fault, options) => ({
1865
+ fault: local.kind === 'held' ? local.fault : fault,
1877
1866
  globals,
1878
1867
  kind: 'held',
1868
+ options,
1879
1869
  request: null,
1880
1870
  result,
1881
1871
  });
1882
- if (local.kind === 'held') {
1883
- return held(local.fault);
1884
- }
1885
1872
  if (sources.fault) {
1886
- return held(sources.fault);
1873
+ return held(sources.fault, null);
1887
1874
  }
1888
1875
  try {
1889
- const ready = await readyDispatch(preparation, {
1876
+ const validation = await validateInvocation(preparation, {
1890
1877
  local,
1878
+ locals,
1891
1879
  sources,
1892
- values: mergeValues(globals, locals),
1880
+ values: globals,
1893
1881
  });
1894
- 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 };
1895
1890
  }
1896
1891
  catch (error) {
1897
1892
  // The fault is held as a failure, so a throw from reading a validator's output is never offered.
1898
- return held(toFailure(error));
1893
+ return held(toFailure(error), null);
1899
1894
  }
1900
1895
  }