@loomcli/core 0.5.0 → 0.7.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 (90) hide show
  1. package/NOTICE +34 -0
  2. package/dist/application.d.ts +18 -2
  3. package/dist/application.js +302 -75
  4. package/dist/bindings.d.ts +15 -10
  5. package/dist/bindings.js +34 -15
  6. package/dist/capture.d.ts +65 -0
  7. package/dist/capture.js +99 -0
  8. package/dist/chain.d.ts +15 -6
  9. package/dist/chain.js +40 -20
  10. package/dist/command-rules.d.ts +55 -0
  11. package/dist/command-rules.js +142 -0
  12. package/dist/command.d.ts +65 -23
  13. package/dist/command.js +734 -236
  14. package/dist/controls.d.ts +8 -0
  15. package/dist/controls.js +23 -0
  16. package/dist/defect.d.ts +18 -0
  17. package/dist/defect.js +272 -0
  18. package/dist/developer.d.ts +24 -0
  19. package/dist/developer.js +52 -0
  20. package/dist/diagnostic-text.d.ts +81 -0
  21. package/dist/diagnostic-text.js +283 -0
  22. package/dist/diagnostic.d.ts +11 -0
  23. package/dist/diagnostic.js +70 -0
  24. package/dist/errors.d.ts +108 -24
  25. package/dist/errors.js +324 -53
  26. package/dist/exit-codes.d.ts +45 -0
  27. package/dist/exit-codes.js +46 -0
  28. package/dist/extension.d.ts +41 -8
  29. package/dist/extension.js +142 -57
  30. package/dist/facts.d.ts +71 -11
  31. package/dist/facts.js +106 -23
  32. package/dist/globals.d.ts +29 -21
  33. package/dist/globals.js +117 -46
  34. package/dist/glyphs.generated.js +1 -1
  35. package/dist/hints.d.ts +79 -0
  36. package/dist/hints.js +247 -0
  37. package/dist/host.d.ts +13 -0
  38. package/dist/host.js +43 -1
  39. package/dist/identity.d.ts +19 -0
  40. package/dist/identity.js +72 -0
  41. package/dist/index.d.ts +12 -2
  42. package/dist/index.js +6 -0
  43. package/dist/input-rules.d.ts +68 -0
  44. package/dist/input-rules.js +152 -0
  45. package/dist/inspect.d.ts +12 -12
  46. package/dist/inspect.js +92 -48
  47. package/dist/lanes.js +1 -1
  48. package/dist/locate.js +4 -4
  49. package/dist/options.d.ts +30 -2
  50. package/dist/options.js +143 -46
  51. package/dist/output.d.ts +9 -2
  52. package/dist/output.js +18 -2
  53. package/dist/plain.d.ts +56 -0
  54. package/dist/plain.js +238 -0
  55. package/dist/plugin-rules.d.ts +68 -0
  56. package/dist/plugin-rules.js +164 -0
  57. package/dist/plugin-settings.d.ts +18 -0
  58. package/dist/plugin-settings.js +38 -0
  59. package/dist/plugin.d.ts +27 -15
  60. package/dist/plugin.js +429 -144
  61. package/dist/prototypes.d.ts +7 -0
  62. package/dist/prototypes.js +29 -0
  63. package/dist/rendering.d.ts +6 -1
  64. package/dist/rendering.js +23 -5
  65. package/dist/rules.d.ts +51 -0
  66. package/dist/rules.js +115 -0
  67. package/dist/sequence.js +6 -1
  68. package/dist/sources.d.ts +6 -4
  69. package/dist/sources.js +25 -16
  70. package/dist/style-layout.js +2 -2
  71. package/dist/style-width.d.ts +13 -0
  72. package/dist/style-width.js +170 -0
  73. package/dist/style-wire.js +1 -1
  74. package/dist/style.js +1 -1
  75. package/dist/theme.d.ts +4 -0
  76. package/dist/theme.js +25 -5
  77. package/dist/thenable.d.ts +15 -0
  78. package/dist/thenable.js +29 -0
  79. package/dist/translators.d.ts +69 -0
  80. package/dist/translators.js +253 -0
  81. package/dist/types.d.ts +13 -3
  82. package/dist/unicode.generated.d.ts +27 -0
  83. package/dist/unicode.generated.js +1036 -0
  84. package/dist/validation.d.ts +69 -7
  85. package/dist/validation.js +211 -67
  86. package/dist/view.d.ts +61 -22
  87. package/dist/view.js +168 -81
  88. package/licenses/unicode-LICENSE.txt +41 -0
  89. package/licenses/uucode-LICENSE.md +35 -0
  90. package/package.json +9 -5
package/dist/command.js CHANGED
@@ -1,13 +1,19 @@
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 { captureDeclaration, unreadableArgument } from './capture.js';
4
+ import { aliasWithoutNames, argumentDeclaredTwice, argumentsBesideChildren, commandAttachedTwice, commandGlobals, commandWithoutAction, declaredAfterAction, declaredName, groupOption, multipleActions, multipleResults, nestingDepth, notACommand, optionalArgumentLast, portableName, repeatedAlias, resultWithoutAction, resultWithoutViews, rowViewOnValue, siblingNameTaken, unknownDefaultView, variadicArgumentLast, viewName, viewShape, viewsWithoutResult, } from './command-rules.js';
5
+ import { elided, quoteString, spelled } from './diagnostic-text.js';
6
+ import { asSentence, commandSentence, commandSubject, DeclarationError, NonCallableCommandError, quoted, ResultError, toFailure, UnexpectedArgumentError, UnknownCommandError, reasonOf, } from './errors.js';
4
7
  import { buildExtensions, extendStore, publishStore, registerDescriptor, storeCommandLayers, validateLayer, } from './extension.js';
5
- import { checkDeprecated, checkDescription, checkHidden, checkNoListingFacts, isPlainObject, } from './facts.js';
8
+ import { checkDeprecated, checkDescription, checkHidden, checkNoListingFacts, declarerNote, siteFinding, } from './facts.js';
6
9
  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';
10
+ import { nameSharedAcrossKinds, optionDeclaredTwice, spellingTaken } from './input-rules.js';
11
+ import { graphMismatch, nodeAt, resultNode } from './inspect.js';
12
+ import { checkOptionName, compileOptions, copyValues, emptyValues, extractGlobals, isOptionToken, mergeValues, parseInputs, spellingMark, } from './options.js';
13
+ import { declaring, isPlainObject, shallowList, snapshot } from './plain.js';
14
+ import { brokenAttachHook, notAnObject } from './plugin-rules.js';
9
15
  import { fillInputs } from './sources.js';
