@loomcli/core 0.5.0 → 0.6.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 (77) hide show
  1. package/dist/application.d.ts +18 -2
  2. package/dist/application.js +266 -71
  3. package/dist/bindings.d.ts +15 -10
  4. package/dist/bindings.js +34 -15
  5. package/dist/chain.d.ts +15 -6
  6. package/dist/chain.js +40 -20
  7. package/dist/command-rules.d.ts +55 -0
  8. package/dist/command-rules.js +142 -0
  9. package/dist/command.d.ts +65 -23
  10. package/dist/command.js +695 -226
  11. package/dist/controls.d.ts +8 -0
  12. package/dist/controls.js +23 -0
  13. package/dist/defect.d.ts +18 -0
  14. package/dist/defect.js +272 -0
  15. package/dist/developer.d.ts +24 -0
  16. package/dist/developer.js +52 -0
  17. package/dist/diagnostic-text.d.ts +81 -0
  18. package/dist/diagnostic-text.js +283 -0
  19. package/dist/diagnostic.d.ts +11 -0
  20. package/dist/diagnostic.js +70 -0
  21. package/dist/errors.d.ts +108 -24
  22. package/dist/errors.js +324 -53
  23. package/dist/exit-codes.d.ts +45 -0
  24. package/dist/exit-codes.js +46 -0
  25. package/dist/extension.d.ts +41 -8
  26. package/dist/extension.js +142 -57
  27. package/dist/facts.d.ts +66 -11
  28. package/dist/facts.js +98 -22
  29. package/dist/globals.d.ts +29 -21
  30. package/dist/globals.js +115 -46
  31. package/dist/hints.d.ts +79 -0
  32. package/dist/hints.js +247 -0
  33. package/dist/host.d.ts +13 -0
  34. package/dist/host.js +43 -1
  35. package/dist/identity.d.ts +19 -0
  36. package/dist/identity.js +72 -0
  37. package/dist/index.d.ts +11 -2
  38. package/dist/index.js +5 -0
  39. package/dist/input-rules.d.ts +64 -0
  40. package/dist/input-rules.js +145 -0
  41. package/dist/inspect.d.ts +12 -4
  42. package/dist/inspect.js +88 -28
  43. package/dist/lanes.js +1 -1
  44. package/dist/locate.js +4 -4
  45. package/dist/options.d.ts +23 -2
  46. package/dist/options.js +135 -44
  47. package/dist/output.d.ts +9 -2
  48. package/dist/output.js +18 -2
  49. package/dist/plain.d.ts +6 -0
  50. package/dist/plain.js +12 -0
  51. package/dist/plugin-rules.d.ts +62 -0
  52. package/dist/plugin-rules.js +155 -0
  53. package/dist/plugin.d.ts +22 -10
  54. package/dist/plugin.js +348 -116
  55. package/dist/prototypes.d.ts +7 -0
  56. package/dist/prototypes.js +29 -0
  57. package/dist/rendering.d.ts +6 -1
  58. package/dist/rendering.js +23 -5
  59. package/dist/rules.d.ts +51 -0
  60. package/dist/rules.js +115 -0
  61. package/dist/sequence.js +6 -1
  62. package/dist/sources.d.ts +6 -4
  63. package/dist/sources.js +22 -13
  64. package/dist/style-wire.js +1 -1
  65. package/dist/style.js +1 -1
  66. package/dist/theme.d.ts +4 -0
  67. package/dist/theme.js +25 -5
  68. package/dist/thenable.d.ts +15 -0
  69. package/dist/thenable.js +29 -0
  70. package/dist/translators.d.ts +69 -0
  71. package/dist/translators.js +253 -0
  72. package/dist/types.d.ts +11 -1
  73. package/dist/validation.d.ts +44 -3
  74. package/dist/validation.js +156 -57
  75. package/dist/view.d.ts +48 -14
  76. package/dist/view.js +155 -77
  77. package/package.json +1 -1
package/dist/command.js CHANGED
@@ -1,13 +1,18 @@
1
1
  var _a, _b;
2
2
  import { checkEnvBinding, checkNoArgumentBinding } from './bindings.js';
3
- import { commandSentence, commandSubject, DeclarationError, InternalError, NonCallableCommandError, ResultError, UnexpectedArgumentError, UnknownCommandError, } from './errors.js';
3
+ 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';
4
6
  import { buildExtensions, extendStore, publishStore, registerDescriptor, storeCommandLayers, validateLayer, } from './extension.js';
5
- import { checkDeprecated, checkDescription, checkHidden, checkNoListingFacts, isPlainObject, } from './facts.js';
7
+ import { checkDeprecated, checkDescription, checkHidden, checkNoListingFacts, siteFinding, } from './facts.js';
6
8
  import { buildGlobals, checkLocalOptions } from './globals.js';
7
- import { nodeAt, resultNode, snapshot } from './inspect.js';
8
- import { compileOptions, copyValues, emptyValues, extractGlobals, isOptionToken, mergeValues, parseInputs, } from './options.js';
9
+ 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';
13
+ import { brokenAttachHook, notAnObject } from './plugin-rules.js';
9
14
  import { fillInputs } from './sources.js';
10
- import { captureConfig, checkDeclarations, validateValues } from './validation.js';
15
+ import { captureConfig, checkInputConfig, checkDeclarations, declaringSite, inputPlace, validateValues, } from './validation.js';
11
16
  /**
12
17
  * The deepest level below the root a Command may sit at, and the word its diagnostic spells it with.
13
18
  * A child of the root sits at level 1. Raising the cap relaxes a rule and breaks no application.
@@ -41,13 +46,48 @@ function nodeHandle(name, state) {
41
46
  export function commandNode(value) {
42
47
  return typeof value === 'object' && value !== null ? nodes.get(value) : undefined;
43
48
  }
49
+ /**
50
+ * The path a finding opens with for a declaration call on one Command. The root's is empty. A named
51
+ * Command's call cannot know the parent it will join, so its path is its own name.
52
+ */
53
+ function pathOf(name) {
54
+ return name === null ? [] : [name];
55
+ }
56
+ /** A call's arguments as its author wrote them: the trailing ones left undefined are dropped. */
57
+ export function callArguments(...values) {
58
+ let length = values.length;
59
+ while (length > 0 && values[length - 1] === undefined) {
60
+ length -= 1;
61
+ }
62
+ return values.slice(0, length);
63
+ }
64
+ /** A Command value as a finding prints it, which it would otherwise print as an ellipsis. */
65
+ export function commandCode(name) {
66
+ return spelled(`new Command(${quoteString(name)})`);
67
+ }
68
+ /** The finding for one `command()` call that attached a child under the Command at `path`. */
69
+ export function commandPlacement(path, child) {
70
+ return { arguments: [commandCode(child)], call: 'command', mark: '0', path };
71
+ }
72
+ /** The finding for one `new Command()` call, which carries the name and the options slot. */
73
+ function constructorFinding(name, options, mark) {
74
+ return { arguments: callArguments(name, options), call: 'new Command', mark };
75
+ }
44
76
  /** A named Command's options slot holds a plain options object, and never retired globals wiring. */
45
77
  function checkCommandOptions(name, options) {
46
78
  if (options !== undefined && !isPlainObject(options)) {
47
- throw new DeclarationError(`${commandSentence(name)} options must be an object. Supply a Command options object.`);
79
+ throw new DeclarationError(notAnObject, {
80
+ correction: 'Supply a Command options object.',
81
+ findings: [constructorFinding(name, options, '1')],
82
+ sentence: `${commandSentence(name)} declares options that are not an object.`,
83
+ });
48
84
  }
49
85
  if (isPlainObject(options) && 'globals' in options) {
50
- throw new DeclarationError(`${commandSentence(name)} declares globals. Declare globals on the Application and register its environment.`);
86
+ throw new DeclarationError(commandGlobals, {
87
+ correction: 'Declare globals on the Application and register its environment.',
88
+ findings: [constructorFinding(name, options, '1.globals')],
89
+ sentence: `${commandSentence(name)} declares globals.`,
90
+ });
51
91
  }
52
92
  }
53
93
  /** One name rule for an argument, option, or view name: a bare token the parser can read. */
@@ -66,11 +106,25 @@ export function isPortableName(name) {
66
106
  /** The one correction every portable name diagnostic ends with. */
67
107
  export const portableNameCorrection = 'Use a nonempty name of A-Z, a-z, 0-9, ".", "_", and "-" that does not start with "-" or ".".';
68
108
  /** An alias is typed at the prompt the way a Command name is, so it answers to the portable rule. */
69
- function checkAliasName(command, alias) {
70
- if (!isPortableName(alias)) {
71
- throw new DeclarationError(`${commandSentence(command)} declares an alias named "${String(alias)}". ${portableNameCorrection}`);
109
+ function checkAliasNames(command, names) {
110
+ for (const [index, alias] of names.entries()) {
111
+ if (!isPortableName(alias)) {
112
+ throw new DeclarationError(portableName, {
113
+ correction: portableNameCorrection,
114
+ findings: [{ arguments: names, call: 'alias', mark: String(index), path: pathOf(command) }],
115
+ sentence: `${commandSentence(command)} declares an alias named ${quoted(alias)}.`,
116
+ });
117
+ }
72
118
  }
73
119
  }
120
+ /**
121
+ * The finding for the `alias()` call that declared one alias on the Command at `path`, rebuilt from
122
+ * every alias the Command holds.
123
+ */
124
+ function aliasFinding(command, alias, note) {
125
+ const { aliases, path } = command;
126
+ return { arguments: aliases, call: 'alias', mark: String(aliases.indexOf(alias)), note, path };
127
+ }
74
128
  /** How the extension diagnostics of one Command's own layers name it. */
75
129
  export function layerOf(name) {
76
130
  return { phrase: `on ${commandSubject(name)}`, sentence: commandSentence(name) };
@@ -101,19 +155,28 @@ export function freshState(declaration) {
101
155
  */
102
156
  function namedState(name, options) {
103
157
  if (!isPortableName(name)) {
104
- throw new DeclarationError(`Command name "${String(name)}" is invalid. ${portableNameCorrection}`);
158
+ throw new DeclarationError(portableName, {
159
+ correction: portableNameCorrection,
160
+ findings: [constructorFinding(name, options, '0')],
161
+ sentence: `Command name ${quoted(name)} is invalid.`,
162
+ });
105
163
  }
106
164
  checkCommandOptions(name, options);
107
165
  const slot = isPlainObject(options) ? options : undefined;
108
- const sentence = commandSentence(name);
166
+ const site = {
167
+ at: '1',
168
+ declaration: { arguments: [name, options], call: 'new Command' },
169
+ subject: commandSentence(name),
170
+ };
109
171
  // The facts are read in the order their diagnostics have always ranked.
110
- const description = checkDescription(sentence, slot?.description);
111
- const hidden = checkHidden(sentence, slot?.hidden);
112
- const deprecated = checkDeprecated(sentence, slot?.deprecated);
172
+ const description = checkDescription(site, slot?.description);
173
+ const hidden = checkHidden(site, slot?.hidden);
174
+ const deprecated = checkDeprecated(site, slot?.deprecated);
113
175
  const descriptors = new Map();
114
176
  const extensions = storeCommandLayers({
115
177
  descriptors,
116
178
  layers: [slot?.extensions],
179
+ site: { ...site, at: '1.extensions' },
117
180
  subject: layerOf(name),
118
181
  });
119
182
  return {
@@ -130,16 +193,48 @@ function namedState(name, options) {
130
193
  * A declaration call after `action()` is an order fault the receiver's own earlier call makes
131
194
  * certain. The types remove the call for a TypeScript author; a JavaScript author meets it here.
132
195
  */
133
- function checkOpen(state, clause, remedy) {
196
+ function checkOpen(state, late) {
134
197
  if (state.action !== undefined) {
135
- throw new DeclarationError(`${commandSentence(state.name)} ${clause} after its action. ${remedy}`);
198
+ throw new DeclarationError(declaredAfterAction, {
199
+ correction: late.remedy,
200
+ findings: [
201
+ {
202
+ arguments: late.arguments,
203
+ call: late.call,
204
+ mark: '0',
205
+ note: 'after action()',
206
+ path: pathOf(state.name),
207
+ },
208
+ ],
209
+ sentence: `${commandSentence(state.name)} ${late.clause} after its action.`,
210
+ });
136
211
  }
137
212
  }
138
213
  /** The remedy a late argument or option earns. */
139
214
  const inputRemedy = 'Declare arguments and options before action().';
215
+ /** Where one input was declared: the call on the Command at `path` that declared it. */
216
+ function inputSite(name, path, input) {
217
+ return declaringSite(input, inputPlace(input, { global: false, path }), `${commandSentence(name)} ${input.kind} ${quoted(input.name)}`);
218
+ }
219
+ /** The finding for the `argument()` or `option()` call that declared one input, marking its name. */
220
+ function inputFinding(path, input, note) {
221
+ const site = declaringSite(input, inputPlace(input, { global: false, path }));
222
+ return siteFinding(site, site.named, note);
223
+ }
224
+ /** The scope one Command's own options compile under: its subject, and each option's call. */
225
+ function localScope(name, path) {
226
+ return { siteOf: (input) => inputSite(name, path, input), subject: commandSubject(name) };
227
+ }
140
228
  /** One Command declares arguments or attaches children, whichever call came second. */
141
- function placementFault(name, argument, child) {
142
- return new DeclarationError(`${commandSentence(name)} declares argument "${argument}" and attaches child "${child}". Move the argument into a child Command or remove the children.`);
229
+ function placementFault(parent, argument, child) {
230
+ return new DeclarationError(argumentsBesideChildren, {
231
+ correction: 'Move the argument into a child Command or remove the children.',
232
+ findings: [
233
+ inputFinding(parent.path, argument, 'the argument'),
234
+ { ...child.placement, note: 'the child' },
235
+ ],
236
+ sentence: `${commandSentence(parent.name)} declares argument "${argument.name}" and attaches child "${child.name}".`,
237
+ });
143
238
  }
144
239
  /** The options one declaration list holds, in declaration order. */
145
240
  function optionsOf(inputs) {
@@ -163,6 +258,7 @@ function recordInput(state, input) {
163
258
  const record = buildExtensions({
164
259
  declared: input.config.extensions,
165
260
  descriptors,
261
+ site: { ...inputSite(name, pathOf(name), input), at: '1.extensions' },
166
262
  subject: {
167
263
  phrase: `on ${commandSubject(name)} ${input.kind} "${input.name}"`,
168
264
  sentence: `${commandSentence(name)} ${input.kind} "${input.name}"`,
@@ -177,31 +273,61 @@ function recordInput(state, input) {
177
273
  */
178
274
  function checkArgument(state, input) {
179
275
  const { name } = state;
180
- const subject = commandSubject(name);
181
- if (!isDeclaredName(input.name)) {
182
- throw new DeclarationError(`${commandSentence(name)} declares an argument named "${String(input.name)}". Use a nonempty name without a leading hyphen, whitespace, or "=".`);
183
- }
276
+ const path = pathOf(name);
277
+ const command = { name, path, subject: commandSubject(name) };
184
278
  const declared = state.inputs.filter((entry) => entry.kind === 'argument');
185
- if (declared.some((entry) => entry.name === input.name)) {
186
- throw new DeclarationError(`Argument "${input.name}" is declared more than once on ${subject}. Remove or rename the duplicate.`);
187
- }
279
+ checkArgumentName(command, declared, input);
188
280
  const child = state.children[0];
189
281
  if (child) {
190
- throw placementFault(name, input.name, child.name);
282
+ throw placementFault(command, input, child);
191
283
  }
192
284
  const previous = declared.at(-1);
193
285
  if (previous) {
194
- checkSlotOrder(slotOf(previous), slotOf(input), subject);
286
+ checkSlotOrder(slotOf(previous), slotOf(input), command);
287
+ }
288
+ const site = inputSite(name, path, input);
289
+ checkDescription(site, input.config.description);
290
+ checkNoListingFacts(site, input.config);
291
+ checkNoArgumentBinding(site, input.config);
292
+ checkDeclarations([{ input, site }]);
293
+ }
294
+ /** One argument's name answers the declared-name rule and names no argument declared before it. */
295
+ function checkArgumentName(command, declared, input) {
296
+ const { path } = command;
297
+ if (!isDeclaredName(input.name)) {
298
+ throw new DeclarationError(declaredName, {
299
+ correction: 'Use a nonempty name without a leading hyphen, whitespace, or "=".',
300
+ findings: [inputFinding(path, input)],
301
+ sentence: `${commandSentence(command.name)} declares an argument named ${quoted(input.name)}.`,
302
+ });
303
+ }
304
+ const first = declared.find((entry) => entry.name === input.name);
305
+ if (first) {
306
+ throw new DeclarationError(argumentDeclaredTwice, {
307
+ correction: 'Remove or rename the duplicate.',
308
+ findings: [
309
+ inputFinding(path, first, 'the first declaration'),
310
+ inputFinding(path, input, 'the second declaration'),
311
+ ],
312
+ sentence: `Argument "${input.name}" is declared more than once on ${command.subject}.`,
313
+ });
195
314
  }
196
- const sentence = `${commandSentence(name)} argument "${input.name}"`;
197
- checkDescription(sentence, input.config.description);
198
- checkNoListingFacts(sentence, input.config);
199
- checkNoArgumentBinding(sentence, input.config);
200
- checkDeclarations([input]);
201
315
  }
202
316
  /** The declared value joins `args` under its literal name, typed by its own config. */
203
- export function declareArgument(state, input) {
204
- checkOpen(state, `declares argument "${input.name}"`, inputRemedy);
317
+ export function declareArgument(state, declared) {
318
+ // The call's own input is judged before the receiver's state, as alias() judges its names.
319
+ // 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.
321
+ const { name } = state;
322
+ 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) };
325
+ checkOpen(state, {
326
+ arguments: [input.name, input.config],
327
+ call: 'argument',
328
+ clause: `declares argument ${quoted(input.name)}`,
329
+ remedy: inputRemedy,
330
+ });
205
331
  checkArgument(state, input);
206
332
  const recorded = recordInput(state, input);
207
333
  const previous = state.bind;
@@ -222,16 +348,26 @@ const noGlobals = { names: new Map(), options: new Map(), variables: new Map() }
222
348
  * options also meet the Application's globals table, which a named Command meets when its subtree
223
349
  * joins an Application.
224
350
  */
225
- export function declareOption(state, input, table = noGlobals) {
226
- checkOpen(state, `declares option "${input.name}"`, inputRemedy);
227
- const sentence = `${commandSentence(state.name)} option "${input.name}"`;
228
- checkDescription(sentence, input.config.description);
229
- checkHidden(sentence, input.config.hidden);
230
- checkDeprecated(sentence, input.config.deprecated);
231
- checkEnvBinding(sentence, input.config);
351
+ export function declareOption(state, declared, table = noGlobals) {
352
+ // 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);
358
+ checkOpen(state, {
359
+ arguments: [input.name, input.config],
360
+ call: 'option',
361
+ clause: `declares option ${quoted(input.name)}`,
362
+ remedy: inputRemedy,
363
+ });
364
+ checkDescription(site, input.config.description);
365
+ checkHidden(site, input.config.hidden);
366
+ checkDeprecated(site, input.config.deprecated);
367
+ checkEnvBinding(site, input.config);
232
368
  const recorded = recordInput(state, input);
233
- checkLocalOptions([...optionsOf(state.inputs), input], table, commandSubject(state.name));
234
- checkDeclarations([input]);
369
+ checkLocalOptions([...optionsOf(state.inputs), input], table, localScope(state.name, pathOf(state.name)));
370
+ checkDeclarations([{ input, site }]);
235
371
  const previous = state.bind;
236
372
  return {
237
373
  ...state,
@@ -249,23 +385,40 @@ export function declareOption(state, input, table = noGlobals) {
249
385
  */
250
386
  export function declareAlias(state, names) {
251
387
  const { name } = state;
388
+ const path = pathOf(name);
252
389
  const first = names[0];
253
390
  if (first === undefined) {
254
- throw new DeclarationError(`${commandSentence(name)} declares an alias with no names. Supply at least one name.`);
391
+ throw new DeclarationError(aliasWithoutNames, {
392
+ correction: 'Supply at least one name.',
393
+ findings: [{ arguments: [], call: 'alias', path }],
394
+ sentence: `${commandSentence(name)} declares an alias with no names.`,
395
+ });
255
396
  }
256
397
  // The call's own input is judged before the receiver's state.
257
398
  // A non-string name then reports as an alias name instead of failing to print in the order diagnostic.
258
- for (const alias of names) {
259
- checkAliasName(name, alias);
260
- }
261
- checkOpen(state, `declares alias "${first}"`, 'Declare aliases before action().');
399
+ checkAliasNames(name, names);
400
+ checkOpen(state, {
401
+ arguments: names,
402
+ call: 'alias',
403
+ clause: `declares alias "${first}"`,
404
+ remedy: 'Declare aliases before action().',
405
+ });
262
406
  const aliases = [...state.aliases];
263
- for (const alias of names) {
407
+ for (const [index, alias] of names.entries()) {
408
+ const repeated = { arguments: names, call: 'alias', mark: String(index), path };
264
409
  if (alias === name) {
265
- throw new DeclarationError(`${commandSentence(name)} declares alias "${alias}", which is its own name. Remove the alias.`);
410
+ throw new DeclarationError(repeatedAlias, {
411
+ correction: 'Remove the alias.',
412
+ findings: [{ ...repeated, note: 'its own name' }],
413
+ sentence: `${commandSentence(name)} declares alias "${alias}", which is its own name.`,
414
+ });
266
415
  }
267
416
  if (aliases.includes(alias)) {
268
- throw new DeclarationError(`${commandSentence(name)} declares alias "${alias}" twice. Remove the repeated alias.`);
417
+ throw new DeclarationError(repeatedAlias, {
418
+ correction: 'Remove the repeated alias.',
419
+ findings: [{ ...repeated, note: 'already an alias' }],
420
+ sentence: `${commandSentence(name)} declares alias "${alias}" twice.`,
421
+ });
269
422
  }
270
423
  aliases.push(alias);
271
424
  }
@@ -280,6 +433,7 @@ export function declareExtensions(state, values) {
280
433
  const layer = validateLayer({
281
434
  declared: values,
282
435
  descriptors,
436
+ site: { at: '', declaration: { arguments: values, call: 'extend', path: pathOf(state.name) } },
283
437
  subject: layerOf(state.name),
284
438
  target: 'command',
285
439
  });
@@ -299,7 +453,19 @@ function declaredChannel(out) {
299
453
  */
300
454
  export function declareAction(state, handler) {
301
455
  if (state.action !== undefined) {
302
- throw new DeclarationError(`${commandSentence(state.name)} has multiple actions. Register one action.`);
456
+ throw new DeclarationError(multipleActions, {
457
+ correction: 'Register one action.',
458
+ findings: [
459
+ {
460
+ arguments: [handler],
461
+ call: 'action',
462
+ mark: '0',
463
+ note: 'the second action',
464
+ path: pathOf(state.name),
465
+ },
466
+ ],
467
+ sentence: `${commandSentence(state.name)} has multiple actions.`,
468
+ });
303
469
  }
304
470
  // The graph and the routed node stay getters, because a spread would build the graph on every run.
305
471
  const stored = (context) => handler({
@@ -324,33 +490,71 @@ export function declareAction(state, handler) {
324
490
  * here; the empty record and the default wait for attach, because `views()` can still add keys.
325
491
  */
326
492
  export function declareResult(state, kind, declaration) {
327
- checkOpen(state, 'declares its result', 'Declare result() or rows() before action().');
328
- const results = [...state.results, { kind, views: recordOf(declaration, 'views') }];
329
- mergeResult(state.name, results);
493
+ const call = {
494
+ arguments: callArguments(declaration),
495
+ kind,
496
+ views: recordOf(declaration, 'views'),
497
+ };
498
+ checkOpen(state, {
499
+ arguments: call.arguments,
500
+ call: resultCallName(call),
501
+ clause: 'declares its result',
502
+ remedy: 'Declare result() or rows() before action().',
503
+ });
504
+ const results = [...state.results, call];
505
+ mergeResult({ name: state.name, path: pathOf(state.name) }, results);
330
506
  return { ...state, results };
331
507
  }
332
508
  /** A `views()` call reshapes views and closes nothing, so it is never a late declaration. */
333
509
  export function declareResultViews(state, replacements, options) {
334
- const results = [
335
- ...state.results,
336
- { default: recordOf(options, 'default'), kind: 'views', views: replacements },
337
- ];
338
- mergeResult(state.name, results);
510
+ const results = [...state.results, viewsCall(replacements, options)];
511
+ mergeResult({ name: state.name, path: pathOf(state.name) }, results);
339
512
  return { ...state, results };
340
513
  }
514
+ /** One `views()` call as the results lane records it. */
515
+ function viewsCall(replacements, options) {
516
+ return {
517
+ arguments: callArguments(replacements, options),
518
+ default: recordOf(options, 'default'),
519
+ kind: 'views',
520
+ views: replacements,
521
+ };
522
+ }
341
523
  /** One property of an authoring argument, read defensively: the rules report whatever it holds. */
342
524
  function recordOf(declaration, key) {
343
525
  return isPlainObject(declaration) ? declaration[key] : undefined;
344
526
  }
527
+ /** The authoring call one results-lane call was made through. */
528
+ function resultCallName(call) {
529
+ return call.kind === 'value' ? 'result' : call.kind;
530
+ }
531
+ /** The finding for one results-lane call on the Command at `path`, marking one of its arguments. */
532
+ function resultFinding(path, call, mark) {
533
+ const finding = { arguments: call.arguments, call: resultCallName(call), mark: mark.at, path };
534
+ return mark.note === undefined ? finding : { ...finding, note: mark.note };
535
+ }
536
+ /** Where one views entry sits among its call's arguments: in `views` for a declaration, or first. */
537
+ function entryMark(call, key) {
538
+ return call.kind === 'views' ? `0.${key}` : `0.views.${key}`;
539
+ }
345
540
  /** The parent a declaration is, as attach reads it. */
346
541
  function parentOf(state) {
347
542
  return {
348
- argument: state.inputs.find((input) => input.kind === 'argument')?.name,
543
+ argument: state.inputs.find((input) => input.kind === 'argument'),
349
544
  children: state.children,
350
545
  hasAction: state.action !== undefined,
351
546
  name: state.name,
547
+ path: pathOf(state.name),
352
548
  };
353
549
  }
550
+ /** The path one child sits at under its parent. */
551
+ function childPath(parent, child) {
552
+ return [...parent.path, child];
553
+ }
554
+ /** The call that attached one child, noted with the child's name. */
555
+ function childFinding(entry) {
556
+ return { ...entry.placement, note: `child "${entry.name}"` };
557
+ }
354
558
  /**
355
559
  * One parent's namespace, which every canonical name and alias under it shares. The rule reads the
356
560
  * same whichever sibling the author attached first.
@@ -358,21 +562,41 @@ function parentOf(state) {
358
562
  function checkSiblings(parent, child) {
359
563
  const sentence = commandSentence(parent.name);
360
564
  const { children } = parent;
361
- if (children.some((entry) => entry.name === child.name)) {
362
- throw new DeclarationError(`${sentence} attaches two children named "${child.name}". Rename or remove one.`);
565
+ const aliased = (entry, alias) => aliasFinding({ aliases: entry.node.declared.aliases, path: childPath(parent, entry.name) }, alias, `alias of child "${entry.name}"`);
566
+ const correction = 'Rename or remove one.';
567
+ const twin = children.find((entry) => entry.name === child.name);
568
+ if (twin) {
569
+ throw new DeclarationError(siblingNameTaken, {
570
+ correction,
571
+ findings: [childFinding(twin), childFinding(child)],
572
+ sentence: `${sentence} attaches two children named "${child.name}".`,
573
+ });
363
574
  }
364
575
  for (const alias of child.node.declared.aliases) {
365
- if (children.some((entry) => entry.name === alias)) {
366
- throw new DeclarationError(`${sentence} attaches child "${child.name}" with alias "${alias}", which is also the name of child "${alias}". Rename or remove one.`);
576
+ const holder = children.find((entry) => entry.name === alias);
577
+ if (holder) {
578
+ throw new DeclarationError(siblingNameTaken, {
579
+ correction,
580
+ findings: [childFinding(holder), aliased(child, alias)],
581
+ sentence: `${sentence} attaches child "${child.name}" with alias "${alias}", which is also the name of child "${alias}".`,
582
+ });
367
583
  }
368
584
  const owner = children.find((entry) => entry.node.declared.aliases.includes(alias));
369
585
  if (owner) {
370
- throw new DeclarationError(`${sentence} attaches child "${child.name}" with alias "${alias}", which is also an alias of child "${owner.name}". Rename or remove one.`);
586
+ throw new DeclarationError(siblingNameTaken, {
587
+ correction,
588
+ findings: [aliased(owner, alias), aliased(child, alias)],
589
+ sentence: `${sentence} attaches child "${child.name}" with alias "${alias}", which is also an alias of child "${owner.name}".`,
590
+ });
371
591
  }
372
592
  }
373
- const aliased = children.find((entry) => entry.node.declared.aliases.includes(child.name));
374
- if (aliased) {
375
- throw new DeclarationError(`${sentence} attaches child "${aliased.name}" with alias "${child.name}", which is also the name of child "${child.name}". Rename or remove one.`);
593
+ const owner = children.find((entry) => entry.node.declared.aliases.includes(child.name));
594
+ if (owner) {
595
+ throw new DeclarationError(siblingNameTaken, {
596
+ correction,
597
+ findings: [aliased(owner, child.name), childFinding(child)],
598
+ sentence: `${sentence} attaches child "${owner.name}" with alias "${child.name}", which is also the name of child "${child.name}".`,
599
+ });
376
600
  }
377
601
  }
378
602
  /**
@@ -381,7 +605,11 @@ function checkSiblings(parent, child) {
381
605
  */
382
606
  function checkNesting(parent, child, level) {
383
607
  if (level >= nestingCap.depth && child.node.declared.children.length > 0) {
384
- throw new DeclarationError(`${commandSentence(parent)} attaches child "${child.name}", which has children of its own. Nest Commands at most ${nestingCap.words} levels below the root.`);
608
+ throw new DeclarationError(nestingDepth, {
609
+ correction: `Nest Commands at most ${nestingCap.words} levels below the root.`,
610
+ findings: [{ ...child.placement, note: 'has children of its own' }],
611
+ sentence: `${commandSentence(parent)} attaches child "${child.name}", which has children of its own.`,
612
+ });
385
613
  }
386
614
  }
387
615
  /**
@@ -389,14 +617,23 @@ function checkNesting(parent, child, level) {
389
617
  * children. A group with no children receives an invocation no handler can answer, and a local
390
618
  * option on a group reaches no handler either, because locals never inherit.
391
619
  */
392
- function checkGroup(state) {
620
+ function checkGroup(state, place) {
393
621
  const { name } = state;
394
622
  if (state.children.length === 0) {
395
- throw new DeclarationError(`${commandSentence(name)} has no action. Register an action.`);
623
+ const { placement } = place;
624
+ throw new DeclarationError(commandWithoutAction, {
625
+ correction: 'Register an action.',
626
+ findings: placement === undefined ? [] : [{ ...placement, note: 'no action and no children' }],
627
+ sentence: `${commandSentence(name)} has no action.`,
628
+ });
396
629
  }
397
630
  const option = state.inputs.find((input) => input.kind === 'option');
398
631
  if (option) {
399
- throw new DeclarationError(`${commandSentence(name)} declares option "${option.name}" but registers no action to receive it. Register an action or remove the option.`);
632
+ throw new DeclarationError(groupOption, {
633
+ correction: 'Register an action or remove the option.',
634
+ findings: [inputFinding(place.path, option, 'no action reads it')],
635
+ sentence: `${commandSentence(name)} declares option "${option.name}" but registers no action to receive it.`,
636
+ });
400
637
  }
401
638
  }
402
639
  /**
@@ -404,10 +641,10 @@ function checkGroup(state) {
404
641
  * result needs an action, its merged views record names at least one view and its default, and a
405
642
  * Command without an action is a group. It answers with the resolved result.
406
643
  */
407
- function checkFinished(declared, hasAction) {
408
- const result = buildResult(declared, hasAction);
644
+ function checkFinished(declared, hasAction, place) {
645
+ const result = buildResult(declared, hasAction, place.path);
409
646
  if (!hasAction) {
410
- checkGroup(declared);
647
+ checkGroup(declared, place);
411
648
  }
412
649
  return result;
413
650
  }
@@ -415,18 +652,23 @@ function checkFinished(declared, hasAction) {
415
652
  * The one attach operation `Command.command()`, `Application.command()`, and a plugin's
416
653
  * `commands` list share. A Command is an immutable value, so the child is final here: it is checked
417
654
  * as a finished Command, against the parent's current children, and against the nesting cap from
418
- * the shallowest level the parent can sit at.
655
+ * the shallowest level the parent can sit at. The placement is the call that attached it, which a
656
+ * finding about the child as a whole marks.
419
657
  */
420
- export function attach(parent, node) {
658
+ export function attach(parent, node, placement) {
421
659
  const { name } = node;
660
+ const child = { name, node, placement };
422
661
  if (parent.hasAction) {
423
- throw new DeclarationError(`${commandSentence(parent.name)} attaches child "${name}" after its action. Attach children before action().`);
662
+ throw new DeclarationError(declaredAfterAction, {
663
+ correction: 'Attach children before action().',
664
+ findings: [{ ...placement, note: 'after action()' }],
665
+ sentence: `${commandSentence(parent.name)} attaches child "${name}" after its action.`,
666
+ });
424
667
  }
425
668
  if (parent.argument !== undefined) {
426
- throw placementFault(parent.name, parent.argument, name);
669
+ throw placementFault(parent, parent.argument, child);
427
670
  }
428
- checkFinished(node.declared, node.hasAction);
429
- const child = { name, node };
671
+ checkFinished(node.declared, node.hasAction, { path: childPath(parent, name), placement });
430
672
  checkSiblings(parent, child);
431
673
  checkNesting(parent.name, child, parentLevel(parent.name) + 1);
432
674
  return child;
@@ -435,13 +677,18 @@ export function attach(parent, node) {
435
677
  export function childNode(parent, child) {
436
678
  const node = commandNode(child);
437
679
  if (!node) {
438
- throw new DeclarationError(`${commandSentence(parent)} attaches a value that is not a Command. Attach the value returned by new Command(name).`);
680
+ throw new DeclarationError(notACommand, {
681
+ correction: 'Attach the value returned by new Command(name).',
682
+ findings: [{ arguments: [child], call: 'command', mark: '0', path: pathOf(parent) }],
683
+ sentence: `${commandSentence(parent)} attaches a value that is not a Command.`,
684
+ });
439
685
  }
440
686
  return node;
441
687
  }
442
688
  /** Attaching is a declaration call too, so the receiver keeps the children it already had. */
443
689
  function attachChild(state, child) {
444
- const attached = attach(parentOf(state), childNode(state.name, child));
690
+ const node = childNode(state.name, child);
691
+ const attached = attach(parentOf(state), node, commandPlacement(pathOf(state.name), node.name));
445
692
  return { ...state, children: [...state.children, attached] };
446
693
  }
447
694
  /**
@@ -453,34 +700,47 @@ function attachChild(state, child) {
453
700
  * here fires only once the cap rises: attach reads one level, and only this walk knows each level.
454
701
  */
455
702
  function joinSubtree(scope, child, { level, parent }) {
456
- const { name, node } = child;
457
- checkNesting(parent, child, level);
458
- if (scope.owners.has(node)) {
459
- throw new DeclarationError(`${commandSentence(parent)} attaches child "${name}", which ${commandSubject(scope.owners.get(node) ?? null)} also attaches. Attach a Command value at one point; create a new Command for each placement.`);
703
+ const { name, node, placement } = child;
704
+ checkNesting(parent.name, child, level);
705
+ const owner = scope.owners.get(node);
706
+ if (owner) {
707
+ throw new DeclarationError(commandAttachedTwice, {
708
+ correction: 'Attach a Command value at one point; create a new Command for each placement.',
709
+ findings: [
710
+ { ...owner.placement, note: 'the first placement' },
711
+ { ...placement, note: 'the second placement' },
712
+ ],
713
+ sentence: `${commandSentence(parent.name)} attaches child "${name}", which ${commandSubject(owner.name)} also attaches.`,
714
+ });
460
715
  }
461
- scope.owners.set(node, parent);
462
- for (const descriptor of node.declared.descriptors.values()) {
463
- registerDescriptor(scope.descriptors, descriptor);
716
+ scope.owners.set(node, { name: parent.name, placement });
717
+ for (const [key, descriptor] of node.declared.descriptors) {
718
+ registerDescriptor(scope.descriptors, descriptor, { identity: key });
464
719
  }
465
- checkLocalOptions(optionsOf(node.declared.inputs), scope.table, commandSubject(name));
720
+ const path = childPath(parent, name);
721
+ checkLocalOptions(optionsOf(node.declared.inputs), scope.table, localScope(name, path));
466
722
  for (const entry of node.declared.children) {
467
- joinSubtree(scope, entry, { level: level + 1, parent: name });
723
+ // A nested child's own placement knew its parent alone, so the walk places it under the root.
724
+ const rooted = { ...entry, placement: commandPlacement(path, entry.name) };
725
+ joinSubtree(scope, rooted, { level: level + 1, parent: { name, path } });
468
726
  }
469
727
  }
470
728
  /**
471
729
  * Attaches one child to an Application's root and walks the subtree it brings, once. The scope's
472
730
  * registers are copies: the caller commits the owners, and the returned root holds the descriptors.
473
731
  */
474
- export function attachToRoot(state, node, scope) {
475
- const attached = attach(parentOf(state), node);
476
- joinSubtree(scope, attached, { level: parentLevel(state.name) + 1, parent: state.name });
732
+ export function attachToRoot(state, child, scope) {
733
+ const parent = parentOf(state);
734
+ const attached = attach(parent, child.node, child.placement);
735
+ joinSubtree(scope, attached, { level: parentLevel(state.name) + 1, parent });
477
736
  return { ...state, children: [...state.children, attached], descriptors: scope.descriptors };
478
737
  }
479
738
  /** Every attached Command's local options against a globals table that has just grown. */
480
- function checkAttachedOptions(table, children) {
739
+ function checkAttachedOptions(table, parent, children) {
481
740
  for (const { name, node } of children) {
482
- checkLocalOptions(optionsOf(node.declared.inputs), table, commandSubject(name));
483
- checkAttachedOptions(table, node.declared.children);
741
+ const path = childPath(parent, name);
742
+ checkLocalOptions(optionsOf(node.declared.inputs), table, localScope(name, path));
743
+ checkAttachedOptions(table, { name, path }, node.declared.children);
484
744
  }
485
745
  }
486
746
  /**
@@ -488,42 +748,54 @@ function checkAttachedOptions(table, children) {
488
748
  * grown. The table's new option reads as the other side of any collision.
489
749
  */
490
750
  export function checkDeclaredOptions(state, table) {
491
- checkLocalOptions(optionsOf(state.inputs), table, commandSubject(state.name));
492
- checkAttachedOptions(table, state.children);
751
+ const place = { name: state.name, path: pathOf(state.name) };
752
+ checkLocalOptions(optionsOf(state.inputs), table, localScope(place.name, place.path));
753
+ checkAttachedOptions(table, place, state.children);
493
754
  }
494
755
  /** A variadic or optional slot ends the positional list, so nothing may follow either one. */
495
- function checkSlotOrder(slot, next, subject) {
756
+ function checkSlotOrder(slot, next, command) {
757
+ const { path, subject } = command;
758
+ const after = inputFinding(path, next.input, 'the argument after it');
496
759
  if (slot.variadic) {
497
- throw new DeclarationError(`Argument "${slot.input.name}" is variadic and precedes argument "${next.input.name}" on ${subject}. Declare the variadic argument last.`);
760
+ throw new DeclarationError(variadicArgumentLast, {
761
+ correction: 'Declare the variadic argument last.',
762
+ findings: [inputFinding(path, slot.input, 'the variadic argument'), after],
763
+ sentence: `Argument "${slot.input.name}" is variadic and precedes argument "${next.input.name}" on ${subject}.`,
764
+ });
498
765
  }
499
766
  if (!slot.required) {
500
- throw new DeclarationError(next.required
501
- ? `Argument "${slot.input.name}" is optional and precedes required argument "${next.input.name}" on ${subject}. Declare optional arguments after required ones.`
502
- : `Argument "${next.input.name}" follows optional argument "${slot.input.name}" on ${subject}. Declare an optional argument last.`);
767
+ const findings = [inputFinding(path, slot.input, 'the optional argument'), after];
768
+ throw new DeclarationError(optionalArgumentLast, next.required
769
+ ? {
770
+ correction: 'Declare optional arguments after required ones.',
771
+ findings,
772
+ sentence: `Argument "${slot.input.name}" is optional and precedes required argument "${next.input.name}" on ${subject}.`,
773
+ }
774
+ : {
775
+ correction: 'Declare an optional argument last.',
776
+ findings,
777
+ sentence: `Argument "${next.input.name}" follows optional argument "${slot.input.name}" on ${subject}.`,
778
+ });
503
779
  }
504
780
  }
505
781
  /**
506
782
  * The positional slots one declaration holds. Every authored argument answered these rules at its
507
783
  * own call, so they throw here only for an argument a lifecycle hook declared.
508
784
  */
509
- function collectArguments(state, subject) {
785
+ function collectArguments(state, command) {
510
786
  const slots = [];
511
- const seen = new Set();
787
+ const declared = [];
788
+ const judged = { ...command, subject: commandSubject(command.name) };
512
789
  for (const input of state.inputs.filter((entry) => entry.kind === 'argument')) {
513
- if (!isDeclaredName(input.name)) {
514
- throw new DeclarationError(`${commandSentence(state.name)} declares an argument named "${String(input.name)}". Use a nonempty name without a leading hyphen, whitespace, or "=".`);
515
- }
516
- if (seen.has(input.name)) {
517
- throw new DeclarationError(`Argument "${input.name}" is declared more than once on ${subject}. Remove or rename the duplicate.`);
518
- }
519
- seen.add(input.name);
790
+ checkArgumentName(judged, declared, input);
791
+ declared.push(input);
520
792
  slots.push(slotOf(input));
521
793
  }
522
794
  for (let index = 0; index + 1 < slots.length; index += 1) {
523
795
  const slot = slots[index];
524
796
  const next = slots[index + 1];
525
797
  if (slot && next) {
526
- checkSlotOrder(slot, next, subject);
798
+ checkSlotOrder(slot, next, judged);
527
799
  }
528
800
  }
529
801
  return slots;
@@ -564,23 +836,36 @@ function recordEntries(record) {
564
836
  * One `views` entry under the unit its declaration named, read back as the shape its own functions
565
837
  * name. The two shapes are exclusive, and a row view answers a rows declaration alone.
566
838
  */
567
- function resultView(declaration, name, entry) {
568
- const { kind, sentence } = declaration;
839
+ function resultView(kind, site, entry) {
840
+ const { call, key, path, sentence } = site;
841
+ const findings = [resultFinding(path, call, { at: entryMark(call, key) })];
569
842
  const whole = isWholeView(entry);
570
843
  const row = isRowView(entry);
571
844
  if (whole && row) {
572
- throw new DeclarationError(`${sentence} names view "${name}" with render and row. Supply one of the two.`);
845
+ throw new DeclarationError(viewShape, {
846
+ correction: 'Supply one of the two.',
847
+ findings,
848
+ sentence: `${sentence} names view ${quoted(key)} with render and row.`,
849
+ });
573
850
  }
574
851
  if (row) {
575
852
  if (kind === 'value') {
576
- throw new DeclarationError(`${sentence} names row view "${name}" on a value result. Supply a view with render, or declare the result with rows().`);
853
+ throw new DeclarationError(rowViewOnValue, {
854
+ correction: 'Supply a view with render, or declare the result with rows().',
855
+ findings,
856
+ sentence: `${sentence} names row view ${quoted(key)} on a value result.`,
857
+ });
577
858
  }
578
859
  return entry;
579
860
  }
580
861
  if (whole) {
581
862
  return entry;
582
863
  }
583
- throw new DeclarationError(`${sentence} names view "${name}" with a value that is not a view. Supply a view with render or a row view with row.`);
864
+ throw new DeclarationError(viewShape, {
865
+ correction: 'Supply a view with render or a row view with row.',
866
+ findings,
867
+ sentence: `${sentence} names view ${quoted(key)} with a value that is not a view.`,
868
+ });
584
869
  }
585
870
  /**
586
871
  * Every rule a results-lane call can judge on its own, applied to one Command's calls in the order
@@ -588,18 +873,31 @@ function resultView(declaration, name, entry) {
588
873
  * place and appends a new one, so each name keeps the position the call that first named it gave
589
874
  * it. A `default` once named persists through later calls that name none.
590
875
  */
591
- function mergeResult(name, results) {
592
- const sentence = commandSentence(name);
876
+ function mergeResult(command, results) {
877
+ const { path } = command;
878
+ const sentence = commandSentence(command.name);
593
879
  const declarations = results.filter((call) => call.kind !== 'views');
594
- if (declarations.length > 1) {
595
- throw new DeclarationError(`${sentence} declares two results. Declare one result() or rows() call.`);
880
+ const [declaration, second] = declarations;
881
+ if (declaration && second) {
882
+ throw new DeclarationError(multipleResults, {
883
+ correction: 'Declare one result() or rows() call.',
884
+ findings: [
885
+ resultFinding(path, declaration, { at: '0', note: 'the first result' }),
886
+ resultFinding(path, second, { at: '0', note: 'the second result' }),
887
+ ],
888
+ sentence: `${sentence} declares two results.`,
889
+ });
596
890
  }
597
- const declaration = declarations[0];
598
891
  // A `views()` call reshapes a result's views, so one with no result reshapes nothing.
599
892
  // The types publish the call where a result is carried, so this reaches a JavaScript author.
600
893
  if (!declaration) {
601
- if (results.length > 0) {
602
- throw new DeclarationError(`${sentence} reshapes its views and declares no result. Declare result() or rows() before action().`);
894
+ const [reshape] = results;
895
+ if (reshape) {
896
+ throw new DeclarationError(viewsWithoutResult, {
897
+ correction: 'Declare result() or rows() before action().',
898
+ findings: [resultFinding(path, reshape, { at: '0' })],
899
+ sentence: `${sentence} reshapes its views and declares no result.`,
900
+ });
603
901
  }
604
902
  return undefined;
605
903
  }
@@ -607,40 +905,57 @@ function mergeResult(name, results) {
607
905
  let selected = undefined;
608
906
  for (const call of results) {
609
907
  for (const [key, entry] of recordEntries(call.views)) {
610
- const view = resultView({ kind: declaration.kind, sentence }, key, entry);
908
+ const view = resultView(declaration.kind, { call, key, path, sentence }, entry);
611
909
  if (!isViewName(key)) {
612
- throw new DeclarationError(`${sentence} names view "${key}". Use a nonempty name without whitespace, a leading hyphen, or "=", and not a number.`);
910
+ throw new DeclarationError(viewName, {
911
+ correction: 'Use a nonempty name without whitespace, a leading hyphen, or "=", and not a number.',
912
+ findings: [resultFinding(path, call, { at: entryMark(call, key) })],
913
+ sentence: `${sentence} names view ${quoted(key)}.`,
914
+ });
613
915
  }
614
916
  views.set(key, view);
615
917
  }
616
- if (call.kind === 'views') {
617
- selected = selectedKey(call.default) ?? selected;
918
+ const key = call.kind === 'views' ? selectedKey(call.default) : undefined;
919
+ if (key !== undefined) {
920
+ selected = { call, key };
618
921
  }
619
922
  }
620
- return { kind: declaration.kind, selected, views };
923
+ return { declaration, kind: declaration.kind, selected, views };
621
924
  }
622
925
  /**
623
926
  * The finished result: the merged record, which needs an action, at least one view, and a default
624
927
  * that names one of them. The first key answers until one is named.
625
928
  */
626
- function buildResult(declared, hasAction) {
627
- const merged = mergeResult(declared.name, declared.results);
929
+ function buildResult(declared, hasAction, path) {
930
+ const merged = mergeResult({ name: declared.name, path }, declared.results);
628
931
  if (!merged) {
629
932
  return undefined;
630
933
  }
631
934
  const sentence = commandSentence(declared.name);
935
+ const { declaration, selected, views } = merged;
632
936
  if (!hasAction) {
633
- throw new DeclarationError(`${sentence} declares a result and no action. Register an action or remove the result.`);
937
+ throw new DeclarationError(resultWithoutAction, {
938
+ correction: 'Register an action or remove the result.',
939
+ findings: [resultFinding(path, declaration, { at: '0' })],
940
+ sentence: `${sentence} declares a result and no action.`,
941
+ });
634
942
  }
635
- const { selected, views } = merged;
636
943
  const first = views.keys().next();
637
944
  if (first.done === true) {
638
- throw new DeclarationError(`${sentence} declares a result with no views. Name at least one view.`);
945
+ throw new DeclarationError(resultWithoutViews, {
946
+ correction: 'Name at least one view.',
947
+ findings: [resultFinding(path, declaration, { at: '0.views' })],
948
+ sentence: `${sentence} declares a result with no views.`,
949
+ });
639
950
  }
640
- if (selected !== undefined && !views.has(selected)) {
641
- throw new DeclarationError(`${sentence} selects default view "${selected}", which it does not name. Name the view or select a named one.`);
951
+ if (selected !== undefined && !views.has(selected.key)) {
952
+ throw new DeclarationError(unknownDefaultView, {
953
+ correction: 'Name the view or select a named one.',
954
+ findings: [resultFinding(path, selected.call, { at: '1.default' })],
955
+ sentence: `${sentence} selects default view ${quoted(selected.key)}, which it does not name.`,
956
+ });
642
957
  }
643
- return { default: selected ?? first.value, kind: merged.kind, views };
958
+ return { default: selected?.key ?? first.value, kind: merged.kind, views };
644
959
  }
645
960
  /** The values one build made, so a value a hook returns is one of them and never a forged shape. */
646
961
  const attachments = new WeakMap();
@@ -663,7 +978,7 @@ class AttachedCommandValue {
663
978
  #result;
664
979
  constructor(state) {
665
980
  this.#state = state;
666
- this.#result = resultNode(buildResult(state.declared, state.hasAction));
981
+ this.#result = resultNode(buildResult(state.declared, state.hasAction, state.path));
667
982
  attachments.set(this, state);
668
983
  Object.freeze(this);
669
984
  }
@@ -690,17 +1005,13 @@ class AttachedCommandValue {
690
1005
  return publishStore(this.#state.extensions);
691
1006
  }
692
1007
  argument(name, config) {
693
- return this.#declare({ config: captureConfig(config), kind: 'argument', name });
1008
+ return this.#declare({ config, kind: 'argument', name });
694
1009
  }
695
1010
  option(name, config) {
696
- return this.#declare({ config: captureConfig(config), kind: 'option', name });
1011
+ return this.#declare({ config, kind: 'option', name });
697
1012
  }
698
1013
  views(replacements, options) {
699
- const call = {
700
- default: recordOf(options, 'default'),
701
- kind: 'views',
702
- views: replacements,
703
- };
1014
+ const call = viewsCall(replacements, options);
704
1015
  const { declared } = this.#state;
705
1016
  return this.#derive({ call, kind: 'views' }, { ...declared, results: [...declared.results, call] });
706
1017
  }
@@ -714,6 +1025,7 @@ class AttachedCommandValue {
714
1025
  const layer = validateLayer({
715
1026
  declared: values,
716
1027
  descriptors: registry,
1028
+ site: { at: '', declaration: { arguments: values, call: 'extend', path: state.path } },
717
1029
  subject: state.subject,
718
1030
  target: 'command',
719
1031
  });
@@ -724,8 +1036,22 @@ class AttachedCommandValue {
724
1036
  });
725
1037
  }
726
1038
  /** One input the running hook declared, which the Command's own names now hold. */
727
- #declare(input) {
1039
+ #declare(raw) {
728
1040
  const { declared, identity } = this.#state;
1041
+ const { path } = this.#state;
1042
+ // The name is judged before the config, as Command and Application judge it.
1043
+ if (raw.kind === 'argument') {
1044
+ const { name } = declared;
1045
+ checkArgumentName({ name, path, subject: commandSubject(name) }, [], raw);
1046
+ }
1047
+ else {
1048
+ checkOptionName(raw.name, inputSite(declared.name, path, raw));
1049
+ }
1050
+ checkInputConfig(raw, { call: raw.kind, path });
1051
+ // Each kind captures its own config, so the input keeps the pairing its kind declares.
1052
+ const input = raw.kind === 'argument'
1053
+ ? { ...raw, config: captureConfig(raw.config) }
1054
+ : { ...raw, config: captureConfig(raw.config) };
729
1055
  return this.#derive({ identity, input, kind: 'input' }, { ...declared, inputs: [...declared.inputs, input] });
730
1056
  }
731
1057
  #derive(call, declared) {
@@ -734,9 +1060,13 @@ class AttachedCommandValue {
734
1060
  }
735
1061
  }
736
1062
  _a = AttachedCommandValue;
737
- /** The reason one failed hook reports, which is the thrown value's own message. */
738
- function attachReason(error) {
739
- return error instanceof Error ? error.message : String(error);
1063
+ /** The finding for the hook one plugin declared, as `plugin(identity, { onCommandAttach })`. */
1064
+ function hookFinding(identity, hook) {
1065
+ return {
1066
+ arguments: [identity, { onCommandAttach: hook }],
1067
+ call: 'plugin',
1068
+ mark: '1.onCommandAttach',
1069
+ };
740
1070
  }
741
1071
  /** One hook's own call, whose failure is the plugin's, and whose own report is its own. */
742
1072
  function callHook(value, hook, named) {
@@ -748,7 +1078,11 @@ function callHook(value, hook, named) {
748
1078
  if (error instanceof DeclarationError) {
749
1079
  throw error;
750
1080
  }
751
- throw new DeclarationError(`Plugin "${named.identity}" failed in onCommandAttach for ${named.subject}: ${attachReason(error)}.`);
1081
+ throw new DeclarationError(brokenAttachHook, {
1082
+ correction: 'Return the value the hook received or a value derived from it, and throw only a DeclarationError from the hook.',
1083
+ findings: [hookFinding(named.identity, hook)],
1084
+ sentence: `Plugin ${quoted(named.identity)} failed in onCommandAttach for ${named.subject}: ${asSentence(reasonOf(error))}`,
1085
+ }, { cause: error });
752
1086
  }
753
1087
  }
754
1088
  /**
@@ -759,7 +1093,11 @@ function callHook(value, hook, named) {
759
1093
  function attachOnce(value, hook, named) {
760
1094
  const state = attachments.get(callHook(value, hook, named));
761
1095
  if (!state || state.lineage !== named.lineage) {
762
- throw new DeclarationError(`Plugin "${named.identity}" returned a value that is not the attached Command from onCommandAttach for ${named.subject}. Return the value it received or a value derived from it.`);
1096
+ throw new DeclarationError(brokenAttachHook, {
1097
+ correction: 'Return the value it received or a value derived from it.',
1098
+ findings: [hookFinding(named.identity, hook)],
1099
+ sentence: `Plugin ${quoted(named.identity)} returned a value that is not the attached Command from onCommandAttach for ${named.subject}.`,
1100
+ });
763
1101
  }
764
1102
  return state;
765
1103
  }
@@ -817,10 +1155,28 @@ function applyAttachCalls(state, calls) {
817
1155
  }
818
1156
  return next;
819
1157
  }
1158
+ /**
1159
+ * The reason rule one name collision breaks.
1160
+ * Two options or two arguments break the rule the application's own collision of the pair breaks.
1161
+ * An argument and an option under one name break name-shared-across-kinds, which only a hook reaches.
1162
+ */
1163
+ function nameCollisionRule(declared, held) {
1164
+ if (declared !== held) {
1165
+ return nameSharedAcrossKinds;
1166
+ }
1167
+ return declared === 'option' ? optionDeclaredTwice : argumentDeclaredTwice;
1168
+ }
1169
+ /** The note a finding for one hook-declared input carries. */
1170
+ function hookNote(identity) {
1171
+ return `declared by plugin ${quoted(identity)}`;
1172
+ }
820
1173
  /** The clause and the remedy an input another plugin's hook already declared earns. */
821
- function hookClause(kind, identity) {
1174
+ function hookClause(earlier, path) {
1175
+ const { identity, input } = earlier;
822
1176
  return {
823
- clause: `an ${kind} plugin "${identity}" declared through onCommandAttach`,
1177
+ clause: `an ${input.kind} plugin ${quoted(identity)} declared through onCommandAttach`,
1178
+ held: inputFinding(path, input, hookNote(identity)),
1179
+ kind: input.kind,
824
1180
  remedy: 'Install one of them.',
825
1181
  };
826
1182
  }
@@ -842,6 +1198,25 @@ function attachedRemedy(target) {
842
1198
  }
843
1199
  return 'Install one of them.';
844
1200
  }
1201
+ /** The collision with one option the globals table holds, a global or a plugin's own option. */
1202
+ function tableCollision(entry) {
1203
+ const { owner, site } = entry;
1204
+ if (owner.kind === 'plugin') {
1205
+ const clause = `an option of plugin ${quoted(owner.identity)}`;
1206
+ return {
1207
+ clause,
1208
+ held: siteFinding(site, site.named, clause),
1209
+ kind: 'option',
1210
+ remedy: attachedRemedy('plugin'),
1211
+ };
1212
+ }
1213
+ return {
1214
+ clause: 'a global option',
1215
+ held: siteFinding(site, site.named, 'the global option'),
1216
+ kind: 'option',
1217
+ remedy: attachedRemedy('global'),
1218
+ };
1219
+ }
845
1220
  /**
846
1221
  * What one name a hook-declared input of either kind collides with, in the order the scopes are
847
1222
  * reported: an earlier hook's option, an earlier hook's argument, a local option, a global or
@@ -850,32 +1225,36 @@ function attachedRemedy(target) {
850
1225
  */
851
1226
  function attachedCollision(input, held) {
852
1227
  const { name } = input;
853
- const hook = held.hooks.get(name);
1228
+ const { path } = held;
1229
+ const hook = held.hooks.get(name) ?? held.hookArguments.get(name);
854
1230
  if (hook !== undefined) {
855
- return hookClause('option', hook);
1231
+ return hookClause(hook, path);
856
1232
  }
857
- const hooked = held.hookArguments.get(name);
858
- if (hooked !== undefined) {
859
- return hookClause('argument', hooked);
860
- }
861
- if (held.locals.has(name)) {
862
- return { clause: 'a local option', remedy: attachedRemedy('local') };
1233
+ const local = held.locals.get(name);
1234
+ if (local !== undefined) {
1235
+ return {
1236
+ clause: 'a local option',
1237
+ held: inputFinding(path, local, 'the local option'),
1238
+ kind: 'option',
1239
+ remedy: attachedRemedy('local'),
1240
+ };
863
1241
  }
864
- const claimed = held.globals.names.get(name);
865
- if (claimed) {
866
- const target = claimed.kind === 'plugin' ? 'plugin' : 'global';
867
- const clause = claimed.kind === 'plugin' ? `an option of plugin "${claimed.identity}"` : 'a global option';
868
- return { clause, remedy: attachedRemedy(target) };
1242
+ const entry = held.globals.names.get(name);
1243
+ if (entry) {
1244
+ return tableCollision(entry);
869
1245
  }
870
- return held.arguments.has(name)
871
- ? { clause: 'an argument', remedy: attachedRemedy('argument') }
872
- : undefined;
1246
+ const argument = held.arguments.get(name);
1247
+ return argument === undefined
1248
+ ? undefined
1249
+ : {
1250
+ clause: 'an argument',
1251
+ held: inputFinding(path, argument, 'the argument'),
1252
+ kind: 'argument',
1253
+ remedy: attachedRemedy('argument'),
1254
+ };
873
1255
  }
874
- /**
875
- * The spellings one compiled table holds, each under the form its own option is named by, which is
876
- * its long form where it declares one and the colliding spelling itself where it declares none.
877
- */
878
- function readSpellings(table, claimed) {
1256
+ /** The spellings one compiled table holds, each with its form and the place that declared it. */
1257
+ function readSpellings(table, claimed, placeOf) {
879
1258
  const longs = new Map();
880
1259
  for (const [spelling, option] of table) {
881
1260
  if (option.role === 'long') {
@@ -884,53 +1263,107 @@ function readSpellings(table, claimed) {
884
1263
  }
885
1264
  for (const [spelling, option] of table) {
886
1265
  if (!claimed.has(spelling)) {
887
- claimed.set(spelling, longs.get(option.name) ?? spelling);
1266
+ const form = longs.get(option.name) ?? spelling;
1267
+ claimed.set(spelling, { finding: placeOf(option.name, option.role), form });
888
1268
  }
889
1269
  }
890
1270
  }
1271
+ /** 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);
1274
+ }
891
1275
  /** One hook-declared option's spellings, against every spelling the table already claims. */
892
1276
  function checkAttachedSpelling(declared, named) {
893
- const { claimed, subject } = named;
894
- const table = compileOptions([declared.input], subject);
895
- for (const [spelling] of table) {
1277
+ const { claimed, scope } = named;
1278
+ const { identity, input } = declared;
1279
+ const { subject } = scope;
1280
+ const site = scope.siteOf(input);
1281
+ const table = compileOptions([input], scope);
1282
+ for (const [spelling, option] of table) {
896
1283
  const used = claimed.get(spelling);
897
1284
  if (used !== undefined) {
898
- throw new DeclarationError(`Plugin "${declared.identity}" declares option "${declared.input.name}" with spelling "${spelling}" on ${subject}, which "${used}" already uses.`);
1285
+ throw new DeclarationError(spellingTaken, {
1286
+ correction: 'Change one of the two spellings or omit the plugin.',
1287
+ findings: [
1288
+ spellingPlace(site, option.role, hookNote(identity)),
1289
+ ...(used.finding === undefined ? [] : [used.finding]),
1290
+ ],
1291
+ sentence: `Plugin ${quoted(identity)} declares option ${quoted(input.name)} with spelling ${quoted(spelling)} on ${subject}, which ${quoted(used.form)} already uses.`,
1292
+ });
899
1293
  }
900
1294
  }
901
- readSpellings(table, claimed);
1295
+ readSpellings(table, claimed, (_name, role) => spellingPlace(site, role, hookNote(identity)));
1296
+ }
1297
+ /** The declarations of one kind, keyed by name, the first of each name winning. */
1298
+ function byName(inputs) {
1299
+ const named = new Map();
1300
+ for (const input of inputs) {
1301
+ if (!named.has(input.name)) {
1302
+ named.set(input.name, input);
1303
+ }
1304
+ }
1305
+ return named;
1306
+ }
1307
+ /** Where the globals table's options declared their spellings, a global or a plugin's option. */
1308
+ function tableSpellings(globals) {
1309
+ return (name, role) => {
1310
+ const entry = globals.names.get(name);
1311
+ if (!entry) {
1312
+ return undefined;
1313
+ }
1314
+ const { owner, site } = entry;
1315
+ const note = owner.kind === 'plugin'
1316
+ ? `an option of plugin ${quoted(owner.identity)}`
1317
+ : `the global option "${name}"`;
1318
+ return spellingPlace(site, role, note);
1319
+ };
902
1320
  }
903
1321
  /**
904
1322
  * Every input a hook declared, against the names and spellings the Command, the globals table, and
905
1323
  * an earlier hook already hold. It runs before the Command's own inputs compile, so a hook-declared
906
- * input reports as the plugin's fault and never as the author's.
1324
+ * input reports as the plugin's fault and never as the author's. A collision carries a finding for
1325
+ * the hook's input and one for the declaration that already holds the name or spelling.
907
1326
  */
908
- function checkAttachedInputs(declared, attached, globals) {
909
- const subject = commandSubject(declared.name);
1327
+ function checkAttachedInputs(declared, attached, place) {
1328
+ const { globals, path } = place;
1329
+ const scope = localScope(declared.name, path);
1330
+ const { subject } = scope;
910
1331
  const hooked = new Set(attached.map((entry) => entry.input));
911
1332
  const authored = declared.inputs.filter((input) => !hooked.has(input));
912
1333
  const options = authored.filter((input) => input.kind === 'option');
1334
+ const locals = byName(options);
913
1335
  const held = {
914
- arguments: new Set(authored.filter((input) => input.kind === 'argument').map((input) => input.name)),
1336
+ arguments: byName(authored.filter((input) => input.kind === 'argument')),
915
1337
  globals,
916
1338
  hookArguments: new Map(),
917
1339
  hooks: new Map(),
918
- locals: new Set(options.map((input) => input.name)),
1340
+ locals,
1341
+ path,
919
1342
  };
920
1343
  const claimed = new Map();
921
- readSpellings(globals.options, claimed);
922
- readSpellings(compileOptions(options, subject), claimed);
923
- for (const { identity, input } of attached) {
1344
+ readSpellings(globals.options, claimed, tableSpellings(globals));
1345
+ readSpellings(compileOptions(options, scope), claimed, (name, role) => {
1346
+ const local = locals.get(name);
1347
+ return local === undefined || local.kind !== 'option'
1348
+ ? undefined
1349
+ : spellingPlace(scope.siteOf(local), role, `the local option "${name}"`);
1350
+ });
1351
+ for (const entry of attached) {
1352
+ const { identity, input } = entry;
924
1353
  const collision = attachedCollision(input, held);
925
1354
  if (collision) {
926
- throw new DeclarationError(`Plugin "${identity}" declares ${input.kind} "${input.name}" on ${subject}, which is already declared as ${collision.clause}. ${collision.remedy}`);
1355
+ throw new DeclarationError(nameCollisionRule(input.kind, collision.kind), {
1356
+ correction: collision.remedy,
1357
+ findings: [inputFinding(path, input, hookNote(identity)), collision.held],
1358
+ sentence: `Plugin ${quoted(identity)} declares ${input.kind} ${quoted(input.name)} on ${subject}, which is already declared as ${collision.clause}.`,
1359
+ });
927
1360
  }
928
1361
  if (input.kind === 'option') {
929
- checkAttachedSpelling({ identity, input }, { claimed, subject });
930
- held.hooks.set(input.name, identity);
1362
+ checkAttachedSpelling({ identity, input }, { claimed, scope });
1363
+ held.hooks.set(input.name, entry);
931
1364
  }
932
1365
  else {
933
- held.hookArguments.set(input.name, identity);
1366
+ held.hookArguments.set(input.name, entry);
934
1367
  }
935
1368
  }
936
1369
  }
@@ -969,29 +1402,31 @@ function bindDispatch(state, action, globals) {
969
1402
  function checkInputFacts(inputs, command, context) {
970
1403
  const { name, subject } = command;
971
1404
  for (const input of inputs) {
972
- const sentence = `${commandSentence(name)} ${input.kind} "${input.name}"`;
973
- checkDescription(sentence, input.config.description);
1405
+ const site = inputSite(name, context.path, input);
1406
+ const sentence = site.subject;
1407
+ checkDescription(site, input.config.description);
974
1408
  if (input.kind === 'argument') {
975
- checkNoListingFacts(sentence, input.config);
976
- checkNoArgumentBinding(sentence, input.config);
1409
+ checkNoListingFacts(site, input.config);
1410
+ checkNoArgumentBinding(site, input.config);
977
1411
  }
978
1412
  else {
979
- checkHidden(sentence, input.config.hidden);
980
- checkDeprecated(sentence, input.config.deprecated);
981
- checkEnvBinding(sentence, input.config);
1413
+ checkHidden(site, input.config.hidden);
1414
+ checkDeprecated(site, input.config.deprecated);
1415
+ checkEnvBinding(site, input.config);
982
1416
  }
983
1417
  context.extensions.set(input, buildExtensions({
984
1418
  declared: input.config.extensions,
985
1419
  descriptors: context.descriptors,
1420
+ site: { ...site, at: '1.extensions' },
986
1421
  subject: { phrase: `on ${subject} ${input.kind} "${input.name}"`, sentence },
987
1422
  target: input.kind,
988
1423
  }));
989
1424
  }
990
1425
  }
991
1426
  /** One Command declares arguments or attaches children, whichever declaration made each of them. */
992
- function checkArgumentPlacement(name, slot, child) {
1427
+ function checkArgumentPlacement(command, slot, child) {
993
1428
  if (slot && child) {
994
- throw placementFault(name, slot.input.name, child.name);
1429
+ throw placementFault(command, slot.input, child);
995
1430
  }
996
1431
  }
997
1432
  /**
@@ -1008,7 +1443,8 @@ export function buildCommand(state, context) {
1008
1443
  for (const [input, record] of state.records) {
1009
1444
  context.extensions.set(input, record);
1010
1445
  }
1011
- const declaredSlots = collectArguments(state, subject);
1446
+ const place = { name, path: context.path };
1447
+ const declaredSlots = collectArguments(state, place);
1012
1448
  const hasAction = action !== undefined;
1013
1449
  /**
1014
1450
  * The hooks run once the author's declaration is complete, and each value a hook receives resolves
@@ -1027,15 +1463,15 @@ export function buildCommand(state, context) {
1027
1463
  checkInputFacts(hookInputs.map((call) => call.input), { name, subject }, context);
1028
1464
  // The hook-declared names are checked first, so a collision reports in the plugin's voice.
1029
1465
  // A rule the author's own declaration voices never speaks for a name a hook declared.
1030
- checkAttachedInputs(hooked, hookInputs, globals);
1466
+ checkAttachedInputs(hooked, hookInputs, { globals, path: context.path });
1031
1467
  // Every value was validated once, the author's at each call and each hook's at its `extend()`.
1032
1468
  const extensions = publishStore(progress.extensions);
1033
- const slots = hookInputs.length > 0 ? collectArguments(hooked, subject) : declaredSlots;
1034
- checkArgumentPlacement(name, slots[0], state.children[0]);
1035
- const result = checkFinished(hooked, hasAction);
1036
- const options = checkLocalOptions(optionsOf(hooked.inputs), globals, subject);
1469
+ const slots = hookInputs.length > 0 ? collectArguments(hooked, place) : declaredSlots;
1470
+ checkArgumentPlacement(place, slots[0], state.children[0]);
1471
+ const result = checkFinished(hooked, hasAction, { path: context.path });
1472
+ const options = checkLocalOptions(optionsOf(hooked.inputs), globals, localScope(name, context.path));
1037
1473
  // A hook's erased calls answer the declaration rules an authored call answers at the call.
1038
- checkDeclarations(hookInputs.map((call) => call.input));
1474
+ checkDeclarations(hookInputs.map(({ input }) => ({ input, site: inputSite(name, context.path, input) })));
1039
1475
  const children = new Map();
1040
1476
  const routes = new Map();
1041
1477
  for (const child of state.children) {
@@ -1093,7 +1529,7 @@ export class CommandBuilder {
1093
1529
  }
1094
1530
  argument(name, config) {
1095
1531
  const input = {
1096
- config: captureConfig(config),
1532
+ config,
1097
1533
  kind: 'argument',
1098
1534
  name,
1099
1535
  };
@@ -1101,7 +1537,7 @@ export class CommandBuilder {
1101
1537
  }
1102
1538
  option(name, config) {
1103
1539
  const input = {
1104
- config: captureConfig(config),
1540
+ config,
1105
1541
  kind: 'option',
1106
1542
  name,
1107
1543
  };
@@ -1172,12 +1608,35 @@ export function collectInputs(command) {
1172
1608
  ];
1173
1609
  }
1174
1610
  /**
1175
- * The names a routing failure offers: the canonical names of the visible children, in authoring
1176
- * order. A candidate list is a listing, so a hidden child is absent from it, and a parent whose
1177
- * children are all hidden offers none.
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.
1613
+ */
1614
+ 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
+ }
1619
+ const walk = (command, path) => {
1620
+ for (const input of command.inputs) {
1621
+ places.set(input, inputPlace(input, { global: false, path }));
1622
+ }
1623
+ for (const [name, child] of command.children) {
1624
+ walk(child, [...path, name]);
1625
+ }
1626
+ };
1627
+ walk(graph.root, []);
1628
+ return places;
1629
+ }
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.
1178
1635
  */
1179
1636
  function candidatesOf(command) {
1180
- return [...command.children].filter(([, child]) => !child.hidden).map(([name]) => name);
1637
+ return [...command.children]
1638
+ .filter(([, child]) => !child.hidden && child.deprecated === undefined)
1639
+ .map(([name]) => name);
1181
1640
  }
1182
1641
  /**
1183
1642
  * Whether a token names one of the Command's children: the Command has children and the token is
@@ -1186,8 +1645,12 @@ function candidatesOf(command) {
1186
1645
  export function readsAsChild(command, token) {
1187
1646
  return command.children.size > 0 && !isOptionToken(token);
1188
1647
  }
1189
- /** Bare tokens, names or aliases, select children until a Command has none; a hyphen commits. */
1190
- export function route(root, tokens) {
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) {
1191
1654
  let command = root;
1192
1655
  const path = [];
1193
1656
  let index = 0;
@@ -1202,6 +1665,7 @@ export function route(root, tokens) {
1202
1665
  // An alias routes like the canonical name, and the path it walks reports that name alone.
1203
1666
  command = child.command;
1204
1667
  path.push(child.name);
1668
+ walked?.(Object.freeze([...path]));
1205
1669
  index += 1;
1206
1670
  }
1207
1671
  return { command, path, tokens: tokens.slice(index) };
@@ -1240,10 +1704,13 @@ export function argumentSlot(slots, position) {
1240
1704
  const last = slots.at(-1);
1241
1705
  return slots[position] ?? (last?.variadic ? last : undefined);
1242
1706
  }
1243
- /** Consumes the globals table, then routes the remaining bare tokens to a Command. */
1244
- export function routeInvocation(graph, argv) {
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) {
1245
1712
  const scan = extractGlobals(graph.globals.options, [...argv]);
1246
- const routed = route(graph.root, scan.rest);
1713
+ const routed = route(graph.root, scan.rest, walked);
1247
1714
  return {
1248
1715
  command: routed.command,
1249
1716
  path: routed.path,
@@ -1306,7 +1773,7 @@ function requestNode(graph, path, place) {
1306
1773
  const options = place.global ? graph.globals : nodeAt(graph, path).options;
1307
1774
  const node = options.find((option) => option.name === place.name);
1308
1775
  if (!node) {
1309
- throw new InternalError(`Option "${place.name}" is not in the inspected graph.`, undefined);
1776
+ throw graphMismatch(`Option "${place.name}" is not in the inspected graph.`);
1310
1777
  }
1311
1778
  return node;
1312
1779
  }
@@ -1330,6 +1797,7 @@ async function fillScope({ graph, invocation, routed }, local) {
1330
1797
  locals: local.kind === 'parsed'
1331
1798
  ? { global: false, inputs: optionsOf(routed.command.inputs), values: locals }
1332
1799
  : undefined,
1800
+ offer: invocation.offer,
1333
1801
  out: invocation.sourceOut,
1334
1802
  plugins: graph.globals.plugins,
1335
1803
  request: (input, global) => requestNode(invocation.inspected(), routed.path, { global, name: input.name }),
@@ -1426,6 +1894,7 @@ export async function prepareDispatch(graph, routed, invocation) {
1426
1894
  return { ...ready, globals, kind: 'ready', result };
1427
1895
  }
1428
1896
  catch (error) {
1429
- return held(error);
1897
+ // The fault is held as a failure, so a throw from reading a validator's output is never offered.
1898
+ return held(toFailure(error));
1430
1899
  }
1431
1900
  }