10
- import { captureConfig, checkDeclarations, validateValues } from './validation.js';
16
+ import { captureInputConfig, configUnread, checkDeclarations, declaringSite, inputPlace, validateValues, } from './validation.js';
11
17
  /**
12
18
  * The deepest level below the root a Command may sit at, and the word its diagnostic spells it with.
13
19
  * A child of the root sits at level 1. Raising the cap relaxes a rule and breaks no application.
@@ -41,13 +47,62 @@ function nodeHandle(name, state) {
41
47
  export function commandNode(value) {
42
48
  return typeof value === 'object' && value !== null ? nodes.get(value) : undefined;
43
49
  }
44
- /** A named Command's options slot holds a plain options object, and never retired globals wiring. */
45
- function checkCommandOptions(name, options) {
46
- if (options !== undefined && !isPlainObject(options)) {
47
- throw new DeclarationError(`${commandSentence(name)} options must be an object. Supply a Command options object.`);
50
+ /**
51
+ * The path a finding opens with for a declaration call on one Command. The root's is empty. A named
52
+ * Command's call cannot know the parent it will join, so its path is its own name.
53
+ */
54
+ function pathOf(name) {
55
+ return name === null ? [] : [name];
56
+ }
57
+ /** A call's arguments as its author wrote them: the trailing ones left undefined are dropped. */
58
+ export function callArguments(...values) {
59
+ let length = values.length;
60
+ while (length > 0 && values[length - 1] === undefined) {
61
+ length -= 1;
62
+ }
63
+ return values.slice(0, length);
64
+ }
65
+ /** A Command value as a finding prints it, which it would otherwise print as an ellipsis. */
66
+ export function commandCode(name) {
67
+ return spelled(`new Command(${quoteString(name)})`);
68
+ }
69
+ /** The finding for one `command()` call that attached a child under the Command at `path`. */
70
+ export function commandPlacement(path, child) {
71
+ return { arguments: [commandCode(child)], call: 'command', mark: '0', path };
72
+ }
73
+ /** The finding for one `new Command()` call, which carries the name and the options slot. */
74
+ function constructorFinding(name, options, mark) {
75
+ return { arguments: callArguments(name, options), call: 'new Command', mark };
76
+ }
77
+ /**
78
+ * The one copy of the options that `new Command(name, options)` reads, taken once the name is
79
+ * judged: the options object and its `extensions` list, whose entries are what their factory built
80
+ * and are not copied. A read that throws is the unreadable fault of the options, a value that is not
81
+ * a plain object is the not-an-object fault, and a Command declared without options has none.
82
+ */
83
+ function captureCommandOptions(name, options) {
84
+ if (options === undefined) {
85
+ return undefined;
48
86
  }
49
- if (isPlainObject(options) && 'globals' in options) {
50
- throw new DeclarationError(`${commandSentence(name)} declares globals. Declare globals on the Application and register its environment.`);
87
+ return captureDeclaration(options, (copy, read) => {
88
+ read.nested(copy, 'extensions', shallowList);
89
+ }, {
90
+ notAnObject: () => new DeclarationError(notAnObject, {
91
+ correction: 'Supply a Command options object.',
92
+ findings: [constructorFinding(name, options, '1')],
93
+ sentence: `${commandSentence(name)} declares options that are not an object.`,
94
+ }),
95
+ unreadable: unreadableArgument({ call: 'new Command', named: name, subject: commandSentence(name) }, 'options'),
96
+ });
97
+ }
98
+ /** A named Command's options never carry the retired globals wiring. */
99
+ function checkCommandOptions(name, options) {
100
+ if ('globals' in options) {
101
+ throw new DeclarationError(commandGlobals, {
102
+ correction: 'Declare globals on the Application and register its environment.',
103
+ findings: [constructorFinding(name, options, '1.globals')],
104
+ sentence: `${commandSentence(name)} declares globals.`,
105
+ });
51
106
  }
52
107
  }
53
108
  /** One name rule for an argument, option, or view name: a bare token the parser can read. */
@@ -66,11 +121,25 @@ export function isPortableName(name) {
66
121
  /** The one correction every portable name diagnostic ends with. */
67
122
  export const portableNameCorrection = 'Use a nonempty name of A-Z, a-z, 0-9, ".", "_", and "-" that does not start with "-" or ".".';
68
123
  /** 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}`);
124
+ function checkAliasNames(command, names) {
125
+ for (const [index, alias] of names.entries()) {
126
+ if (!isPortableName(alias)) {
127
+ throw new DeclarationError(portableName, {
128
+ correction: portableNameCorrection,
129
+ findings: [{ arguments: names, call: 'alias', mark: String(index), path: pathOf(command) }],
130
+ sentence: `${commandSentence(command)} declares an alias named ${quoted(alias)}.`,
131
+ });
132
+ }
72
133
  }
73
134
  }
135
+ /**
136
+ * The finding for the `alias()` call that declared one alias on the Command at `path`, rebuilt from
137
+ * every alias the Command holds.
138
+ */
139
+ function aliasFinding(command, alias, note) {
140
+ const { aliases, path } = command;
141
+ return { arguments: aliases, call: 'alias', mark: String(aliases.indexOf(alias)), note, path };
142
+ }
74
143
  /** How the extension diagnostics of one Command's own layers name it. */
75
144
  export function layerOf(name) {
76
145
  return { phrase: `on ${commandSubject(name)}`, sentence: commandSentence(name) };
@@ -95,25 +164,39 @@ export function freshState(declaration) {
95
164
  };
96
165
  }
97
166
  /**
98
- * A named Command's own declaration, checked before the value exists: the name, the options slot,
99
- * the core facts, and the extension values the slot carries. A later change to the options object
100
- * the author passed changes nothing the declaration holds.
167
+ * A named Command's own declaration, checked before the value exists: the name, then the one copy
168
+ * of the options slot, its core facts, and the extension values it carries. A later change to the
169
+ * options object the author passed changes nothing the declaration holds.
101
170
  */
102
171
  function namedState(name, options) {
103
172
  if (!isPortableName(name)) {
104
- throw new DeclarationError(`Command name "${String(name)}" is invalid. ${portableNameCorrection}`);
173
+ // The options are read after the name is judged, so the name's finding prints them elided.
174
+ throw new DeclarationError(portableName, {
175
+ correction: portableNameCorrection,
176
+ findings: [
177
+ constructorFinding(name, options === undefined ? undefined : spelled(elided), '0'),
178
+ ],
179
+ sentence: `Command name ${quoted(name)} is invalid.`,
180
+ });
105
181
  }
106
- checkCommandOptions(name, options);
107
- const slot = isPlainObject(options) ? options : undefined;
108
- const sentence = commandSentence(name);
182
+ const slot = captureCommandOptions(name, options);
183
+ if (slot !== undefined) {
184
+ checkCommandOptions(name, slot);
185
+ }
186
+ const site = {
187
+ at: '1',
188
+ declaration: { arguments: [name, slot], call: 'new Command' },
189
+ subject: commandSentence(name),
190
+ };
109
191
  // 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);
192
+ const description = checkDescription(site, slot?.description);
193
+ const hidden = checkHidden(site, slot?.hidden);
194
+ const deprecated = checkDeprecated(site, slot?.deprecated);
113
195
  const descriptors = new Map();
114
196
  const extensions = storeCommandLayers({
115
197
  descriptors,
116
198
  layers: [slot?.extensions],
199
+ site: { ...site, at: '1.extensions' },
117
200
  subject: layerOf(name),
118
201
  });
119
202
  return {
@@ -130,16 +213,56 @@ function namedState(name, options) {
130
213
  * A declaration call after `action()` is an order fault the receiver's own earlier call makes
131
214
  * certain. The types remove the call for a TypeScript author; a JavaScript author meets it here.
132
215
  */
133
- function checkOpen(state, clause, remedy) {
216
+ function checkOpen(state, late) {
134
217
  if (state.action !== undefined) {
135
- throw new DeclarationError(`${commandSentence(state.name)} ${clause} after its action. ${remedy}`);
218
+ throw new DeclarationError(declaredAfterAction, {
219
+ correction: late.remedy,
220
+ findings: [
221
+ {
222
+ arguments: late.arguments,
223
+ call: late.call,
224
+ mark: '0',
225
+ note: 'after action()',
226
+ path: pathOf(state.name),
227
+ },
228
+ ],
229
+ sentence: `${commandSentence(state.name)} ${late.clause} after its action.`,
230
+ });
136
231
  }
137
232
  }
138
233
  /** The remedy a late argument or option earns. */
139
234
  const inputRemedy = 'Declare arguments and options before action().';
235
+ /** Where one input was declared: the call on the Command at `path` that declared it. */
236
+ function inputSite(name, path, input) {
237
+ return declaringSite(input, inputPlace(input, { global: false, path }), `${commandSentence(name)} ${input.kind} ${quoted(input.name)}`);
238
+ }
239
+ /** The finding for the `argument()` or `option()` call that declared one input, marking its name. */
240
+ function inputFinding(path, input, note) {
241
+ const site = declaringSite(input, inputPlace(input, { global: false, path }));
242
+ return siteFinding(site, site.named, note);
243
+ }
244
+ /**
245
+ * The finding for an input whose name is invalid, marking the name. The config is read after the
246
+ * name is judged, so it prints elided.
247
+ */
248
+ function nameFinding(path, input) {
249
+ const site = configUnread(declaringSite(input, inputPlace(input, { global: false, path })));
250
+ return siteFinding(site, site.named);
251
+ }
252
+ /** The scope one Command's own options compile under: its subject, and each option's call. */
253
+ function localScope(name, path) {
254
+ return { siteOf: (input) => inputSite(name, path, input), subject: commandSubject(name) };
255
+ }
140
256
  /** 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.`);
257
+ function placementFault(parent, argument, child) {
258
+ return new DeclarationError(argumentsBesideChildren, {
259
+ correction: 'Move the argument into a child Command or remove the children.',
260
+ findings: [
261
+ inputFinding(parent.path, argument, 'the argument'),
262
+ { ...child.placement, note: 'the child' },
263
+ ],
264
+ sentence: `${commandSentence(parent.name)} declares argument "${argument.name}" and attaches child "${child.name}".`,
265
+ });
143
266
  }
144
267
  /** The options one declaration list holds, in declaration order. */
145
268
  function optionsOf(inputs) {
@@ -163,6 +286,7 @@ function recordInput(state, input) {
163
286
  const record = buildExtensions({
164
287
  declared: input.config.extensions,
165
288
  descriptors,
289
+ site: { ...inputSite(name, pathOf(name), input), at: '1.extensions' },
166
290
  subject: {
167
291
  phrase: `on ${commandSubject(name)} ${input.kind} "${input.name}"`,
168
292
  sentence: `${commandSentence(name)} ${input.kind} "${input.name}"`,
@@ -177,31 +301,63 @@ function recordInput(state, input) {
177
301
  */
178
302
  function checkArgument(state, input) {
179
303
  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
- }
304
+ const path = pathOf(name);
305
+ const command = { name, path, subject: commandSubject(name) };
184
306
  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
- }
307
+ checkArgumentName(command, declared, input);
188
308
  const child = state.children[0];
189
309
  if (child) {
190
- throw placementFault(name, input.name, child.name);
310
+ throw placementFault(command, input, child);
191
311
  }
192
312
  const previous = declared.at(-1);
193
313
  if (previous) {
194
- checkSlotOrder(slotOf(previous), slotOf(input), subject);
314
+ checkSlotOrder(slotOf(previous), slotOf(input), command);
315
+ }
316
+ const site = inputSite(name, path, input);
317
+ checkDescription(site, input.config.description);
318
+ checkNoListingFacts(site, input.config);
319
+ checkNoArgumentBinding(site, input.config);
320
+ checkDeclarations([{ input, site }]);
321
+ }
322
+ /** One argument's name answers the declared-name rule and names no argument declared before it. */
323
+ function checkArgumentName(command, declared, input) {
324
+ const { path } = command;
325
+ if (!isDeclaredName(input.name)) {
326
+ throw new DeclarationError(declaredName, {
327
+ correction: 'Use a nonempty name without a leading hyphen, whitespace, or "=".',
328
+ findings: [nameFinding(path, input)],
329
+ sentence: `${commandSentence(command.name)} declares an argument named ${quoted(input.name)}.`,
330
+ });
331
+ }
332
+ const first = declared.find((entry) => entry.name === input.name);
333
+ if (first) {
334
+ throw new DeclarationError(argumentDeclaredTwice, {
335
+ correction: 'Remove or rename the duplicate.',
336
+ findings: [
337
+ inputFinding(path, first, 'the first declaration'),
338
+ inputFinding(path, input, 'the second declaration'),
339
+ ],
340
+ sentence: `Argument "${input.name}" is declared more than once on ${command.subject}.`,
341
+ });
195
342
  }
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
343
  }
202
344
  /** 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);
345
+ export function declareArgument(state, declared) {
346
+ // The call's own input is judged before the receiver's state, as alias() judges its names.
347
+ // A name of another kind then reports as a declared name instead of failing to print in the order diagnostic.
348
+ // The config is read once right after its name is judged, and every later check reads that copy.
349
+ const { name } = state;
350
+ checkArgumentName({ name, path: pathOf(name), subject: commandSubject(name) }, [], declared);
351
+ const input = {
352
+ ...declared,
353
+ config: captureInputConfig(declared, { call: 'argument', path: pathOf(name) }),
354
+ };
355
+ checkOpen(state, {
356
+ arguments: [input.name, input.config],
357
+ call: 'argument',
358
+ clause: `declares argument ${quoted(input.name)}`,
359
+ remedy: inputRemedy,
360
+ });
205
361
  checkArgument(state, input);
206
362
  const recorded = recordInput(state, input);
207
363
  const previous = state.bind;
@@ -222,16 +378,29 @@ const noGlobals = { names: new Map(), options: new Map(), variables: new Map() }
222
378
  * options also meet the Application's globals table, which a named Command meets when its subtree
223
379
  * joins an Application.
224
380
  */
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);
381
+ export function declareOption(state, declared, table = noGlobals) {
382
+ // The call's own input is judged before the receiver's state, as alias() judges its names.
383
+ // The config is read once right after its name is judged, and every later check reads that copy.
384
+ const path = pathOf(state.name);
385
+ checkOptionName(declared.name, configUnread(inputSite(state.name, path, declared)));
386
+ const input = {
387
+ ...declared,
388
+ config: captureInputConfig(declared, { call: 'option', path }),
389
+ };
390
+ const site = inputSite(state.name, path, input);
391
+ checkOpen(state, {
392
+ arguments: [input.name, input.config],
393
+ call: 'option',
394
+ clause: `declares option ${quoted(input.name)}`,
395
+ remedy: inputRemedy,
396
+ });
397
+ checkDescription(site, input.config.description);
398
+ checkHidden(site, input.config.hidden);
399
+ checkDeprecated(site, input.config.deprecated);
400
+ checkEnvBinding(site, input.config);
232
401
  const recorded = recordInput(state, input);
233
- checkLocalOptions([...optionsOf(state.inputs), input], table, commandSubject(state.name));
234
- checkDeclarations([input]);
402
+ checkLocalOptions([...optionsOf(state.inputs), input], table, localScope(state.name, pathOf(state.name)));
403
+ checkDeclarations([{ input, site }]);
235
404
  const previous = state.bind;
236
405
  return {
237
406
  ...state,
@@ -249,23 +418,40 @@ export function declareOption(state, input, table = noGlobals) {
249
418
  */
250
419
  export function declareAlias(state, names) {
251
420
  const { name } = state;
421
+ const path = pathOf(name);
252
422
  const first = names[0];
253
423
  if (first === undefined) {
254
- throw new DeclarationError(`${commandSentence(name)} declares an alias with no names. Supply at least one name.`);
424
+ throw new DeclarationError(aliasWithoutNames, {
425
+ correction: 'Supply at least one name.',
426
+ findings: [{ arguments: [], call: 'alias', path }],
427
+ sentence: `${commandSentence(name)} declares an alias with no names.`,
428
+ });
255
429
  }
256
430
  // The call's own input is judged before the receiver's state.
257
431
  // 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().');
432
+ checkAliasNames(name, names);
433
+ checkOpen(state, {
434
+ arguments: names,
435
+ call: 'alias',
436
+ clause: `declares alias "${first}"`,
437
+ remedy: 'Declare aliases before action().',
438
+ });
262
439
  const aliases = [...state.aliases];
263
- for (const alias of names) {
440
+ for (const [index, alias] of names.entries()) {
441
+ const repeated = { arguments: names, call: 'alias', mark: String(index), path };
264
442
  if (alias === name) {
265
- throw new DeclarationError(`${commandSentence(name)} declares alias "${alias}", which is its own name. Remove the alias.`);
443
+ throw new DeclarationError(repeatedAlias, {
444
+ correction: 'Remove the alias.',
445
+ findings: [{ ...repeated, note: 'its own name' }],
446
+ sentence: `${commandSentence(name)} declares alias "${alias}", which is its own name.`,
447
+ });
266
448
  }
267
449
  if (aliases.includes(alias)) {
268
- throw new DeclarationError(`${commandSentence(name)} declares alias "${alias}" twice. Remove the repeated alias.`);
450
+ throw new DeclarationError(repeatedAlias, {
451
+ correction: 'Remove the repeated alias.',
452
+ findings: [{ ...repeated, note: 'already an alias' }],
453
+ sentence: `${commandSentence(name)} declares alias "${alias}" twice.`,
454
+ });
269
455
  }
270
456
  aliases.push(alias);
271
457
  }
@@ -280,6 +466,7 @@ export function declareExtensions(state, values) {
280
466
  const layer = validateLayer({
281
467
  declared: values,
282
468
  descriptors,
469
+ site: { at: '', declaration: { arguments: values, call: 'extend', path: pathOf(state.name) } },
283
470
  subject: layerOf(state.name),
284
471
  target: 'command',
285
472
  });
@@ -299,7 +486,19 @@ function declaredChannel(out) {
299
486
  */
300
487
  export function declareAction(state, handler) {
301
488
  if (state.action !== undefined) {
302
- throw new DeclarationError(`${commandSentence(state.name)} has multiple actions. Register one action.`);
489
+ throw new DeclarationError(multipleActions, {
490
+ correction: 'Register one action.',
491
+ findings: [
492
+ {
493
+ arguments: [handler],
494
+ call: 'action',
495
+ mark: '0',
496
+ note: 'the second action',
497
+ path: pathOf(state.name),
498
+ },
499
+ ],
500
+ sentence: `${commandSentence(state.name)} has multiple actions.`,
501
+ });
303
502
  }
304
503
  // The graph and the routed node stay getters, because a spread would build the graph on every run.
305
504
  const stored = (context) => handler({
@@ -324,33 +523,71 @@ export function declareAction(state, handler) {
324
523
  * here; the empty record and the default wait for attach, because `views()` can still add keys.
325
524
  */
326
525
  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);
526
+ const call = {
527
+ arguments: callArguments(declaration),
528
+ kind,
529
+ views: recordOf(declaration, 'views'),
530
+ };
531
+ checkOpen(state, {
532
+ arguments: call.arguments,
533
+ call: resultCallName(call),
534
+ clause: 'declares its result',
535
+ remedy: 'Declare result() or rows() before action().',
536
+ });
537
+ const results = [...state.results, call];
538
+ mergeResult({ name: state.name, path: pathOf(state.name) }, results);
330
539
  return { ...state, results };
331
540
  }
332
541
  /** A `views()` call reshapes views and closes nothing, so it is never a late declaration. */
333
542
  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);
543
+ const results = [...state.results, viewsCall(replacements, options)];
544
+ mergeResult({ name: state.name, path: pathOf(state.name) }, results);
339
545
  return { ...state, results };
340
546
  }
547
+ /** One `views()` call as the results lane records it. */
548
+ function viewsCall(replacements, options) {
549
+ return {
550
+ arguments: callArguments(replacements, options),
551
+ default: recordOf(options, 'default'),
552
+ kind: 'views',
553
+ views: replacements,
554
+ };
555
+ }
341
556
  /** One property of an authoring argument, read defensively: the rules report whatever it holds. */
342
557
  function recordOf(declaration, key) {
343
558
  return isPlainObject(declaration) ? declaration[key] : undefined;
344
559
  }
560
+ /** The authoring call one results-lane call was made through. */
561
+ function resultCallName(call) {
562
+ return call.kind === 'value' ? 'result' : call.kind;
563
+ }
564
+ /** The finding for one results-lane call on the Command at `path`, marking one of its arguments. */
565
+ function resultFinding(path, call, mark) {
566
+ const finding = { arguments: call.arguments, call: resultCallName(call), mark: mark.at, path };
567
+ return mark.note === undefined ? finding : { ...finding, note: mark.note };
568
+ }
569
+ /** Where one views entry sits among its call's arguments: in `views` for a declaration, or first. */
570
+ function entryMark(call, key) {
571
+ return call.kind === 'views' ? `0.${key}` : `0.views.${key}`;
572
+ }
345
573
  /** The parent a declaration is, as attach reads it. */
346
574
  function parentOf(state) {
347
575
  return {
348
- argument: state.inputs.find((input) => input.kind === 'argument')?.name,
576
+ argument: state.inputs.find((input) => input.kind === 'argument'),
349
577
  children: state.children,
350
578
  hasAction: state.action !== undefined,
351
579
  name: state.name,
580
+ path: pathOf(state.name),
352
581
  };
353
582
  }
583
+ /** The path one child sits at under its parent. */
584
+ function childPath(parent, child) {
585
+ return [...parent.path, child];
586
+ }
587
+ /** The call that attached one child, noted with the child's name. */
588
+ function childFinding(entry) {
589
+ return { ...entry.placement, note: `child "${entry.name}"` };
590
+ }
354
591
  /**
355
592
  * One parent's namespace, which every canonical name and alias under it shares. The rule reads the
356
593
  * same whichever sibling the author attached first.
@@ -358,21 +595,41 @@ function parentOf(state) {
358
595
  function checkSiblings(parent, child) {
359
596
  const sentence = commandSentence(parent.name);
360
597
  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.`);
598
+ const aliased = (entry, alias) => aliasFinding({ aliases: entry.node.declared.aliases, path: childPath(parent, entry.name) }, alias, `alias of child "${entry.name}"`);
599
+ const correction = 'Rename or remove one.';
600
+ const twin = children.find((entry) => entry.name === child.name);
601
+ if (twin) {
602
+ throw new DeclarationError(siblingNameTaken, {
603
+ correction,
604
+ findings: [childFinding(twin), childFinding(child)],
605
+ sentence: `${sentence} attaches two children named "${child.name}".`,
606
+ });
363
607
  }
364
608
  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.`);
609
+ const holder = children.find((entry) => entry.name === alias);
610
+ if (holder) {
611
+ throw new DeclarationError(siblingNameTaken, {
612
+ correction,
613
+ findings: [childFinding(holder), aliased(child, alias)],
614
+ sentence: `${sentence} attaches child "${child.name}" with alias "${alias}", which is also the name of child "${alias}".`,
615
+ });
367
616
  }
368
617
  const owner = children.find((entry) => entry.node.declared.aliases.includes(alias));
369
618
  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.`);
619
+ throw new DeclarationError(siblingNameTaken, {
620
+ correction,
621
+ findings: [aliased(owner, alias), aliased(child, alias)],
622
+ sentence: `${sentence} attaches child "${child.name}" with alias "${alias}", which is also an alias of child "${owner.name}".`,
623
+ });
371
624
  }
372
625
  }
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.`);
626
+ const owner = children.find((entry) => entry.node.declared.aliases.includes(child.name));
627
+ if (owner) {
628
+ throw new DeclarationError(siblingNameTaken, {
629
+ correction,
630
+ findings: [aliased(owner, child.name), childFinding(child)],
631
+ sentence: `${sentence} attaches child "${owner.name}" with alias "${child.name}", which is also the name of child "${child.name}".`,
632
+ });
376
633
  }
377
634
  }
378
635
  /**
@@ -381,7 +638,11 @@ function checkSiblings(parent, child) {
381
638
  */
382
639
  function checkNesting(parent, child, level) {
383
640
  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.`);
641
+ throw new DeclarationError(nestingDepth, {
642
+ correction: `Nest Commands at most ${nestingCap.words} levels below the root.`,
643
+ findings: [{ ...child.placement, note: 'has children of its own' }],
644
+ sentence: `${commandSentence(parent)} attaches child "${child.name}", which has children of its own.`,
645
+ });
385
646
  }
386
647
  }
387
648
  /**
@@ -389,14 +650,23 @@ function checkNesting(parent, child, level) {
389
650
  * children. A group with no children receives an invocation no handler can answer, and a local
390
651
  * option on a group reaches no handler either, because locals never inherit.
391
652
  */
392
- function checkGroup(state) {
653
+ function checkGroup(state, place) {
393
654
  const { name } = state;
394
655
  if (state.children.length === 0) {
395
- throw new DeclarationError(`${commandSentence(name)} has no action. Register an action.`);
656
+ const { placement } = place;
657
+ throw new DeclarationError(commandWithoutAction, {
658
+ correction: 'Register an action.',
659
+ findings: placement === undefined ? [] : [{ ...placement, note: 'no action and no children' }],
660
+ sentence: `${commandSentence(name)} has no action.`,
661
+ });
396
662
  }
397
663
  const option = state.inputs.find((input) => input.kind === 'option');
398
664
  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.`);
665
+ throw new DeclarationError(groupOption, {
666
+ correction: 'Register an action or remove the option.',
667
+ findings: [inputFinding(place.path, option, 'no action reads it')],
668
+ sentence: `${commandSentence(name)} declares option ${quoted(option.name)} but registers no action to receive it.`,
669
+ });
400
670
  }
401
671
  }
402
672
  /**
@@ -404,10 +674,10 @@ function checkGroup(state) {
404
674
  * result needs an action, its merged views record names at least one view and its default, and a
405
675
  * Command without an action is a group. It answers with the resolved result.
406
676
  */
407
- function checkFinished(declared, hasAction) {
408
- const result = buildResult(declared, hasAction);
677
+ function checkFinished(declared, hasAction, place) {
678
+ const result = buildResult(declared, hasAction, place.path);
409
679
  if (!hasAction) {
410
- checkGroup(declared);
680
+ checkGroup(declared, place);
411
681
  }
412
682
  return result;
413
683
  }
@@ -415,18 +685,23 @@ function checkFinished(declared, hasAction) {
415
685
  * The one attach operation `Command.command()`, `Application.command()`, and a plugin's
416
686
  * `commands` list share. A Command is an immutable value, so the child is final here: it is checked
417
687
  * 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.
688
+ * the shallowest level the parent can sit at. The placement is the call that attached it, which a
689
+ * finding about the child as a whole marks.
419
690
  */
420
- export function attach(parent, node) {
691
+ export function attach(parent, node, placement) {
421
692
  const { name } = node;
693
+ const child = { name, node, placement };
422
694
  if (parent.hasAction) {
423
- throw new DeclarationError(`${commandSentence(parent.name)} attaches child "${name}" after its action. Attach children before action().`);
695
+ throw new DeclarationError(declaredAfterAction, {
696
+ correction: 'Attach children before action().',
697
+ findings: [{ ...placement, note: 'after action()' }],
698
+ sentence: `${commandSentence(parent.name)} attaches child "${name}" after its action.`,
699
+ });
424
700
  }
425
701
  if (parent.argument !== undefined) {
426
- throw placementFault(parent.name, parent.argument, name);
702
+ throw placementFault(parent, parent.argument, child);
427
703
  }
428
- checkFinished(node.declared, node.hasAction);
429
- const child = { name, node };
704
+ checkFinished(node.declared, node.hasAction, { path: childPath(parent, name), placement });
430
705
  checkSiblings(parent, child);
431
706
  checkNesting(parent.name, child, parentLevel(parent.name) + 1);
432
707
  return child;
@@ -435,13 +710,18 @@ export function attach(parent, node) {
435
710
  export function childNode(parent, child) {
436
711
  const node = commandNode(child);
437
712
  if (!node) {
438
- throw new DeclarationError(`${commandSentence(parent)} attaches a value that is not a Command. Attach the value returned by new Command(name).`);
713
+ throw new DeclarationError(notACommand, {
714
+ correction: 'Attach the value returned by new Command(name).',
715
+ findings: [{ arguments: [child], call: 'command', mark: '0', path: pathOf(parent) }],
716
+ sentence: `${commandSentence(parent)} attaches a value that is not a Command.`,
717
+ });
439
718
  }
440
719
  return node;
441
720
  }
442
721
  /** Attaching is a declaration call too, so the receiver keeps the children it already had. */
443
722
  function attachChild(state, child) {
444
- const attached = attach(parentOf(state), childNode(state.name, child));
723
+ const node = childNode(state.name, child);
724
+ const attached = attach(parentOf(state), node, commandPlacement(pathOf(state.name), node.name));
445
725
  return { ...state, children: [...state.children, attached] };
446
726
  }
447
727
  /**
@@ -453,34 +733,47 @@ function attachChild(state, child) {
453
733
  * here fires only once the cap rises: attach reads one level, and only this walk knows each level.
454
734
  */
455
735
  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.`);
736
+ const { name, node, placement } = child;
737
+ checkNesting(parent.name, child, level);
738
+ const owner = scope.owners.get(node);
739
+ if (owner) {
740
+ throw new DeclarationError(commandAttachedTwice, {
741
+ correction: 'Attach a Command value at one point; create a new Command for each placement.',
742
+ findings: [
743
+ { ...owner.placement, note: 'the first placement' },
744
+ { ...placement, note: 'the second placement' },
745
+ ],
746
+ sentence: `${commandSentence(parent.name)} attaches child "${name}", which ${commandSubject(owner.name)} also attaches.`,
747
+ });
460
748
  }
461
- scope.owners.set(node, parent);
462
- for (const descriptor of node.declared.descriptors.values()) {
463
- registerDescriptor(scope.descriptors, descriptor);
749
+ scope.owners.set(node, { name: parent.name, placement });
750
+ for (const [key, descriptor] of node.declared.descriptors) {
751
+ registerDescriptor(scope.descriptors, descriptor, { identity: key });
464
752
  }
465
- checkLocalOptions(optionsOf(node.declared.inputs), scope.table, commandSubject(name));
753
+ const path = childPath(parent, name);
754
+ checkLocalOptions(optionsOf(node.declared.inputs), scope.table, localScope(name, path));
466
755
  for (const entry of node.declared.children) {
467
- joinSubtree(scope, entry, { level: level + 1, parent: name });
756
+ // A nested child's own placement knew its parent alone, so the walk places it under the root.
757
+ const rooted = { ...entry, placement: commandPlacement(path, entry.name) };
758
+ joinSubtree(scope, rooted, { level: level + 1, parent: { name, path } });
468
759
  }
469
760
  }
470
761
  /**
471
762
  * Attaches one child to an Application's root and walks the subtree it brings, once. The scope's
472
763
  * registers are copies: the caller commits the owners, and the returned root holds the descriptors.
473
764
  */
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 });
765
+ export function attachToRoot(state, child, scope) {
766
+ const parent = parentOf(state);
767
+ const attached = attach(parent, child.node, child.placement);
768
+ joinSubtree(scope, attached, { level: parentLevel(state.name) + 1, parent });
477
769
  return { ...state, children: [...state.children, attached], descriptors: scope.descriptors };
478
770
  }
479
771
  /** Every attached Command's local options against a globals table that has just grown. */
480
- function checkAttachedOptions(table, children) {
772
+ function checkAttachedOptions(table, parent, children) {
481
773
  for (const { name, node } of children) {
482
- checkLocalOptions(optionsOf(node.declared.inputs), table, commandSubject(name));
483
- checkAttachedOptions(table, node.declared.children);
774
+ const path = childPath(parent, name);
775
+ checkLocalOptions(optionsOf(node.declared.inputs), table, localScope(name, path));
776
+ checkAttachedOptions(table, { name, path }, node.declared.children);
484
777
  }
485
778
  }
486
779
  /**
@@ -488,42 +781,54 @@ function checkAttachedOptions(table, children) {
488
781
  * grown. The table's new option reads as the other side of any collision.
489
782
  */
490
783
  export function checkDeclaredOptions(state, table) {
491
- checkLocalOptions(optionsOf(state.inputs), table, commandSubject(state.name));
492
- checkAttachedOptions(table, state.children);
784
+ const place = { name: state.name, path: pathOf(state.name) };
785
+ checkLocalOptions(optionsOf(state.inputs), table, localScope(place.name, place.path));
786
+ checkAttachedOptions(table, place, state.children);
493
787
  }
494
788
  /** A variadic or optional slot ends the positional list, so nothing may follow either one. */
495
- function checkSlotOrder(slot, next, subject) {
789
+ function checkSlotOrder(slot, next, command) {
790
+ const { path, subject } = command;
791
+ const after = inputFinding(path, next.input, 'the argument after it');
496
792
  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.`);
793
+ throw new DeclarationError(variadicArgumentLast, {
794
+ correction: 'Declare the variadic argument last.',
795
+ findings: [inputFinding(path, slot.input, 'the variadic argument'), after],
796
+ sentence: `Argument "${slot.input.name}" is variadic and precedes argument "${next.input.name}" on ${subject}.`,
797
+ });
498
798
  }
499
799
  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.`);
800
+ const findings = [inputFinding(path, slot.input, 'the optional argument'), after];
801
+ throw new DeclarationError(optionalArgumentLast, next.required
802
+ ? {
803
+ correction: 'Declare optional arguments after required ones.',
804
+ findings,
805
+ sentence: `Argument "${slot.input.name}" is optional and precedes required argument "${next.input.name}" on ${subject}.`,
806
+ }
807
+ : {
808
+ correction: 'Declare an optional argument last.',
809
+ findings,
810
+ sentence: `Argument "${next.input.name}" follows optional argument "${slot.input.name}" on ${subject}.`,
811
+ });
503
812
  }
504
813
  }
505
814
  /**
506
815
  * The positional slots one declaration holds. Every authored argument answered these rules at its
507
816
  * own call, so they throw here only for an argument a lifecycle hook declared.
508
817
  */
509
- function collectArguments(state, subject) {
818
+ function collectArguments(state, command) {
510
819
  const slots = [];
511
- const seen = new Set();
820
+ const declared = [];
821
+ const judged = { ...command, subject: commandSubject(command.name) };
512
822
  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);
823
+ checkArgumentName(judged, declared, input);
824
+ declared.push(input);
520
825
  slots.push(slotOf(input));
521
826
  }
522
827
  for (let index = 0; index + 1 < slots.length; index += 1) {
523
828
  const slot = slots[index];
524
829
  const next = slots[index + 1];
525
830
  if (slot && next) {
526
- checkSlotOrder(slot, next, subject);
831
+ checkSlotOrder(slot, next, judged);
527
832
  }
528
833
  }
529
834
  return slots;
@@ -564,23 +869,36 @@ function recordEntries(record) {
564
869
  * One `views` entry under the unit its declaration named, read back as the shape its own functions
565
870
  * name. The two shapes are exclusive, and a row view answers a rows declaration alone.
566
871
  */
567
- function resultView(declaration, name, entry) {
568
- const { kind, sentence } = declaration;
872
+ function resultView(kind, site, entry) {
873
+ const { call, key, path, sentence } = site;
874
+ const findings = [resultFinding(path, call, { at: entryMark(call, key) })];
569
875
  const whole = isWholeView(entry);
570
876
  const row = isRowView(entry);
571
877
  if (whole && row) {
572
- throw new DeclarationError(`${sentence} names view "${name}" with render and row. Supply one of the two.`);
878
+ throw new DeclarationError(viewShape, {
879
+ correction: 'Supply one of the two.',
880
+ findings,
881
+ sentence: `${sentence} names view ${quoted(key)} with render and row.`,
882
+ });
573
883
  }
574
884
  if (row) {
575
885
  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().`);
886
+ throw new DeclarationError(rowViewOnValue, {
887
+ correction: 'Supply a view with render, or declare the result with rows().',
888
+ findings,
889
+ sentence: `${sentence} names row view ${quoted(key)} on a value result.`,
890
+ });
577
891
  }
578
892
  return entry;
579
893
  }
580
894
  if (whole) {
581
895
  return entry;
582
896
  }
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.`);
897
+ throw new DeclarationError(viewShape, {
898
+ correction: 'Supply a view with render or a row view with row.',
899
+ findings,
900
+ sentence: `${sentence} names view ${quoted(key)} with a value that is not a view.`,
901
+ });
584
902
  }
585
903
  /**
586
904
  * Every rule a results-lane call can judge on its own, applied to one Command's calls in the order
@@ -588,18 +906,31 @@ function resultView(declaration, name, entry) {
588
906
  * place and appends a new one, so each name keeps the position the call that first named it gave
589
907
  * it. A `default` once named persists through later calls that name none.
590
908
  */
591
- function mergeResult(name, results) {
592
- const sentence = commandSentence(name);
909
+ function mergeResult(command, results) {
910
+ const { path } = command;
911
+ const sentence = commandSentence(command.name);
593
912
  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.`);
913
+ const [declaration, second] = declarations;
914
+ if (declaration && second) {
915
+ throw new DeclarationError(multipleResults, {
916
+ correction: 'Declare one result() or rows() call.',
917
+ findings: [
918
+ resultFinding(path, declaration, { at: '0', note: 'the first result' }),
919
+ resultFinding(path, second, { at: '0', note: 'the second result' }),
920
+ ],
921
+ sentence: `${sentence} declares two results.`,
922
+ });
596
923
  }
597
- const declaration = declarations[0];
598
924
  // A `views()` call reshapes a result's views, so one with no result reshapes nothing.
599
925
  // The types publish the call where a result is carried, so this reaches a JavaScript author.
600
926
  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().`);
927
+ const [reshape] = results;
928
+ if (reshape) {
929
+ throw new DeclarationError(viewsWithoutResult, {
930
+ correction: 'Declare result() or rows() before action().',
931
+ findings: [resultFinding(path, reshape, { at: '0' })],
932
+ sentence: `${sentence} reshapes its views and declares no result.`,
933
+ });
603
934
  }
604
935
  return undefined;
605
936
  }
@@ -607,40 +938,57 @@ function mergeResult(name, results) {
607
938
  let selected = undefined;
608
939
  for (const call of results) {
609
940
  for (const [key, entry] of recordEntries(call.views)) {
610
- const view = resultView({ kind: declaration.kind, sentence }, key, entry);
941
+ const view = resultView(declaration.kind, { call, key, path, sentence }, entry);
611
942
  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.`);
943
+ throw new DeclarationError(viewName, {
944
+ correction: 'Use a nonempty name without whitespace, a leading hyphen, or "=", and not a number.',
945
+ findings: [resultFinding(path, call, { at: entryMark(call, key) })],
946
+ sentence: `${sentence} names view ${quoted(key)}.`,
947
+ });
613
948
  }
614
949
  views.set(key, view);
615
950
  }
616
- if (call.kind === 'views') {
617
- selected = selectedKey(call.default) ?? selected;
951
+ const key = call.kind === 'views' ? selectedKey(call.default) : undefined;
952
+ if (key !== undefined) {
953
+ selected = { call, key };
618
954
  }
619
955
  }
620
- return { kind: declaration.kind, selected, views };
956
+ return { declaration, kind: declaration.kind, selected, views };
621
957
  }
622
958
  /**
623
959
  * The finished result: the merged record, which needs an action, at least one view, and a default
624
960
  * that names one of them. The first key answers until one is named.
625
961
  */
626
- function buildResult(declared, hasAction) {
627
- const merged = mergeResult(declared.name, declared.results);
962
+ function buildResult(declared, hasAction, path) {
963
+ const merged = mergeResult({ name: declared.name, path }, declared.results);
628
964
  if (!merged) {
629
965
  return undefined;
630
966
  }
631
967
  const sentence = commandSentence(declared.name);
968
+ const { declaration, selected, views } = merged;
632
969
  if (!hasAction) {
633
- throw new DeclarationError(`${sentence} declares a result and no action. Register an action or remove the result.`);
970
+ throw new DeclarationError(resultWithoutAction, {
971
+ correction: 'Register an action or remove the result.',
972
+ findings: [resultFinding(path, declaration, { at: '0' })],
973
+ sentence: `${sentence} declares a result and no action.`,
974
+ });
634
975
  }
635
- const { selected, views } = merged;
636
976
  const first = views.keys().next();
637
977
  if (first.done === true) {
638
- throw new DeclarationError(`${sentence} declares a result with no views. Name at least one view.`);
978
+ throw new DeclarationError(resultWithoutViews, {
979
+ correction: 'Name at least one view.',
980
+ findings: [resultFinding(path, declaration, { at: '0.views' })],
981
+ sentence: `${sentence} declares a result with no views.`,
982
+ });
639
983
  }
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.`);
984
+ if (selected !== undefined && !views.has(selected.key)) {
985
+ throw new DeclarationError(unknownDefaultView, {
986
+ correction: 'Name the view or select a named one.',
987
+ findings: [resultFinding(path, selected.call, { at: '1.default' })],
988
+ sentence: `${sentence} selects default view ${quoted(selected.key)}, which it does not name.`,
989
+ });
642
990
  }
643
- return { default: selected ?? first.value, kind: merged.kind, views };
991
+ return { default: selected?.key ?? first.value, kind: merged.kind, views };
644
992
  }
645
993
  /** The values one build made, so a value a hook returns is one of them and never a forged shape. */
646
994
  const attachments = new WeakMap();
@@ -663,7 +1011,7 @@ class AttachedCommandValue {
663
1011
  #result;
664
1012
  constructor(state) {
665
1013
  this.#state = state;
666
- this.#result = resultNode(buildResult(state.declared, state.hasAction));
1014
+ this.#result = resultNode(buildResult(state.declared, state.hasAction, state.path));
667
1015
  attachments.set(this, state);
668
1016
  Object.freeze(this);
669
1017
  }
@@ -690,17 +1038,13 @@ class AttachedCommandValue {
690
1038
  return publishStore(this.#state.extensions);
691
1039
  }
692
1040
  argument(name, config) {
693
- return this.#declare({ config: captureConfig(config), kind: 'argument', name });
1041
+ return this.#declare({ config, kind: 'argument', name });
694
1042
  }
695
1043
  option(name, config) {
696
- return this.#declare({ config: captureConfig(config), kind: 'option', name });
1044
+ return this.#declare({ config, kind: 'option', name });
697
1045
  }
698
1046
  views(replacements, options) {
699
- const call = {
700
- default: recordOf(options, 'default'),
701
- kind: 'views',
702
- views: replacements,
703
- };
1047
+ const call = viewsCall(replacements, options);
704
1048
  const { declared } = this.#state;
705
1049
  return this.#derive({ call, kind: 'views' }, { ...declared, results: [...declared.results, call] });
706
1050
  }
@@ -714,6 +1058,7 @@ class AttachedCommandValue {
714
1058
  const layer = validateLayer({
715
1059
  declared: values,
716
1060
  descriptors: registry,
1061
+ site: { at: '', declaration: { arguments: values, call: 'extend', path: state.path } },
717
1062
  subject: state.subject,
718
1063
  target: 'command',
719
1064
  });
@@ -724,8 +1069,22 @@ class AttachedCommandValue {
724
1069
  });
725
1070
  }
726
1071
  /** One input the running hook declared, which the Command's own names now hold. */
727
- #declare(input) {
1072
+ #declare(raw) {
728
1073
  const { declared, identity } = this.#state;
1074
+ const { path } = this.#state;
1075
+ // The name is judged before the config, as Command and Application judge it.
1076
+ if (raw.kind === 'argument') {
1077
+ const { name } = declared;
1078
+ checkArgumentName({ name, path, subject: commandSubject(name) }, [], raw);
1079
+ }
1080
+ else {
1081
+ checkOptionName(raw.name, configUnread(inputSite(declared.name, path, raw)));
1082
+ }
1083
+ // Each kind captures its own config, so the input keeps the pairing its kind declares.
1084
+ const place = { call: raw.kind, path };
1085
+ const input = raw.kind === 'argument'
1086
+ ? { ...raw, config: captureInputConfig(raw, place) }
1087
+ : { ...raw, config: captureInputConfig(raw, place) };
729
1088
  return this.#derive({ identity, input, kind: 'input' }, { ...declared, inputs: [...declared.inputs, input] });
730
1089
  }
731
1090
  #derive(call, declared) {
@@ -734,9 +1093,13 @@ class AttachedCommandValue {
734
1093
  }
735
1094
  }
736
1095
  _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);
1096
+ /** The finding for the hook one plugin declared, as `plugin(identity, { onCommandAttach })`. */
1097
+ function hookFinding(identity, hook) {
1098
+ return {
1099
+ arguments: [identity, { onCommandAttach: hook }],
1100
+ call: 'plugin',
1101
+ mark: '1.onCommandAttach',
1102
+ };
740
1103
  }
741
1104
  /** One hook's own call, whose failure is the plugin's, and whose own report is its own. */
742
1105
  function callHook(value, hook, named) {
@@ -748,7 +1111,11 @@ function callHook(value, hook, named) {
748
1111
  if (error instanceof DeclarationError) {
749
1112
  throw error;
750
1113
  }
751
- throw new DeclarationError(`Plugin "${named.identity}" failed in onCommandAttach for ${named.subject}: ${attachReason(error)}.`);
1114
+ throw new DeclarationError(brokenAttachHook, {
1115
+ correction: 'Return the value the hook received or a value derived from it, and throw only a DeclarationError from the hook.',
1116
+ findings: [hookFinding(named.identity, hook)],
1117
+ sentence: `Plugin ${quoted(named.identity)} failed in onCommandAttach for ${named.subject}: ${asSentence(reasonOf(error))}`,
1118
+ }, { cause: error });
752
1119
  }
753
1120
  }
754
1121
  /**
@@ -759,7 +1126,11 @@ function callHook(value, hook, named) {
759
1126
  function attachOnce(value, hook, named) {
760
1127
  const state = attachments.get(callHook(value, hook, named));
761
1128
  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.`);
1129
+ throw new DeclarationError(brokenAttachHook, {
1130
+ correction: 'Return the value it received or a value derived from it.',
1131
+ findings: [hookFinding(named.identity, hook)],
1132
+ sentence: `Plugin ${quoted(named.identity)} returned a value that is not the attached Command from onCommandAttach for ${named.subject}.`,
1133
+ });
763
1134
  }
764
1135
  return state;
765
1136
  }
@@ -817,10 +1188,24 @@ function applyAttachCalls(state, calls) {
817
1188
  }
818
1189
  return next;
819
1190
  }
1191
+ /**
1192
+ * The reason rule one name collision breaks.
1193
+ * Two options or two arguments break the rule the application's own collision of the pair breaks.
1194
+ * An argument and an option under one name break name-shared-across-kinds, which only a hook reaches.
1195
+ */
1196
+ function nameCollisionRule(declared, held) {
1197
+ if (declared !== held) {
1198
+ return nameSharedAcrossKinds;
1199
+ }
1200
+ return declared === 'option' ? optionDeclaredTwice : argumentDeclaredTwice;
1201
+ }
820
1202
  /** The clause and the remedy an input another plugin's hook already declared earns. */
821
- function hookClause(kind, identity) {
1203
+ function hookClause(earlier, path) {
1204
+ const { identity, input } = earlier;
822
1205
  return {
823
- clause: `an ${kind} plugin "${identity}" declared through onCommandAttach`,
1206
+ clause: `an ${input.kind} plugin ${quoted(identity)} declared through onCommandAttach`,
1207
+ held: inputFinding(path, input, declarerNote(identity)),
1208
+ kind: input.kind,
824
1209
  remedy: 'Install one of them.',
825
1210
  };
826
1211
  }
@@ -842,6 +1227,25 @@ function attachedRemedy(target) {
842
1227
  }
843
1228
  return 'Install one of them.';
844
1229
  }
1230
+ /** The collision with one option the globals table holds, a global or a plugin's own option. */
1231
+ function tableCollision(entry) {
1232
+ const { owner, site } = entry;
1233
+ if (owner.kind === 'plugin') {
1234
+ const clause = `an option of plugin ${quoted(owner.identity)}`;
1235
+ return {
1236
+ clause,
1237
+ held: siteFinding(site, site.named, clause),
1238
+ kind: 'option',
1239
+ remedy: attachedRemedy('plugin'),
1240
+ };
1241
+ }
1242
+ return {
1243
+ clause: 'a global option',
1244
+ held: siteFinding(site, site.named, 'the global option'),
1245
+ kind: 'option',
1246
+ remedy: attachedRemedy('global'),
1247
+ };
1248
+ }
845
1249
  /**
846
1250
  * What one name a hook-declared input of either kind collides with, in the order the scopes are
847
1251
  * reported: an earlier hook's option, an earlier hook's argument, a local option, a global or
@@ -850,32 +1254,36 @@ function attachedRemedy(target) {
850
1254
  */
851
1255
  function attachedCollision(input, held) {
852
1256
  const { name } = input;
853
- const hook = held.hooks.get(name);
1257
+ const { path } = held;
1258
+ const hook = held.hooks.get(name) ?? held.hookArguments.get(name);
854
1259
  if (hook !== undefined) {
855
- return hookClause('option', hook);
856
- }
857
- const hooked = held.hookArguments.get(name);
858
- if (hooked !== undefined) {
859
- return hookClause('argument', hooked);
1260
+ return hookClause(hook, path);
860
1261
  }
861
- if (held.locals.has(name)) {
862
- return { clause: 'a local option', remedy: attachedRemedy('local') };
1262
+ const local = held.locals.get(name);
1263
+ if (local !== undefined) {
1264
+ return {
1265
+ clause: 'a local option',
1266
+ held: inputFinding(path, local, 'the local option'),
1267
+ kind: 'option',
1268
+ remedy: attachedRemedy('local'),
1269
+ };
863
1270
  }
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) };
1271
+ const entry = held.globals.names.get(name);
1272
+ if (entry) {
1273
+ return tableCollision(entry);
869
1274
  }
870
- return held.arguments.has(name)
871
- ? { clause: 'an argument', remedy: attachedRemedy('argument') }
872
- : undefined;
1275
+ const argument = held.arguments.get(name);
1276
+ return argument === undefined
1277
+ ? undefined
1278
+ : {
1279
+ clause: 'an argument',
1280
+ held: inputFinding(path, argument, 'the argument'),
1281
+ kind: 'argument',
1282
+ remedy: attachedRemedy('argument'),
1283
+ };
873
1284
  }
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) {
1285
+ /** The spellings one compiled table holds, each with its form and the place that declared it. */
1286
+ function readSpellings(table, claimed, placeOf) {
879
1287
  const longs = new Map();
880
1288
  for (const [spelling, option] of table) {
881
1289
  if (option.role === 'long') {
@@ -884,53 +1292,107 @@ function readSpellings(table, claimed) {
884
1292
  }
885
1293
  for (const [spelling, option] of table) {
886
1294
  if (!claimed.has(spelling)) {
887
- claimed.set(spelling, longs.get(option.name) ?? spelling);
1295
+ const form = longs.get(option.name) ?? spelling;
1296
+ claimed.set(spelling, { finding: placeOf(option.name, option.role), form });
888
1297
  }
889
1298
  }
890
1299
  }
1300
+ /** The place one input's spelling sits: the key of its call that yields the spelling. */
1301
+ function spellingPlace(site, role, note) {
1302
+ return siteFinding(site, spellingMark(site, role), note);
1303
+ }
891
1304
  /** One hook-declared option's spellings, against every spelling the table already claims. */
892
1305
  function checkAttachedSpelling(declared, named) {
893
- const { claimed, subject } = named;
894
- const table = compileOptions([declared.input], subject);
895
- for (const [spelling] of table) {
1306
+ const { claimed, scope } = named;
1307
+ const { identity, input } = declared;
1308
+ const { subject } = scope;
1309
+ const site = scope.siteOf(input);
1310
+ const table = compileOptions([input], scope);
1311
+ for (const [spelling, option] of table) {
896
1312
  const used = claimed.get(spelling);
897
1313
  if (used !== undefined) {
898
- throw new DeclarationError(`Plugin "${declared.identity}" declares option "${declared.input.name}" with spelling "${spelling}" on ${subject}, which "${used}" already uses.`);
1314
+ throw new DeclarationError(spellingTaken, {
1315
+ correction: 'Change one of the two spellings or omit the plugin.',
1316
+ findings: [
1317
+ spellingPlace(site, option.role, declarerNote(identity)),
1318
+ ...(used.finding === undefined ? [] : [used.finding]),
1319
+ ],
1320
+ sentence: `Plugin ${quoted(identity)} declares option ${quoted(input.name)} with spelling ${quoted(spelling)} on ${subject}, which ${quoted(used.form)} already uses.`,
1321
+ });
1322
+ }
1323
+ }
1324
+ readSpellings(table, claimed, (_name, role) => spellingPlace(site, role, declarerNote(identity)));
1325
+ }
1326
+ /** The declarations of one kind, keyed by name, the first of each name winning. */
1327
+ function byName(inputs) {
1328
+ const named = new Map();
1329
+ for (const input of inputs) {
1330
+ if (!named.has(input.name)) {
1331
+ named.set(input.name, input);
899
1332
  }
900
1333
  }
901
- readSpellings(table, claimed);
1334
+ return named;
1335
+ }
1336
+ /** Where the globals table's options declared their spellings, a global or a plugin's option. */
1337
+ function tableSpellings(globals) {
1338
+ return (name, role) => {
1339
+ const entry = globals.names.get(name);
1340
+ if (!entry) {
1341
+ return undefined;
1342
+ }
1343
+ const { owner, site } = entry;
1344
+ const note = owner.kind === 'plugin'
1345
+ ? `an option of plugin ${quoted(owner.identity)}`
1346
+ : `the global option ${quoted(name)}`;
1347
+ return spellingPlace(site, role, note);
1348
+ };
902
1349
  }
903
1350
  /**
904
1351
  * Every input a hook declared, against the names and spellings the Command, the globals table, and
905
1352
  * 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.
1353
+ * input reports as the plugin's fault and never as the author's. A collision carries a finding for
1354
+ * the hook's input and one for the declaration that already holds the name or spelling.
907
1355
  */
908
- function checkAttachedInputs(declared, attached, globals) {
909
- const subject = commandSubject(declared.name);
1356
+ function checkAttachedInputs(declared, attached, place) {
1357
+ const { globals, path } = place;
1358
+ const scope = localScope(declared.name, path);
1359
+ const { subject } = scope;
910
1360
  const hooked = new Set(attached.map((entry) => entry.input));
911
1361
  const authored = declared.inputs.filter((input) => !hooked.has(input));
912
1362
  const options = authored.filter((input) => input.kind === 'option');
1363
+ const locals = byName(options);
913
1364
  const held = {
914
- arguments: new Set(authored.filter((input) => input.kind === 'argument').map((input) => input.name)),
1365
+ arguments: byName(authored.filter((input) => input.kind === 'argument')),
915
1366
  globals,
916
1367
  hookArguments: new Map(),
917
1368
  hooks: new Map(),
918
- locals: new Set(options.map((input) => input.name)),
1369
+ locals,
1370
+ path,
919
1371
  };
920
1372
  const claimed = new Map();
921
- readSpellings(globals.options, claimed);
922
- readSpellings(compileOptions(options, subject), claimed);
923
- for (const { identity, input } of attached) {
1373
+ readSpellings(globals.options, claimed, tableSpellings(globals));
1374
+ readSpellings(compileOptions(options, scope), claimed, (name, role) => {
1375
+ const local = locals.get(name);
1376
+ return local === undefined || local.kind !== 'option'
1377
+ ? undefined
1378
+ : spellingPlace(scope.siteOf(local), role, `the local option ${quoted(name)}`);
1379
+ });
1380
+ for (const entry of attached) {
1381
+ const { identity, input } = entry;
924
1382
  const collision = attachedCollision(input, held);
925
1383
  if (collision) {
926
- throw new DeclarationError(`Plugin "${identity}" declares ${input.kind} "${input.name}" on ${subject}, which is already declared as ${collision.clause}. ${collision.remedy}`);
1384
+ throw new DeclarationError(nameCollisionRule(input.kind, collision.kind), {
1385
+ correction: collision.remedy,
1386
+ findings: [inputFinding(path, input, declarerNote(identity)), collision.held],
1387
+ sentence: `Plugin ${quoted(identity)} declares ${input.kind} ${quoted(input.name)} on ${subject}, which is already declared as ${collision.clause}.`,
1388
+ });
927
1389
  }
928
1390
  if (input.kind === 'option') {
929
- checkAttachedSpelling({ identity, input }, { claimed, subject });
930
- held.hooks.set(input.name, identity);
1391
+ checkAttachedSpelling({ identity, input }, { claimed, scope });
1392
+ held.hooks.set(input.name, entry);
931
1393
  }
932
1394
  else {
933
- held.hookArguments.set(input.name, identity);
1395
+ held.hookArguments.set(input.name, entry);
934
1396
  }
935
1397
  }
936
1398
  }
@@ -969,29 +1431,31 @@ function bindDispatch(state, action, globals) {
969
1431
  function checkInputFacts(inputs, command, context) {
970
1432
  const { name, subject } = command;
971
1433
  for (const input of inputs) {
972
- const sentence = `${commandSentence(name)} ${input.kind} "${input.name}"`;
973
- checkDescription(sentence, input.config.description);
1434
+ const site = inputSite(name, context.path, input);
1435
+ const sentence = site.subject;
1436
+ checkDescription(site, input.config.description);
974
1437
  if (input.kind === 'argument') {
975
- checkNoListingFacts(sentence, input.config);
976
- checkNoArgumentBinding(sentence, input.config);
1438
+ checkNoListingFacts(site, input.config);
1439
+ checkNoArgumentBinding(site, input.config);
977
1440
  }
978
1441
  else {
979
- checkHidden(sentence, input.config.hidden);
980
- checkDeprecated(sentence, input.config.deprecated);
981
- checkEnvBinding(sentence, input.config);
1442
+ checkHidden(site, input.config.hidden);
1443
+ checkDeprecated(site, input.config.deprecated);
1444
+ checkEnvBinding(site, input.config);
982
1445
  }
983
1446
  context.extensions.set(input, buildExtensions({
984
1447
  declared: input.config.extensions,
985
1448
  descriptors: context.descriptors,
1449
+ site: { ...site, at: '1.extensions' },
986
1450
  subject: { phrase: `on ${subject} ${input.kind} "${input.name}"`, sentence },
987
1451
  target: input.kind,
988
1452
  }));
989
1453
  }
990
1454
  }
991
1455
  /** One Command declares arguments or attaches children, whichever declaration made each of them. */
992
- function checkArgumentPlacement(name, slot, child) {
1456
+ function checkArgumentPlacement(command, slot, child) {
993
1457
  if (slot && child) {
994
- throw placementFault(name, slot.input.name, child.name);
1458
+ throw placementFault(command, slot.input, child);
995
1459
  }
996
1460
  }
997
1461
  /**
@@ -1008,7 +1472,8 @@ export function buildCommand(state, context) {
1008
1472
  for (const [input, record] of state.records) {
1009
1473
  context.extensions.set(input, record);
1010
1474
  }
1011
- const declaredSlots = collectArguments(state, subject);
1475
+ const place = { name, path: context.path };
1476
+ const declaredSlots = collectArguments(state, place);
1012
1477
  const hasAction = action !== undefined;
1013
1478
  /**
1014
1479
  * The hooks run once the author's declaration is complete, and each value a hook receives resolves
@@ -1027,15 +1492,15 @@ export function buildCommand(state, context) {
1027
1492
  checkInputFacts(hookInputs.map((call) => call.input), { name, subject }, context);
1028
1493
  // The hook-declared names are checked first, so a collision reports in the plugin's voice.
1029
1494
  // A rule the author's own declaration voices never speaks for a name a hook declared.
1030
- checkAttachedInputs(hooked, hookInputs, globals);
1495
+ checkAttachedInputs(hooked, hookInputs, { globals, path: context.path });
1031
1496
  // Every value was validated once, the author's at each call and each hook's at its `extend()`.
1032
1497
  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);
1498
+ const slots = hookInputs.length > 0 ? collectArguments(hooked, place) : declaredSlots;
1499
+ checkArgumentPlacement(place, slots[0], state.children[0]);
1500
+ const result = checkFinished(hooked, hasAction, { path: context.path });
1501
+ const options = checkLocalOptions(optionsOf(hooked.inputs), globals, localScope(name, context.path));
1037
1502
  // A hook's erased calls answer the declaration rules an authored call answers at the call.
1038
- checkDeclarations(hookInputs.map((call) => call.input));
1503
+ checkDeclarations(hookInputs.map(({ input }) => ({ input, site: inputSite(name, context.path, input) })));
1039
1504
  const children = new Map();
1040
1505
  const routes = new Map();
1041
1506
  for (const child of state.children) {
@@ -1093,7 +1558,7 @@ export class CommandBuilder {
1093
1558
  }
1094
1559
  argument(name, config) {
1095
1560
  const input = {
1096
- config: captureConfig(config),
1561
+ config,
1097
1562
  kind: 'argument',
1098
1563
  name,
1099
1564
  };
@@ -1101,7 +1566,7 @@ export class CommandBuilder {
1101
1566
  }
1102
1567
  option(name, config) {
1103
1568
  const input = {
1104
- config: captureConfig(config),
1569
+ config,
1105
1570
  kind: 'option',
1106
1571
  name,
1107
1572
  };
@@ -1158,7 +1623,7 @@ _b = CommandBuilder;
1158
1623
  */
1159
1624
  class CommandDeclaration extends CommandBuilder {
1160
1625
  constructor(name, options) {
1161
- const declared = namedState(name, options);
1626
+ const declared = declaring(() => namedState(name, options));
1162
1627
  super(declared.name, declared.state);
1163
1628
  }
1164
1629
  }
@@ -1172,12 +1637,35 @@ export function collectInputs(command) {
1172
1637
  ];
1173
1638
  }
1174
1639
  /**
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.
1640
+ * Where every input of one graph was declared: each global option at its `globalOption()` call,
1641
+ * and each Command's own inputs at their calls on the Command, under its path from the root.
1642
+ */
1643
+ export function inputPlaces(graph) {
1644
+ const places = new Map();
1645
+ for (const input of graph.globals.inputs) {
1646
+ places.set(input, inputPlace(input, { global: true, path: [] }));
1647
+ }
1648
+ const walk = (command, path) => {
1649
+ for (const input of command.inputs) {
1650
+ places.set(input, inputPlace(input, { global: false, path }));
1651
+ }
1652
+ for (const [name, child] of command.children) {
1653
+ walk(child, [...path, name]);
1654
+ }
1655
+ };
1656
+ walk(graph.root, []);
1657
+ return places;
1658
+ }
1659
+ /**
1660
+ * The names a routing failure offers: the canonical names of the visible, current children, in
1661
+ * authoring order. A candidate list is a listing, so a hidden or a deprecated child is absent from
1662
+ * it, as completion leaves them out, and a parent whose children are all hidden or deprecated
1663
+ * offers none. A deprecated child typed in full still routes.
1178
1664
  */
1179
1665
  function candidatesOf(command) {
1180
- return [...command.children].filter(([, child]) => !child.hidden).map(([name]) => name);
1666
+ return [...command.children]
1667
+ .filter(([, child]) => !child.hidden && child.deprecated === undefined)
1668
+ .map(([name]) => name);
1181
1669
  }
1182
1670
  /**
1183
1671
  * Whether a token names one of the Command's children: the Command has children and the token is
@@ -1186,8 +1674,12 @@ function candidatesOf(command) {
1186
1674
  export function readsAsChild(command, token) {
1187
1675
  return command.children.size > 0 && !isOptionToken(token);
1188
1676
  }
1189
- /** Bare tokens, names or aliases, select children until a Command has none; a hyphen commits. */
1190
- export function route(root, tokens) {
1677
+ /**
1678
+ * Bare tokens, names or aliases, select children until a Command has none; a hyphen commits.
1679
+ * `walked` receives the path after each name routes, so an unknown Command leaves its caller
1680
+ * holding the partial path walked before it.
1681
+ */
1682
+ export function route(root, tokens, walked) {
1191
1683
  let command = root;
1192
1684
  const path = [];
1193
1685
  let index = 0;
@@ -1202,6 +1694,7 @@ export function route(root, tokens) {
1202
1694
  // An alias routes like the canonical name, and the path it walks reports that name alone.
1203
1695
  command = child.command;
1204
1696
  path.push(child.name);
1697
+ walked?.(Object.freeze([...path]));
1205
1698
  index += 1;
1206
1699
  }
1207
1700
  return { command, path, tokens: tokens.slice(index) };
@@ -1240,10 +1733,13 @@ export function argumentSlot(slots, position) {
1240
1733
  const last = slots.at(-1);
1241
1734
  return slots[position] ?? (last?.variadic ? last : undefined);
1242
1735
  }
1243
- /** Consumes the globals table, then routes the remaining bare tokens to a Command. */
1244
- export function routeInvocation(graph, argv) {
1736
+ /**
1737
+ * Consumes the globals table, then routes the remaining bare tokens to a Command. `walked`
1738
+ * receives the path as routing extends it, the partial path of an unknown Command included.
1739
+ */
1740
+ export function routeInvocation(graph, argv, walked) {
1245
1741
  const scan = extractGlobals(graph.globals.options, [...argv]);
1246
- const routed = route(graph.root, scan.rest);
1742
+ const routed = route(graph.root, scan.rest, walked);
1247
1743
  return {
1248
1744
  command: routed.command,
1249
1745
  path: routed.path,
@@ -1306,7 +1802,7 @@ function requestNode(graph, path, place) {
1306
1802
  const options = place.global ? graph.globals : nodeAt(graph, path).options;
1307
1803
  const node = options.find((option) => option.name === place.name);
1308
1804
  if (!node) {
1309
- throw new InternalError(`Option "${place.name}" is not in the inspected graph.`, undefined);
1805
+ throw graphMismatch(`Option "${place.name}" is not in the inspected graph.`);
1310
1806
  }
1311
1807
  return node;
1312
1808
  }
@@ -1330,6 +1826,7 @@ async function fillScope({ graph, invocation, routed }, local) {
1330
1826
  locals: local.kind === 'parsed'
1331
1827
  ? { global: false, inputs: optionsOf(routed.command.inputs), values: locals }
1332
1828
  : undefined,
1829
+ offer: invocation.offer,
1333
1830
  out: invocation.sourceOut,
1334
1831
  plugins: graph.globals.plugins,
1335
1832
  request: (input, global) => requestNode(invocation.inspected(), routed.path, { global, name: input.name }),
@@ -1426,6 +1923,7 @@ export async function prepareDispatch(graph, routed, invocation) {
1426
1923
  return { ...ready, globals, kind: 'ready', result };
1427
1924
  }
1428
1925
  catch (error) {
1429
- return held(error);
1926
+ // The fault is held as a failure, so a throw from reading a validator's output is never offered.
1927
+ return held(toFailure(error));
1430
1928
  }
1431
1929
  }