@loomcli/core 0.4.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 (79) hide show
  1. package/LICENSE +21 -0
  2. package/dist/application.d.ts +60 -31
  3. package/dist/application.js +356 -149
  4. package/dist/bindings.d.ts +31 -0
  5. package/dist/bindings.js +64 -0
  6. package/dist/chain.d.ts +26 -12
  7. package/dist/chain.js +59 -92
  8. package/dist/command-rules.d.ts +55 -0
  9. package/dist/command-rules.js +142 -0
  10. package/dist/command.d.ts +212 -93
  11. package/dist/command.js +1224 -454
  12. package/dist/controls.d.ts +8 -0
  13. package/dist/controls.js +23 -0
  14. package/dist/defect.d.ts +18 -0
  15. package/dist/defect.js +272 -0
  16. package/dist/developer.d.ts +24 -0
  17. package/dist/developer.js +52 -0
  18. package/dist/diagnostic-text.d.ts +81 -0
  19. package/dist/diagnostic-text.js +283 -0
  20. package/dist/diagnostic.d.ts +11 -0
  21. package/dist/diagnostic.js +70 -0
  22. package/dist/errors.d.ts +115 -26
  23. package/dist/errors.js +333 -54
  24. package/dist/exit-codes.d.ts +45 -0
  25. package/dist/exit-codes.js +46 -0
  26. package/dist/extension.d.ts +44 -9
  27. package/dist/extension.js +147 -65
  28. package/dist/facts.d.ts +71 -11
  29. package/dist/facts.js +108 -25
  30. package/dist/globals.d.ts +63 -22
  31. package/dist/globals.js +164 -39
  32. package/dist/hints.d.ts +79 -0
  33. package/dist/hints.js +247 -0
  34. package/dist/host.d.ts +13 -0
  35. package/dist/host.js +43 -1
  36. package/dist/identity.d.ts +19 -0
  37. package/dist/identity.js +72 -0
  38. package/dist/index.d.ts +15 -4
  39. package/dist/index.js +6 -0
  40. package/dist/input-rules.d.ts +64 -0
  41. package/dist/input-rules.js +145 -0
  42. package/dist/inspect.d.ts +36 -5
  43. package/dist/inspect.js +125 -27
  44. package/dist/lanes.js +1 -1
  45. package/dist/locate.d.ts +41 -0
  46. package/dist/locate.js +121 -0
  47. package/dist/options.d.ts +96 -2
  48. package/dist/options.js +259 -71
  49. package/dist/output.d.ts +11 -2
  50. package/dist/output.js +23 -3
  51. package/dist/plain.d.ts +6 -0
  52. package/dist/plain.js +12 -0
  53. package/dist/plugin-rules.d.ts +62 -0
  54. package/dist/plugin-rules.js +155 -0
  55. package/dist/plugin.d.ts +121 -55
  56. package/dist/plugin.js +496 -126
  57. package/dist/prototypes.d.ts +7 -0
  58. package/dist/prototypes.js +29 -0
  59. package/dist/rendering.d.ts +6 -1
  60. package/dist/rendering.js +23 -5
  61. package/dist/rules.d.ts +51 -0
  62. package/dist/rules.js +115 -0
  63. package/dist/sequence.js +6 -1
  64. package/dist/sources.d.ts +58 -0
  65. package/dist/sources.js +258 -0
  66. package/dist/style-wire.js +1 -1
  67. package/dist/style.js +1 -1
  68. package/dist/theme.d.ts +4 -0
  69. package/dist/theme.js +25 -5
  70. package/dist/thenable.d.ts +15 -0
  71. package/dist/thenable.js +29 -0
  72. package/dist/translators.d.ts +69 -0
  73. package/dist/translators.js +253 -0
  74. package/dist/types.d.ts +76 -21
  75. package/dist/validation.d.ts +65 -10
  76. package/dist/validation.js +314 -108
  77. package/dist/view.d.ts +49 -15
  78. package/dist/view.js +157 -79
  79. package/package.json +3 -2
package/dist/command.js CHANGED
@@ -1,117 +1,443 @@
1
- var _a;
2
- import { commandSentence, commandSubject, DeclarationError, NonCallableCommandError, ResultError, UnexpectedArgumentError, UnknownCommandError, } from './errors.js';
3
- import { buildExtensions, extendStore, publishStore, storeCommandLayers, validateLayer, } from './extension.js';
4
- import { checkDeprecated, checkDescription, checkHidden, checkNoListingFacts, isPlainObject, } from './facts.js';
5
- import { buildGlobals, keyCollision, spellingCollision } from './globals.js';
6
- import { resultNode, snapshot } from './inspect.js';
7
- import { compileOptions, extractGlobals, mergeValues, parseInputs } from './options.js';
8
- import { captureConfig, validateValues } from './validation.js';
9
- /** Authored values register here, so the public type publishes no state to reach or replace. */
1
+ var _a, _b;
2
+ import { checkEnvBinding, checkNoArgumentBinding } from './bindings.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';
6
+ import { buildExtensions, extendStore, publishStore, registerDescriptor, storeCommandLayers, validateLayer, } from './extension.js';
7
+ import { checkDeprecated, checkDescription, checkHidden, checkNoListingFacts, siteFinding, } from './facts.js';
8
+ import { buildGlobals, checkLocalOptions } from './globals.js';
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';
14
+ import { fillInputs } from './sources.js';
15
+ import { captureConfig, checkInputConfig, checkDeclarations, declaringSite, inputPlace, validateValues, } from './validation.js';
16
+ /**
17
+ * The deepest level below the root a Command may sit at, and the word its diagnostic spells it with.
18
+ * A child of the root sits at level 1. Raising the cap relaxes a rule and breaks no application.
19
+ */
20
+ const nestingCap = { depth: 2, words: 'two' };
21
+ /**
22
+ * The shallowest level a parent can sit at: the root is level 0, and a named Command is attached
23
+ * somewhere below it, so it sits at level 1 or deeper.
24
+ */
25
+ function parentLevel(name) {
26
+ return name === null ? 0 : 1;
27
+ }
28
+ /**
29
+ * Each authored value registers its handle here, so the public value holds no state to reach or
30
+ * replace.
31
+ */
10
32
  const nodes = new WeakMap();
11
- /** Reads the declarations behind an attached value; anything else is a declaration error. */
12
- function nodeOf(parent, child) {
13
- const node = nodes.get(child);
14
- if (!node) {
15
- throw new DeclarationError(`${commandSentence(parent)} attaches a value that is not a Command. Attach the value returned by new Command(name).`);
33
+ /**
34
+ * The handle one Command value registers, closed over its state. The value itself carries no
35
+ * member that reaches the state, so an attached Command stays final.
36
+ */
37
+ function nodeHandle(name, state) {
38
+ return Object.freeze({
39
+ build: (context) => buildCommand(state, context),
40
+ declared: state,
41
+ hasAction: state.action !== undefined,
42
+ name,
43
+ });
44
+ }
45
+ /** The handle behind a value an author built as a Command, or nothing for any other value. */
46
+ export function commandNode(value) {
47
+ return typeof value === 'object' && value !== null ? nodes.get(value) : undefined;
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;
16
61
  }
17
- return node;
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)})`);
18
67
  }
19
- /** Reject retired globals wiring at graph build, before any invocation reads the options. */
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
+ }
76
+ /** A named Command's options slot holds a plain options object, and never retired globals wiring. */
20
77
  function checkCommandOptions(name, options) {
21
78
  if (options !== undefined && !isPlainObject(options)) {
22
- 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
+ });
23
84
  }
24
85
  if (isPlainObject(options) && 'globals' in options) {
25
- 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
+ });
26
91
  }
27
92
  }
28
- /** One name rule for every declared name in the graph, so a child and an argument read alike. */
93
+ /** One name rule for an argument, option, or view name: a bare token the parser can read. */
29
94
  function isDeclaredName(name) {
30
95
  return typeof name === 'string' && Boolean(name) && !name.startsWith('-') && !/[\s=]/u.test(name);
31
96
  }
32
- function checkChildName(parent, name) {
33
- if (!isDeclaredName(name)) {
34
- throw new DeclarationError(`${commandSentence(parent)} attaches a child named "${String(name)}". Use a nonempty name without a leading hyphen, whitespace, or "=".`);
35
- }
97
+ /**
98
+ * The portable name rule, for every name an operator types as a command at a shell prompt: the
99
+ * application name, every Command name, and every alias. The characters are the POSIX portable
100
+ * filename set, and a name starts with neither `-`, which reads as an option, nor `.`, which a
101
+ * shell hides.
102
+ */
103
+ export function isPortableName(name) {
104
+ return typeof name === 'string' && /^[A-Za-z0-9_][A-Za-z0-9._-]*$/u.test(name);
36
105
  }
37
- /** An alias is a bare token the way a child name is, so it answers to the same name rule. */
38
- function checkAliasName(command, alias) {
39
- if (!isDeclaredName(alias)) {
40
- throw new DeclarationError(`${commandSentence(command)} declares an alias named "${String(alias)}". Use a nonempty name without a leading hyphen, whitespace, or "=".`);
106
+ /** The one correction every portable name diagnostic ends with. */
107
+ export const portableNameCorrection = 'Use a nonempty name of A-Z, a-z, 0-9, ".", "_", and "-" that does not start with "-" or ".".';
108
+ /** An alias is typed at the prompt the way a Command name is, so it answers to the portable rule. */
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
+ }
41
118
  }
42
119
  }
43
120
  /**
44
- * The state every declaration starts from. The unnamed root and each named Command share it.
45
- * The declaration values arrive captured, because a later change to the options object the author
46
- * passed changes nothing the declaration holds.
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
+ }
128
+ /** How the extension diagnostics of one Command's own layers name it. */
129
+ export function layerOf(name) {
130
+ return { phrase: `on ${commandSubject(name)}`, sentence: commandSentence(name) };
131
+ }
132
+ /**
133
+ * The state every declaration starts from. The unnamed root and each named Command share it. The
134
+ * declaration values arrive checked, because the constructor that read them threw for any fault.
47
135
  */
48
136
  export function freshState(declaration) {
49
137
  return {
50
- actions: [],
138
+ action: undefined,
51
139
  aliases: [],
52
140
  bind: () => ({ args: {}, options: {} }),
53
141
  children: [],
54
- deprecated: declaration.deprecated,
55
- description: declaration.description,
56
- extensions: [declaration.extensions],
57
- hidden: declaration.hidden,
142
+ descriptors: declaration.descriptors,
143
+ extensions: declaration.extensions,
144
+ facts: declaration.facts,
58
145
  inputs: [],
59
- late: [],
60
146
  name: declaration.name,
61
- options: declaration.options,
147
+ records: new Map(),
62
148
  results: [],
63
149
  };
64
150
  }
65
- /** A declaration after the action is an order fault; build reports the first one recorded. */
66
- function recordLate(state, declarations) {
67
- return state.actions.length > 0 ? [...state.late, ...declarations] : state.late;
151
+ /**
152
+ * A named Command's own declaration, checked before the value exists: the name, the options slot,
153
+ * the core facts, and the extension values the slot carries. A later change to the options object
154
+ * the author passed changes nothing the declaration holds.
155
+ */
156
+ function namedState(name, options) {
157
+ if (!isPortableName(name)) {
158
+ throw new DeclarationError(portableName, {
159
+ correction: portableNameCorrection,
160
+ findings: [constructorFinding(name, options, '0')],
161
+ sentence: `Command name ${quoted(name)} is invalid.`,
162
+ });
163
+ }
164
+ checkCommandOptions(name, options);
165
+ const slot = isPlainObject(options) ? options : undefined;
166
+ const site = {
167
+ at: '1',
168
+ declaration: { arguments: [name, options], call: 'new Command' },
169
+ subject: commandSentence(name),
170
+ };
171
+ // The facts are read in the order their diagnostics have always ranked.
172
+ const description = checkDescription(site, slot?.description);
173
+ const hidden = checkHidden(site, slot?.hidden);
174
+ const deprecated = checkDeprecated(site, slot?.deprecated);
175
+ const descriptors = new Map();
176
+ const extensions = storeCommandLayers({
177
+ descriptors,
178
+ layers: [slot?.extensions],
179
+ site: { ...site, at: '1.extensions' },
180
+ subject: layerOf(name),
181
+ });
182
+ return {
183
+ name,
184
+ state: freshState({
185
+ descriptors,
186
+ extensions,
187
+ facts: { deprecated, description, hidden },
188
+ name,
189
+ }),
190
+ };
191
+ }
192
+ /**
193
+ * A declaration call after `action()` is an order fault the receiver's own earlier call makes
194
+ * certain. The types remove the call for a TypeScript author; a JavaScript author meets it here.
195
+ */
196
+ function checkOpen(state, late) {
197
+ if (state.action !== undefined) {
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
+ });
211
+ }
212
+ }
213
+ /** The remedy a late argument or option earns. */
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
+ }
228
+ /** One Command declares arguments or attaches children, whichever call came second. */
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
+ });
238
+ }
239
+ /** The options one declaration list holds, in declaration order. */
240
+ function optionsOf(inputs) {
241
+ return inputs.filter((input) => input.kind === 'option');
242
+ }
243
+ /** The positional slot one argument fills. */
244
+ function slotOf(input) {
245
+ return {
246
+ input,
247
+ required: input.config.required === true,
248
+ variadic: input.config.variadic === true,
249
+ };
250
+ }
251
+ /**
252
+ * One input's extension values, validated at the call that declares it, and the descriptors they
253
+ * name joined to the declaration's own.
254
+ */
255
+ function recordInput(state, input) {
256
+ const { name } = state;
257
+ const descriptors = new Map(state.descriptors);
258
+ const record = buildExtensions({
259
+ declared: input.config.extensions,
260
+ descriptors,
261
+ site: { ...inputSite(name, pathOf(name), input), at: '1.extensions' },
262
+ subject: {
263
+ phrase: `on ${commandSubject(name)} ${input.kind} "${input.name}"`,
264
+ sentence: `${commandSentence(name)} ${input.kind} "${input.name}"`,
265
+ },
266
+ target: input.kind,
267
+ });
268
+ return { descriptors, records: new Map([...state.records, [input, record]]) };
269
+ }
270
+ /**
271
+ * Every rule one argument answers at its own call: its name, a repeated name, children beside it,
272
+ * its place after the arguments before it, its own facts, and its declaration rules.
273
+ */
274
+ function checkArgument(state, input) {
275
+ const { name } = state;
276
+ const path = pathOf(name);
277
+ const command = { name, path, subject: commandSubject(name) };
278
+ const declared = state.inputs.filter((entry) => entry.kind === 'argument');
279
+ checkArgumentName(command, declared, input);
280
+ const child = state.children[0];
281
+ if (child) {
282
+ throw placementFault(command, input, child);
283
+ }
284
+ const previous = declared.at(-1);
285
+ if (previous) {
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
+ });
314
+ }
68
315
  }
69
316
  /** The declared value joins `args` under its literal name, typed by its own config. */
70
- export function declareArgument(state, input) {
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
+ });
331
+ checkArgument(state, input);
332
+ const recorded = recordInput(state, input);
71
333
  const previous = state.bind;
72
334
  return {
73
335
  ...state,
336
+ ...recorded,
74
337
  bind: (values) => {
75
338
  const bound = previous(values);
76
339
  return { ...bound, args: { ...bound.args, ...values.argument(input) } };
77
340
  },
78
341
  inputs: [...state.inputs, input],
79
- late: recordLate(state, [{ input, kind: 'input' }]),
80
342
  };
81
343
  }
82
- /** The declared value joins `options` under its literal name, typed by its own config. */
83
- export function declareOption(state, input) {
344
+ /** The table a named Command's options meet at its own calls, before any Application holds it. */
345
+ const noGlobals = { names: new Map(), options: new Map(), variables: new Map() };
346
+ /**
347
+ * The declared value joins `options` under its literal name, typed by its own config. The root's
348
+ * options also meet the Application's globals table, which a named Command meets when its subtree
349
+ * joins an Application.
350
+ */
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);
368
+ const recorded = recordInput(state, input);
369
+ checkLocalOptions([...optionsOf(state.inputs), input], table, localScope(state.name, pathOf(state.name)));
370
+ checkDeclarations([{ input, site }]);
84
371
  const previous = state.bind;
85
372
  return {
86
373
  ...state,
374
+ ...recorded,
87
375
  bind: (values) => {
88
376
  const bound = previous(values);
89
377
  return { ...bound, options: { ...bound.options, ...values.option(input) } };
90
378
  },
91
379
  inputs: [...state.inputs, input],
92
- late: recordLate(state, [{ input, kind: 'input' }]),
93
- };
94
- }
95
- /** Globals close when composition starts; retain late calls for the shared build-order check. */
96
- export function recordGlobalOption(state, name) {
97
- return {
98
- ...state,
99
- late: state.actions.length > 0 || state.children.length > 0
100
- ? [...state.late, { kind: 'global', name }]
101
- : state.late,
102
380
  };
103
381
  }
104
- /** One call's names stay one group, so the empty call the types reject still reports as one. */
382
+ /**
383
+ * One call's names join the Command's aliases. A call that names none, which the types reject and
384
+ * a JavaScript author can still write, reports as the call it is.
385
+ */
105
386
  export function declareAlias(state, names) {
106
- return {
107
- ...state,
108
- aliases: [...state.aliases, names],
109
- late: recordLate(state, names.map((alias) => ({ alias, kind: 'alias' }))),
110
- };
387
+ const { name } = state;
388
+ const path = pathOf(name);
389
+ const first = names[0];
390
+ if (first === undefined) {
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
+ });
396
+ }
397
+ // The call's own input is judged before the receiver's state.
398
+ // A non-string name then reports as an alias name instead of failing to print in the order diagnostic.
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
+ });
406
+ const aliases = [...state.aliases];
407
+ for (const [index, alias] of names.entries()) {
408
+ const repeated = { arguments: names, call: 'alias', mark: String(index), path };
409
+ if (alias === name) {
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
+ });
415
+ }
416
+ if (aliases.includes(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
+ });
422
+ }
423
+ aliases.push(alias);
424
+ }
425
+ return { ...state, aliases };
111
426
  }
112
- /** Extension layers remain open after inputs and the action have been fixed. */
427
+ /**
428
+ * Extension layers remain open after inputs and the action have been fixed. Each layer is validated
429
+ * at its own call, against every descriptor the declaration already names.
430
+ */
113
431
  export function declareExtensions(state, values) {
114
- return { ...state, extensions: [...state.extensions, values] };
432
+ const descriptors = new Map(state.descriptors);
433
+ const layer = validateLayer({
434
+ declared: values,
435
+ descriptors,
436
+ site: { at: '', declaration: { arguments: values, call: 'extend', path: pathOf(state.name) } },
437
+ subject: layerOf(state.name),
438
+ target: 'command',
439
+ });
440
+ return { ...state, descriptors, extensions: extendStore(state.extensions, layer) };
115
441
  }
116
442
  /**
117
443
  * The same channel, read as the result the handler's own declaration names. The value one call
@@ -123,217 +449,360 @@ function declaredChannel(out) {
123
449
  }
124
450
  /**
125
451
  * The handler is typed against the result its own declaration carries, and the state holds one
126
- * list for every declaration, so the context each handler receives is read back at the call.
452
+ * action for every declaration, so the context the handler receives is read back at the call.
127
453
  */
128
454
  export function declareAction(state, handler) {
129
- const stored = (context) => handler({ ...context, out: declaredChannel(context.out) });
130
- return { ...state, actions: [...state.actions, stored] };
455
+ if (state.action !== undefined) {
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
+ });
469
+ }
470
+ // The graph and the routed node stay getters, because a spread would build the graph on every run.
471
+ const stored = (context) => handler({
472
+ args: context.args,
473
+ get command() {
474
+ return context.command;
475
+ },
476
+ get graph() {
477
+ return context.graph;
478
+ },
479
+ host: context.host,
480
+ options: context.options,
481
+ out: declaredChannel(context.out),
482
+ passthrough: context.passthrough,
483
+ signal: context.signal,
484
+ style: context.style,
485
+ });
486
+ return { ...state, action: stored };
131
487
  }
132
- /** The result declaration, which closes both result calls and reports lateness like the rest. */
488
+ /**
489
+ * The result declaration, which closes both result calls. Every rule its own call can judge throws
490
+ * here; the empty record and the default wait for attach, because `views()` can still add keys.
491
+ */
133
492
  export function declareResult(state, kind, declaration) {
134
- return {
135
- ...state,
136
- late: recordLate(state, [{ kind: 'result' }]),
137
- results: [...state.results, { kind, views: recordOf(declaration, 'views') }],
493
+ const call = {
494
+ arguments: callArguments(declaration),
495
+ kind,
496
+ views: recordOf(declaration, 'views'),
138
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);
506
+ return { ...state, results };
139
507
  }
140
508
  /** A `views()` call reshapes views and closes nothing, so it is never a late declaration. */
141
509
  export function declareResultViews(state, replacements, options) {
510
+ const results = [...state.results, viewsCall(replacements, options)];
511
+ mergeResult({ name: state.name, path: pathOf(state.name) }, results);
512
+ return { ...state, results };
513
+ }
514
+ /** One `views()` call as the results lane records it. */
515
+ function viewsCall(replacements, options) {
142
516
  return {
143
- ...state,
144
- results: [
145
- ...state.results,
146
- { default: recordOf(options, 'default'), kind: 'views', views: replacements },
147
- ],
517
+ arguments: callArguments(replacements, options),
518
+ default: recordOf(options, 'default'),
519
+ kind: 'views',
520
+ views: replacements,
148
521
  };
149
522
  }
150
- /** One property of an authoring argument, read defensively: build reports whatever it holds. */
523
+ /** One property of an authoring argument, read defensively: the rules report whatever it holds. */
151
524
  function recordOf(declaration, key) {
152
525
  return isPlainObject(declaration) ? declaration[key] : undefined;
153
526
  }
154
- /** Attaching is a declaration call too, so the receiver keeps the children it already had. */
155
- export function attachChild(state, child) {
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
+ }
540
+ /** The parent a declaration is, as attach reads it. */
541
+ function parentOf(state) {
156
542
  return {
157
- ...state,
158
- children: [...state.children, child],
159
- late: recordLate(state, [{ child, kind: 'child' }]),
543
+ argument: state.inputs.find((input) => input.kind === 'argument'),
544
+ children: state.children,
545
+ hasAction: state.action !== undefined,
546
+ name: state.name,
547
+ path: pathOf(state.name),
160
548
  };
161
549
  }
162
- /** The types remove a late call for TypeScript authors; JavaScript authors read it here. */
163
- function checkDeclarationOrder(state) {
164
- const { name } = state;
165
- const late = state.late[0];
166
- if (!late) {
167
- return;
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
+ }
558
+ /**
559
+ * One parent's namespace, which every canonical name and alias under it shares. The rule reads the
560
+ * same whichever sibling the author attached first.
561
+ */
562
+ function checkSiblings(parent, child) {
563
+ const sentence = commandSentence(parent.name);
564
+ const { children } = parent;
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
+ });
168
574
  }
169
- if (late.kind === 'global') {
170
- throw new DeclarationError(`The Application declares global option "${late.name}" after command() or action(). Declare global options before attaching Commands or registering an action.`);
575
+ for (const alias of child.node.declared.aliases) {
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
+ });
583
+ }
584
+ const owner = children.find((entry) => entry.node.declared.aliases.includes(alias));
585
+ if (owner) {
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
+ });
591
+ }
171
592
  }
172
- if (late.kind === 'input') {
173
- throw new DeclarationError(`${commandSentence(name)} declares ${late.input.kind} "${late.input.name}" after its action. Declare arguments and options before action().`);
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
+ });
600
+ }
601
+ }
602
+ /**
603
+ * A child at the cap holds no children. Attach reads the child at the shallowest level its parent
604
+ * can sit at, and the Application's join reads every node at its level below the root.
605
+ */
606
+ function checkNesting(parent, child, level) {
607
+ if (level >= nestingCap.depth && child.node.declared.children.length > 0) {
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
+ });
174
613
  }
175
- if (late.kind === 'alias') {
176
- throw new DeclarationError(`${commandSentence(name)} declares alias "${late.alias}" after its action. Declare aliases before action().`);
614
+ }
615
+ /**
616
+ * A Command without an action is a group, and routing sends an invocation on to one of its
617
+ * children. A group with no children receives an invocation no handler can answer, and a local
618
+ * option on a group reaches no handler either, because locals never inherit.
619
+ */
620
+ function checkGroup(state, place) {
621
+ const { name } = state;
622
+ if (state.children.length === 0) {
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
+ });
177
629
  }
178
- if (late.kind === 'result') {
179
- throw new DeclarationError(`${commandSentence(name)} declares its result after its action. Declare result() or rows() before action().`);
630
+ const option = state.inputs.find((input) => input.kind === 'option');
631
+ if (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
+ });
180
637
  }
181
- // Child identity and names are settled before this call, so the node and its name are valid.
182
- throw new DeclarationError(`${commandSentence(name)} attaches child "${String(nodeOf(name, late.child).name)}" after its action. Attach children before action().`);
183
638
  }
184
- /** Child names are checked before any child builds, so parent diagnostics come first. */
185
- function collectChildren(state) {
186
- const attached = [];
187
- const seen = new Set();
188
- for (const child of state.children) {
189
- const node = nodeOf(state.name, child);
190
- const name = node.name;
191
- checkChildName(state.name, name);
192
- if (seen.has(name)) {
193
- throw new DeclarationError(`${commandSentence(state.name)} attaches two children named "${name}". Rename or remove one.`);
194
- }
195
- seen.add(name);
196
- attached.push([name, node]);
639
+ /**
640
+ * The rules a finished Command answers, which attach applies to a child and build to the root: a
641
+ * result needs an action, its merged views record names at least one view and its default, and a
642
+ * Command without an action is a group. It answers with the resolved result.
643
+ */
644
+ function checkFinished(declared, hasAction, place) {
645
+ const result = buildResult(declared, hasAction, place.path);
646
+ if (!hasAction) {
647
+ checkGroup(declared, place);
197
648
  }
198
- return attached;
649
+ return result;
199
650
  }
200
651
  /**
201
- * A Command's own alias rules, and the flat list in declaration order that routing and inspection
202
- * read. Each call keeps its own group, so a call that names none reports as the call it is.
652
+ * The one attach operation `Command.command()`, `Application.command()`, and a plugin's
653
+ * `commands` list share. A Command is an immutable value, so the child is final here: it is checked
654
+ * as a finished Command, against the parent's current children, and against the nesting cap from
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.
203
657
  */
204
- function collectAliases(state) {
205
- const { name } = state;
206
- const aliases = [];
207
- const seen = new Set();
208
- for (const declaration of state.aliases) {
209
- if (declaration.length === 0) {
210
- throw new DeclarationError(`${commandSentence(name)} declares an alias with no names. Supply at least one name.`);
211
- }
212
- for (const alias of declaration) {
213
- checkAliasName(name, alias);
214
- if (alias === name) {
215
- throw new DeclarationError(`${commandSentence(name)} declares alias "${alias}", which is its own name. Remove the alias.`);
216
- }
217
- if (seen.has(alias)) {
218
- throw new DeclarationError(`${commandSentence(name)} declares alias "${alias}" twice. Remove the repeated alias.`);
219
- }
220
- seen.add(alias);
221
- aliases.push(alias);
222
- }
658
+ export function attach(parent, node, placement) {
659
+ const { name } = node;
660
+ const child = { name, node, placement };
661
+ if (parent.hasAction) {
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
+ });
223
667
  }
224
- return aliases;
668
+ if (parent.argument !== undefined) {
669
+ throw placementFault(parent, parent.argument, child);
670
+ }
671
+ checkFinished(node.declared, node.hasAction, { path: childPath(parent, name), placement });
672
+ checkSiblings(parent, child);
673
+ checkNesting(parent.name, child, parentLevel(parent.name) + 1);
674
+ return child;
675
+ }
676
+ /** The handle behind the value one `command()` call received; anything else is a declaration error. */
677
+ export function childNode(parent, child) {
678
+ const node = commandNode(child);
679
+ if (!node) {
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
+ });
685
+ }
686
+ return node;
687
+ }
688
+ /** Attaching is a declaration call too, so the receiver keeps the children it already had. */
689
+ function attachChild(state, child) {
690
+ const node = childNode(state.name, child);
691
+ const attached = attach(parentOf(state), node, commandPlacement(pathOf(state.name), node.name));
692
+ return { ...state, children: [...state.children, attached] };
225
693
  }
226
694
  /**
227
- * One parent's namespace, which every canonical name and alias under it shares. Each child settled
228
- * its own alias rules while it built, so what is left is the collision with a sibling. The names are
229
- * read before the walk, so the rule reads the same whichever sibling the author declared first.
695
+ * Walks one subtree joining an Application, once, for the rules only the Application can judge: one
696
+ * Command value reached through two paths, two distinct descriptors under one identity, a local
697
+ * option that meets a global or plugin option's key, spelling, or variable, and the nesting cap
698
+ * measured from the root. A claim is by node identity, and the name serves the diagnostic alone.
699
+ * At a cap of two a named parent's `command()` already rejects every deeper tree, so the depth check
700
+ * here fires only once the cap rises: attach reads one level, and only this walk knows each level.
230
701
  */
231
- function aliasNamespace(parent, names) {
232
- const owners = new Map();
233
- return function claim(child, alias) {
234
- if (names.has(alias)) {
235
- throw new DeclarationError(`${commandSentence(parent)} attaches child "${child}" with alias "${alias}", which is also the name of child "${alias}". Rename or remove one.`);
236
- }
237
- const owner = owners.get(alias);
238
- if (owner !== undefined) {
239
- throw new DeclarationError(`${commandSentence(parent)} attaches child "${child}" with alias "${alias}", which is also an alias of child "${owner}". Rename or remove one.`);
240
- }
241
- owners.set(alias, child);
242
- };
702
+ function joinSubtree(scope, child, { level, parent }) {
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
+ });
715
+ }
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 });
719
+ }
720
+ const path = childPath(parent, name);
721
+ checkLocalOptions(optionsOf(node.declared.inputs), scope.table, localScope(name, path));
722
+ for (const entry of node.declared.children) {
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 } });
726
+ }
243
727
  }
244
728
  /**
245
- * Claims a child for its parent when the depth-first walk reaches it, then builds its subtree. A
246
- * claimed node always means a second parent, because the duplicate-name rule rejects one parent
247
- * attaching a value twice. Parents may share a name, so the claim is by node identity and the name
248
- * serves the diagnostic alone.
729
+ * Attaches one child to an Application's root and walks the subtree it brings, once. The scope's
730
+ * registers are copies: the caller commits the owners, and the returned root holds the descriptors.
249
731
  */
250
- function buildChild(parent, [name, node], context) {
251
- const owner = context.owners.get(node);
252
- if (owner !== undefined) {
253
- throw new DeclarationError(`${commandSentence(parent)} attaches child "${name}", which ${commandSubject(owner)} also attaches. Attach a Command value at one point; create a new Command for each placement.`);
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 });
736
+ return { ...state, children: [...state.children, attached], descriptors: scope.descriptors };
737
+ }
738
+ /** Every attached Command's local options against a globals table that has just grown. */
739
+ function checkAttachedOptions(table, parent, children) {
740
+ for (const { name, node } of 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);
254
744
  }
255
- context.owners.set(node, parent);
256
- return node.build({ ...context, path: [...context.path, name] });
745
+ }
746
+ /**
747
+ * A declaration's own options and every attached Command's, against a globals table that has just
748
+ * grown. The table's new option reads as the other side of any collision.
749
+ */
750
+ export function checkDeclaredOptions(state, table) {
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);
257
754
  }
258
755
  /** A variadic or optional slot ends the positional list, so nothing may follow either one. */
259
- 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');
260
759
  if (slot.variadic) {
261
- 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
+ });
262
765
  }
263
766
  if (!slot.required) {
264
- throw new DeclarationError(next.required
265
- ? `Argument "${slot.input.name}" is optional and precedes required argument "${next.input.name}" on ${subject}. Declare optional arguments after required ones.`
266
- : `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
+ });
267
779
  }
268
780
  }
269
- function collectArguments(state, subject) {
781
+ /**
782
+ * The positional slots one declaration holds. Every authored argument answered these rules at its
783
+ * own call, so they throw here only for an argument a lifecycle hook declared.
784
+ */
785
+ function collectArguments(state, command) {
270
786
  const slots = [];
271
- const seen = new Set();
787
+ const declared = [];
788
+ const judged = { ...command, subject: commandSubject(command.name) };
272
789
  for (const input of state.inputs.filter((entry) => entry.kind === 'argument')) {
273
- if (!isDeclaredName(input.name)) {
274
- throw new DeclarationError(`${commandSentence(state.name)} declares an argument named "${String(input.name)}". Use a nonempty name without a leading hyphen, whitespace, or "=".`);
275
- }
276
- if (seen.has(input.name)) {
277
- throw new DeclarationError(`Argument "${input.name}" is declared more than once on ${subject}. Remove or rename the duplicate.`);
278
- }
279
- seen.add(input.name);
280
- slots.push({
281
- input,
282
- required: input.config.required === true,
283
- variadic: input.config.variadic === true,
284
- });
790
+ checkArgumentName(judged, declared, input);
791
+ declared.push(input);
792
+ slots.push(slotOf(input));
285
793
  }
286
794
  for (let index = 0; index + 1 < slots.length; index += 1) {
287
795
  const slot = slots[index];
288
796
  const next = slots[index + 1];
289
797
  if (slot && next) {
290
- checkSlotOrder(slot, next, subject);
798
+ checkSlotOrder(slot, next, judged);
291
799
  }
292
800
  }
293
801
  return slots;
294
802
  }
295
803
  /**
296
- * One Command's own options against the shared globals table. The table holds the application's
297
- * globals and every plugin option, so a local collision reads the same sentence whichever scope on
298
- * the other side claimed the name or the spelling.
299
- */
300
- function compileLocalOptions(state, globals, subject) {
301
- const declarations = state.inputs.filter((input) => input.kind === 'option');
302
- const local = { kind: 'local', subject };
303
- const application = { kind: 'application' };
304
- for (const declaration of declarations) {
305
- const claimed = globals.names.get(declaration.name);
306
- if (claimed) {
307
- throw keyCollision(declaration.name, claimed, local);
308
- }
309
- }
310
- const options = compileOptions(declarations, subject);
311
- for (const [spelling, option] of options) {
312
- const global = globals.options.get(spelling);
313
- if (global) {
314
- throw spellingCollision(spelling, { name: global.name, owner: globals.names.get(global.name) ?? application }, { name: option.name, owner: local });
315
- }
316
- }
317
- return options;
318
- }
319
- /**
320
- * A Command without an action is a group, and routing sends an invocation on to one of its
321
- * children. A group with no children receives an invocation no handler can answer, and a local
322
- * option on a group reaches no handler either, because locals never inherit.
323
- */
324
- function checkGroup(state, children) {
325
- const { name } = state;
326
- if (children.length === 0) {
327
- throw new DeclarationError(`${commandSentence(name)} has no action. Register an action.`);
328
- }
329
- const option = state.inputs.find((input) => input.kind === 'option');
330
- if (option) {
331
- throw new DeclarationError(`${commandSentence(name)} declares option "${option.name}" but registers no action to receive it. Register an action or remove the option.`);
332
- }
333
- }
334
- /**
335
- * A view name is a bare token the way a child name is, and never an array index, because an
336
- * integer-like key does not keep the position the author gave it.
804
+ * A view name is a bare token the way an argument or option name is, and never an array index,
805
+ * because an integer-like key does not keep the position the author gave it.
337
806
  */
338
807
  function isViewName(name) {
339
808
  return isDeclaredName(name) && !/^(?:0|[1-9]\d*)$/u.test(name);
@@ -367,71 +836,126 @@ function recordEntries(record) {
367
836
  * One `views` entry under the unit its declaration named, read back as the shape its own functions
368
837
  * name. The two shapes are exclusive, and a row view answers a rows declaration alone.
369
838
  */
370
- function resultView(declaration, name, entry) {
371
- 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) })];
372
842
  const whole = isWholeView(entry);
373
843
  const row = isRowView(entry);
374
844
  if (whole && row) {
375
- 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
+ });
376
850
  }
377
851
  if (row) {
378
852
  if (kind === 'value') {
379
- 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
+ });
380
858
  }
381
859
  return entry;
382
860
  }
383
861
  if (whole) {
384
862
  return entry;
385
863
  }
386
- 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
+ });
387
869
  }
388
870
  /**
389
- * Every rule the results lane carries, applied to one Command's calls in the order it made them.
390
- * The merged record is what the rules read: a later `views()` call replaces a key in place and
391
- * appends a new one, so each name keeps the position the call that first named it gave it. A
392
- * `default` once named persists through later calls that name none, and the first key answers
393
- * until one is named.
871
+ * Every rule a results-lane call can judge on its own, applied to one Command's calls in the order
872
+ * it made them. The merged record is what the rules read: a later `views()` call replaces a key in
873
+ * place and appends a new one, so each name keeps the position the call that first named it gave
874
+ * it. A `default` once named persists through later calls that name none.
394
875
  */
395
- function buildResult(state, hasAction) {
396
- const sentence = commandSentence(state.name);
397
- const declarations = state.results.filter((call) => call.kind !== 'views');
398
- if (declarations.length > 1) {
399
- throw new DeclarationError(`${sentence} declares two results. Declare one result() or rows() call.`);
876
+ function mergeResult(command, results) {
877
+ const { path } = command;
878
+ const sentence = commandSentence(command.name);
879
+ const declarations = results.filter((call) => call.kind !== 'views');
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
+ });
400
890
  }
401
- const declaration = declarations[0];
402
891
  // A `views()` call reshapes a result's views, so one with no result reshapes nothing.
403
892
  // The types publish the call where a result is carried, so this reaches a JavaScript author.
404
893
  if (!declaration) {
405
- if (state.results.length > 0) {
406
- 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
+ });
407
901
  }
408
902
  return undefined;
409
903
  }
410
- if (!hasAction) {
411
- throw new DeclarationError(`${sentence} declares a result and no action. Register an action or remove the result.`);
412
- }
413
904
  const views = new Map();
414
905
  let selected = undefined;
415
- for (const call of state.results) {
416
- for (const [name, entry] of recordEntries(call.views)) {
417
- const view = resultView({ kind: declaration.kind, sentence }, name, entry);
418
- if (!isViewName(name)) {
419
- throw new DeclarationError(`${sentence} names view "${name}". Use a nonempty name without whitespace, a leading hyphen, or "=", and not a number.`);
906
+ for (const call of results) {
907
+ for (const [key, entry] of recordEntries(call.views)) {
908
+ const view = resultView(declaration.kind, { call, key, path, sentence }, entry);
909
+ if (!isViewName(key)) {
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
+ });
420
915
  }
421
- views.set(name, view);
916
+ views.set(key, view);
422
917
  }
423
- if (call.kind === 'views') {
424
- selected = selectedKey(call.default) ?? selected;
918
+ const key = call.kind === 'views' ? selectedKey(call.default) : undefined;
919
+ if (key !== undefined) {
920
+ selected = { call, key };
425
921
  }
426
922
  }
923
+ return { declaration, kind: declaration.kind, selected, views };
924
+ }
925
+ /**
926
+ * The finished result: the merged record, which needs an action, at least one view, and a default
927
+ * that names one of them. The first key answers until one is named.
928
+ */
929
+ function buildResult(declared, hasAction, path) {
930
+ const merged = mergeResult({ name: declared.name, path }, declared.results);
931
+ if (!merged) {
932
+ return undefined;
933
+ }
934
+ const sentence = commandSentence(declared.name);
935
+ const { declaration, selected, views } = merged;
936
+ if (!hasAction) {
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
+ });
942
+ }
427
943
  const first = views.keys().next();
428
944
  if (first.done === true) {
429
- 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
+ });
430
950
  }
431
- if (selected !== undefined && !views.has(selected)) {
432
- 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
+ });
433
957
  }
434
- return { default: selected ?? first.value, kind: declaration.kind, views };
958
+ return { default: selected?.key ?? first.value, kind: merged.kind, views };
435
959
  }
436
960
  /** The values one build made, so a value a hook returns is one of them and never a forged shape. */
437
961
  const attachments = new WeakMap();
@@ -454,7 +978,7 @@ class AttachedCommandValue {
454
978
  #result;
455
979
  constructor(state) {
456
980
  this.#state = state;
457
- this.#result = resultNode(buildResult(state.declared, state.hasAction));
981
+ this.#result = resultNode(buildResult(state.declared, state.hasAction, state.path));
458
982
  attachments.set(this, state);
459
983
  Object.freeze(this);
460
984
  }
@@ -481,17 +1005,13 @@ class AttachedCommandValue {
481
1005
  return publishStore(this.#state.extensions);
482
1006
  }
483
1007
  argument(name, config) {
484
- return this.#declare({ config: captureConfig(config), kind: 'argument', name });
1008
+ return this.#declare({ config, kind: 'argument', name });
485
1009
  }
486
1010
  option(name, config) {
487
- return this.#declare({ config: captureConfig(config), kind: 'option', name });
1011
+ return this.#declare({ config, kind: 'option', name });
488
1012
  }
489
1013
  views(replacements, options) {
490
- const call = {
491
- default: recordOf(options, 'default'),
492
- kind: 'views',
493
- views: replacements,
494
- };
1014
+ const call = viewsCall(replacements, options);
495
1015
  const { declared } = this.#state;
496
1016
  return this.#derive({ call, kind: 'views' }, { ...declared, results: [...declared.results, call] });
497
1017
  }
@@ -505,6 +1025,7 @@ class AttachedCommandValue {
505
1025
  const layer = validateLayer({
506
1026
  declared: values,
507
1027
  descriptors: registry,
1028
+ site: { at: '', declaration: { arguments: values, call: 'extend', path: state.path } },
508
1029
  subject: state.subject,
509
1030
  target: 'command',
510
1031
  });
@@ -515,8 +1036,22 @@ class AttachedCommandValue {
515
1036
  });
516
1037
  }
517
1038
  /** One input the running hook declared, which the Command's own names now hold. */
518
- #declare(input) {
1039
+ #declare(raw) {
519
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) };
520
1055
  return this.#derive({ identity, input, kind: 'input' }, { ...declared, inputs: [...declared.inputs, input] });
521
1056
  }
522
1057
  #derive(call, declared) {
@@ -525,9 +1060,13 @@ class AttachedCommandValue {
525
1060
  }
526
1061
  }
527
1062
  _a = AttachedCommandValue;
528
- /** The reason one failed hook reports, which is the thrown value's own message. */
529
- function attachReason(error) {
530
- 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
+ };
531
1070
  }
532
1071
  /** One hook's own call, whose failure is the plugin's, and whose own report is its own. */
533
1072
  function callHook(value, hook, named) {
@@ -539,7 +1078,11 @@ function callHook(value, hook, named) {
539
1078
  if (error instanceof DeclarationError) {
540
1079
  throw error;
541
1080
  }
542
- 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 });
543
1086
  }
544
1087
  }
545
1088
  /**
@@ -550,7 +1093,11 @@ function callHook(value, hook, named) {
550
1093
  function attachOnce(value, hook, named) {
551
1094
  const state = attachments.get(callHook(value, hook, named));
552
1095
  if (!state || state.lineage !== named.lineage) {
553
- 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
+ });
554
1101
  }
555
1102
  return state;
556
1103
  }
@@ -581,8 +1128,8 @@ function runAttachHooks(start, facts, plugins) {
581
1128
  }
582
1129
  /**
583
1130
  * One input a hook declared. It joins the values an action reads at run time and the declaration's
584
- * types not at all, and it records no late declaration, because a hook's calls are exempt from the
585
- * closures `action()` applies.
1131
+ * types not at all, and it skips the order checks an authoring call makes, because a hook's calls
1132
+ * are exempt from the closures `action()` applies.
586
1133
  */
587
1134
  function declareHookInput(state, input) {
588
1135
  const previous = state.bind;
@@ -608,10 +1155,28 @@ function applyAttachCalls(state, calls) {
608
1155
  }
609
1156
  return next;
610
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
+ }
611
1173
  /** The clause and the remedy an input another plugin's hook already declared earns. */
612
- function hookClause(kind, identity) {
1174
+ function hookClause(earlier, path) {
1175
+ const { identity, input } = earlier;
613
1176
  return {
614
- 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,
615
1180
  remedy: 'Install one of them.',
616
1181
  };
617
1182
  }
@@ -633,6 +1198,25 @@ function attachedRemedy(target) {
633
1198
  }
634
1199
  return 'Install one of them.';
635
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
+ }
636
1220
  /**
637
1221
  * What one name a hook-declared input of either kind collides with, in the order the scopes are
638
1222
  * reported: an earlier hook's option, an earlier hook's argument, a local option, a global or
@@ -641,32 +1225,36 @@ function attachedRemedy(target) {
641
1225
  */
642
1226
  function attachedCollision(input, held) {
643
1227
  const { name } = input;
644
- const hook = held.hooks.get(name);
1228
+ const { path } = held;
1229
+ const hook = held.hooks.get(name) ?? held.hookArguments.get(name);
645
1230
  if (hook !== undefined) {
646
- return hookClause('option', hook);
1231
+ return hookClause(hook, path);
647
1232
  }
648
- const hooked = held.hookArguments.get(name);
649
- if (hooked !== undefined) {
650
- return hookClause('argument', hooked);
651
- }
652
- if (held.locals.has(name)) {
653
- 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
+ };
654
1241
  }
655
- const claimed = held.globals.names.get(name);
656
- if (claimed) {
657
- const target = claimed.kind === 'plugin' ? 'plugin' : 'global';
658
- const clause = claimed.kind === 'plugin' ? `an option of plugin "${claimed.identity}"` : 'a global option';
659
- return { clause, remedy: attachedRemedy(target) };
1242
+ const entry = held.globals.names.get(name);
1243
+ if (entry) {
1244
+ return tableCollision(entry);
660
1245
  }
661
- return held.arguments.has(name)
662
- ? { clause: 'an argument', remedy: attachedRemedy('argument') }
663
- : 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
+ };
664
1255
  }
665
- /**
666
- * The spellings one compiled table holds, each under the form its own option is named by, which is
667
- * its long form where it declares one and the colliding spelling itself where it declares none.
668
- */
669
- 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) {
670
1258
  const longs = new Map();
671
1259
  for (const [spelling, option] of table) {
672
1260
  if (option.role === 'long') {
@@ -675,69 +1263,129 @@ function readSpellings(table, claimed) {
675
1263
  }
676
1264
  for (const [spelling, option] of table) {
677
1265
  if (!claimed.has(spelling)) {
678
- 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 });
679
1268
  }
680
1269
  }
681
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
+ }
682
1275
  /** One hook-declared option's spellings, against every spelling the table already claims. */
683
1276
  function checkAttachedSpelling(declared, named) {
684
- const { claimed, subject } = named;
685
- const table = compileOptions([declared.input], subject);
686
- 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) {
687
1283
  const used = claimed.get(spelling);
688
1284
  if (used !== undefined) {
689
- 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
+ });
690
1293
  }
691
1294
  }
692
- 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
+ };
693
1320
  }
694
1321
  /**
695
1322
  * Every input a hook declared, against the names and spellings the Command, the globals table, and
696
1323
  * an earlier hook already hold. It runs before the Command's own inputs compile, so a hook-declared
697
- * 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.
698
1326
  */
699
- function checkAttachedInputs(declared, attached, globals) {
700
- 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;
701
1331
  const hooked = new Set(attached.map((entry) => entry.input));
702
1332
  const authored = declared.inputs.filter((input) => !hooked.has(input));
703
1333
  const options = authored.filter((input) => input.kind === 'option');
1334
+ const locals = byName(options);
704
1335
  const held = {
705
- arguments: new Set(authored.filter((input) => input.kind === 'argument').map((input) => input.name)),
1336
+ arguments: byName(authored.filter((input) => input.kind === 'argument')),
706
1337
  globals,
707
1338
  hookArguments: new Map(),
708
1339
  hooks: new Map(),
709
- locals: new Set(options.map((input) => input.name)),
1340
+ locals,
1341
+ path,
710
1342
  };
711
1343
  const claimed = new Map();
712
- readSpellings(globals.options, claimed);
713
- readSpellings(compileOptions(options, subject), claimed);
714
- 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;
715
1353
  const collision = attachedCollision(input, held);
716
1354
  if (collision) {
717
- 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
+ });
718
1360
  }
719
1361
  if (input.kind === 'option') {
720
- checkAttachedSpelling({ identity, input }, { claimed, subject });
721
- held.hooks.set(input.name, identity);
1362
+ checkAttachedSpelling({ identity, input }, { claimed, scope });
1363
+ held.hooks.set(input.name, entry);
722
1364
  }
723
1365
  else {
724
- held.hookArguments.set(input.name, identity);
1366
+ held.hookArguments.set(input.name, entry);
725
1367
  }
726
1368
  }
727
1369
  }
728
1370
  /** Binds one Command's declarations to its action, so an action reads only validated values. */
729
1371
  function bindDispatch(state, action, globals) {
730
- return ({ host, out, passthrough, signal, style, values }) => {
1372
+ return ({ command, graph, host, out, passthrough, signal, style, values }) => {
731
1373
  const bound = state.bind(values);
732
1374
  // Last resort: no typed path exists.
733
1375
  // The graph erases the binder's generic relationship.
734
1376
  // It holds because attachment checks the global output requirement.
735
- // Graph build rejects a collision between a global and a local.
1377
+ // The call or the attach that met both options rejected a collision between a global and a local.
736
1378
  // This binder returns the Application's validated globals alone.
737
1379
  // oxlint-disable-next-line typescript/no-unsafe-type-assertion
738
1380
  const globalOptions = globals.bind(values);
739
1381
  return action({
740
1382
  args: bound.args,
1383
+ get command() {
1384
+ return command();
1385
+ },
1386
+ get graph() {
1387
+ return graph();
1388
+ },
741
1389
  host,
742
1390
  options: { ...globalOptions, ...bound.options },
743
1391
  out,
@@ -747,69 +1395,64 @@ function bindDispatch(state, action, globals) {
747
1395
  });
748
1396
  };
749
1397
  }
750
- /** Each declaration's own facts and extension values, whichever scope declared it. */
1398
+ /**
1399
+ * The own facts and extension values of each input a lifecycle hook declared, which the calls that
1400
+ * declared the author's inputs already checked.
1401
+ */
751
1402
  function checkInputFacts(inputs, command, context) {
752
1403
  const { name, subject } = command;
753
1404
  for (const input of inputs) {
754
- const sentence = `${commandSentence(name)} ${input.kind} "${input.name}"`;
755
- checkDescription(sentence, input.config.description);
1405
+ const site = inputSite(name, context.path, input);
1406
+ const sentence = site.subject;
1407
+ checkDescription(site, input.config.description);
756
1408
  if (input.kind === 'argument') {
757
- checkNoListingFacts(sentence, input.config);
1409
+ checkNoListingFacts(site, input.config);
1410
+ checkNoArgumentBinding(site, input.config);
758
1411
  }
759
1412
  else {
760
- checkHidden(sentence, input.config.hidden);
761
- checkDeprecated(sentence, input.config.deprecated);
1413
+ checkHidden(site, input.config.hidden);
1414
+ checkDeprecated(site, input.config.deprecated);
1415
+ checkEnvBinding(site, input.config);
762
1416
  }
763
1417
  context.extensions.set(input, buildExtensions({
764
1418
  declared: input.config.extensions,
765
1419
  descriptors: context.descriptors,
1420
+ site: { ...site, at: '1.extensions' },
766
1421
  subject: { phrase: `on ${subject} ${input.kind} "${input.name}"`, sentence },
767
1422
  target: input.kind,
768
1423
  }));
769
1424
  }
770
1425
  }
771
1426
  /** One Command declares arguments or attaches children, whichever declaration made each of them. */
772
- function checkArgumentPlacement(name, slot, child) {
1427
+ function checkArgumentPlacement(command, slot, child) {
773
1428
  if (slot && child) {
774
- throw new DeclarationError(`${commandSentence(name)} declares argument "${slot.input.name}" and attaches child "${child[0]}". Move the argument into a child Command or remove the children.`);
1429
+ throw placementFault(command, slot.input, child);
775
1430
  }
776
1431
  }
777
- /** Validates one declaration against the shared globals table and compiles it for dispatch. */
1432
+ /**
1433
+ * Compiles one declaration for dispatch against the shared globals table. Every authored rule threw
1434
+ * at the call or the attach that first held its data, so what can still fail here is the root's
1435
+ * finished-Command rules, the root never being attached, and whatever the lifecycle hooks
1436
+ * contributed.
1437
+ */
778
1438
  export function buildCommand(state, context) {
779
- const { actions, name } = state;
1439
+ const { action, facts, name } = state;
780
1440
  const { globals } = context;
781
1441
  const subject = commandSubject(name);
782
- const layer = { phrase: `on ${subject}`, sentence: commandSentence(name) };
783
- checkCommandOptions(name, state.options);
784
- const description = checkDescription(commandSentence(name), state.description);
785
- const hidden = checkHidden(commandSentence(name), state.hidden);
786
- const deprecated = checkDeprecated(commandSentence(name), state.deprecated);
787
- // The author's layers validate before any hook runs on this Command, so a hook reads them.
788
- const declaredExtensions = storeCommandLayers({
789
- descriptors: context.descriptors,
790
- layers: state.extensions,
791
- subject: layer,
792
- });
793
- // Each declaration's own facts, in authoring order, before the rules that pair declarations.
794
- checkInputFacts(state.inputs, { name, subject }, context);
795
- const attached = collectChildren(state);
796
- checkDeclarationOrder(state);
797
- const aliases = collectAliases(state);
798
- const declaredSlots = collectArguments(state, subject);
799
- checkArgumentPlacement(name, declaredSlots[0], attached[0]);
800
- if (actions.length > 1) {
801
- throw new DeclarationError(`${commandSentence(name)} has multiple actions. Register one action.`);
802
- }
803
- const action = actions[0];
1442
+ const layer = layerOf(name);
1443
+ for (const [input, record] of state.records) {
1444
+ context.extensions.set(input, record);
1445
+ }
1446
+ const place = { name, path: context.path };
1447
+ const declaredSlots = collectArguments(state, place);
804
1448
  const hasAction = action !== undefined;
805
- const declaredResult = buildResult(state, hasAction);
806
1449
  /**
807
- * The hooks run once the author's declaration is complete and its result is resolved, so a hook
808
- * reads an exact record, and every rule below reads what the hooks returned.
1450
+ * The hooks run once the author's declaration is complete, and each value a hook receives resolves
1451
+ * its result, so a hook reads an exact record. Every rule below reads what the hooks returned.
809
1452
  */
810
1453
  // One token per Command per build, which binds a hook's return to the value it received.
811
1454
  const lineage = {};
812
- const progress = runAttachHooks({ declared: state, extensions: declaredExtensions, layer, registry: context.descriptors }, { hasAction, lineage, path: context.path }, context.plugins);
1455
+ const progress = runAttachHooks({ declared: state, extensions: state.extensions, layer, registry: context.descriptors }, { hasAction, lineage, path: context.path }, context.plugins);
813
1456
  const { calls } = progress;
814
1457
  // The descriptors the returned value's extensions registered join the build's registry.
815
1458
  for (const [identity, descriptor] of progress.registry) {
@@ -820,38 +1463,37 @@ export function buildCommand(state, context) {
820
1463
  checkInputFacts(hookInputs.map((call) => call.input), { name, subject }, context);
821
1464
  // The hook-declared names are checked first, so a collision reports in the plugin's voice.
822
1465
  // A rule the author's own declaration voices never speaks for a name a hook declared.
823
- checkAttachedInputs(hooked, hookInputs, globals);
824
- // Every value was validated once, the author's above and each hook's at its `extend()` call.
1466
+ checkAttachedInputs(hooked, hookInputs, { globals, path: context.path });
1467
+ // Every value was validated once, the author's at each call and each hook's at its `extend()`.
825
1468
  const extensions = publishStore(progress.extensions);
826
- const slots = hookInputs.length > 0 ? collectArguments(hooked, subject) : declaredSlots;
827
- checkArgumentPlacement(name, slots[0], attached[0]);
828
- const result = calls.length > 0 ? buildResult(hooked, hasAction) : declaredResult;
829
- if (!action) {
830
- checkGroup(hooked, attached);
831
- }
832
- const options = compileLocalOptions(hooked, 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));
1473
+ // A hook's erased calls answer the declaration rules an authored call answers at the call.
1474
+ checkDeclarations(hookInputs.map(({ input }) => ({ input, site: inputSite(name, context.path, input) })));
833
1475
  const children = new Map();
834
1476
  const routes = new Map();
835
- const claim = aliasNamespace(name, new Set(attached.map((entry) => entry[0])));
836
- for (const entry of attached) {
837
- // The subtree builds before its aliases are claimed, so the tree rule keeps its precedence.
838
- const routed = { command: buildChild(name, entry, context), name: entry[0] };
1477
+ for (const child of state.children) {
1478
+ const routed = {
1479
+ command: child.node.build({ ...context, path: [...context.path, child.name] }),
1480
+ name: child.name,
1481
+ };
839
1482
  children.set(routed.name, routed.command);
840
1483
  routes.set(routed.name, routed);
841
1484
  for (const alias of routed.command.aliases) {
842
- claim(routed.name, alias);
843
1485
  routes.set(alias, routed);
844
1486
  }
845
1487
  }
846
1488
  return {
847
- aliases,
1489
+ aliases: state.aliases,
848
1490
  arguments: slots,
849
1491
  children,
850
- deprecated,
851
- description,
1492
+ deprecated: facts.deprecated,
1493
+ description: facts.description,
852
1494
  dispatch: action ? bindDispatch(hooked, action, globals) : undefined,
853
1495
  extensions,
854
- hidden,
1496
+ hidden: facts.hidden,
855
1497
  inputs: hooked.inputs,
856
1498
  name,
857
1499
  options,
@@ -859,105 +1501,101 @@ export function buildCommand(state, context) {
859
1501
  routes,
860
1502
  };
861
1503
  }
862
- /** The globals table and the owners record each compile once per invocation and the whole graph shares them. */
863
- export function buildGraph(root, globals, install) {
1504
+ /**
1505
+ * The globals table compiles once per build and the whole graph shares it. The root's registry
1506
+ * holds every descriptor the Application names, so a hook's `extend()` call meets all of them.
1507
+ */
1508
+ export function buildGraph(root, globals, plugins) {
1509
+ const extensions = new Map([
1510
+ ...globals.records,
1511
+ ...plugins.flatMap((installed) => [...installed.records]),
1512
+ ]);
864
1513
  const context = {
865
- descriptors: install.descriptors,
866
- extensions: install.extensions,
867
- globals: buildGlobals(globals, install.plugins, install),
868
- owners: new Map(),
1514
+ descriptors: new Map(root.descriptors),
1515
+ extensions,
1516
+ globals: buildGlobals(globals, plugins),
869
1517
  path: [],
870
- plugins: install.plugins,
871
- };
872
- return {
873
- extensions: context.extensions,
874
- globals: context.globals,
875
- root: buildCommand(root, context),
1518
+ plugins,
876
1519
  };
1520
+ return { extensions, globals: context.globals, root: buildCommand(root, context) };
877
1521
  }
878
1522
  export class CommandBuilder {
1523
+ #name;
879
1524
  #state;
880
- constructor(state) {
1525
+ constructor(name, state) {
1526
+ this.#name = name;
881
1527
  this.#state = state;
882
- nodes.set(this, this);
883
- }
884
- get name() {
885
- return this.#state.name;
1528
+ nodes.set(this, nodeHandle(name, state));
886
1529
  }
887
1530
  argument(name, config) {
888
1531
  const input = {
889
- config: captureConfig(config),
1532
+ config,
890
1533
  kind: 'argument',
891
1534
  name,
892
1535
  };
893
- return this.derive(declareArgument(this.#state, input));
1536
+ return this.#derive(declareArgument(this.#state, input));
894
1537
  }
895
1538
  option(name, config) {
896
1539
  const input = {
897
- config: captureConfig(config),
1540
+ config,
898
1541
  kind: 'option',
899
1542
  name,
900
1543
  };
901
- return this.derive(declareOption(this.#state, input));
1544
+ return this.#derive(declareOption(this.#state, input));
902
1545
  }
903
1546
  /**
904
- * Aliases are other bare tokens that route to this Command. They invalidate no call, and the
1547
+ * Aliases are other portable names that route to this Command. They invalidate no call, and the
905
1548
  * tuple rest parameter rejects a call that names none.
906
1549
  */
907
1550
  alias(...names) {
908
- return this.derive(declareAlias(this.#state, names));
1551
+ return this.#derive(declareAlias(this.#state, names));
909
1552
  }
910
1553
  /** A child arrives in any type state, because its own action is the call that finished it. */
911
1554
  command(child) {
912
- return this.derive(attachChild(this.#state, child));
1555
+ return this.#derive(attachChild(this.#state, child));
913
1556
  }
914
1557
  /**
915
1558
  * The value this Command produces for its consumer. The type argument is stated by the author,
916
1559
  * so the views record states no type of its own and an omitted argument names none either.
917
1560
  */
918
1561
  result(declaration) {
919
- return this.derive(declareResult(this.#state, 'value', declaration));
1562
+ return this.#derive(declareResult(this.#state, 'value', declaration));
920
1563
  }
921
1564
  /** The same declaration over a sequence, whose type argument is one row. */
922
1565
  rows(declaration) {
923
- return this.derive(declareResult(this.#state, 'rows', declaration));
1566
+ return this.#derive(declareResult(this.#state, 'rows', declaration));
924
1567
  }
925
1568
  /**
926
1569
  * Views after the fact. It merges by key, so an existing name is replaced in place and a new one
927
1570
  * is appended, and `default` names the key core renders when nothing selects another.
928
1571
  */
929
1572
  views(replacements, options) {
930
- return this.derive(declareResultViews(this.#state, replacements, options));
1573
+ return this.#derive(declareResultViews(this.#state, replacements, options));
931
1574
  }
932
1575
  /** The action closes input authoring; `extend()` remains outside this state transition. */
933
1576
  action(handler) {
934
- return this.derive(declareAction(this.#state, handler));
1577
+ return this.#derive(declareAction(this.#state, handler));
935
1578
  }
936
1579
  extend(...values) {
937
- return this.derive(declareExtensions(this.#state, values));
938
- }
939
- build(context) {
940
- return buildCommand(this.#state, context);
1580
+ return this.#derive(declareExtensions(this.#state, values));
941
1581
  }
942
1582
  /**
943
1583
  * The same runtime value in the state the calling method's return type names. Each call states
944
1584
  * its own transition, and the declared result travels with it unless the call replaces it.
945
1585
  */
946
- derive(state) {
947
- return new CommandBuilder(state);
1586
+ #derive(state) {
1587
+ return new _b(this.#name, state);
948
1588
  }
949
1589
  }
950
- /** The constructor uses the Application registration; public type defaults stay library-neutral. */
1590
+ _b = CommandBuilder;
1591
+ /**
1592
+ * The constructor uses the Application registration; public type defaults stay library-neutral.
1593
+ * Every rule on the name and the options slot throws before the value exists.
1594
+ */
951
1595
  class CommandDeclaration extends CommandBuilder {
952
1596
  constructor(name, options) {
953
- super(freshState({
954
- deprecated: options?.deprecated,
955
- description: options?.description,
956
- extensions: options?.extensions,
957
- hidden: options?.hidden,
958
- name,
959
- options,
960
- }));
1597
+ const declared = namedState(name, options);
1598
+ super(declared.name, declared.state);
961
1599
  }
962
1600
  }
963
1601
  /** The public constructor takes a name and one options object, as the Application does. */
@@ -970,21 +1608,54 @@ export function collectInputs(command) {
970
1608
  ];
971
1609
  }
972
1610
  /**
973
- * The names a routing failure offers: the canonical names of the visible children, in authoring
974
- * order. A candidate list is a listing, so a hidden child is absent from it, and a parent whose
975
- * 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.
976
1635
  */
977
1636
  function candidatesOf(command) {
978
- 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);
979
1640
  }
980
- /** Bare tokens, names or aliases, select children until a Command has none; a hyphen commits. */
981
- export function route(root, tokens) {
1641
+ /**
1642
+ * Whether a token names one of the Command's children: the Command has children and the token is
1643
+ * no option token. Routing and `locate` read a bare word through this rule.
1644
+ */
1645
+ export function readsAsChild(command, token) {
1646
+ return command.children.size > 0 && !isOptionToken(token);
1647
+ }
1648
+ /**
1649
+ * Bare tokens, names or aliases, select children until a Command has none; a hyphen commits.
1650
+ * `walked` receives the path after each name routes, so an unknown Command leaves its caller
1651
+ * holding the partial path walked before it.
1652
+ */
1653
+ export function route(root, tokens, walked) {
982
1654
  let command = root;
983
1655
  const path = [];
984
1656
  let index = 0;
985
- while (command.children.size > 0) {
986
- const token = tokens[index];
987
- if (token === undefined || token === '--' || token.startsWith('-')) {
1657
+ for (let token = tokens[index]; token !== undefined; token = tokens[index]) {
1658
+ if (!readsAsChild(command, token)) {
988
1659
  break;
989
1660
  }
990
1661
  const child = command.routes.get(token);
@@ -994,6 +1665,7 @@ export function route(root, tokens) {
994
1665
  // An alias routes like the canonical name, and the path it walks reports that name alone.
995
1666
  command = child.command;
996
1667
  path.push(child.name);
1668
+ walked?.(Object.freeze([...path]));
997
1669
  index += 1;
998
1670
  }
999
1671
  return { command, path, tokens: tokens.slice(index) };
@@ -1005,33 +1677,40 @@ export function route(root, tokens) {
1005
1677
  */
1006
1678
  function bindArguments(command, path, positionals) {
1007
1679
  const values = new Map();
1008
- let index = 0;
1009
- for (const slot of command.arguments) {
1010
- if (slot.variadic) {
1011
- const rest = positionals.slice(index);
1012
- // An empty tail binds nothing, so validation reads it as `[]` or reports the omission.
1013
- if (rest.length > 0) {
1014
- values.set(slot.input, rest);
1015
- }
1016
- index = positionals.length;
1680
+ for (const [position, token] of positionals.entries()) {
1681
+ const slot = argumentSlot(command.arguments, position);
1682
+ if (!slot) {
1683
+ throw new UnexpectedArgumentError(path, command.arguments.length, positionals.slice(position));
1684
+ }
1685
+ // An empty variadic tail binds nothing, so validation reads it as `[]` or reports the omission.
1686
+ const bound = values.get(slot.input);
1687
+ if (!slot.variadic) {
1688
+ values.set(slot.input, token);
1689
+ }
1690
+ else if (Array.isArray(bound)) {
1691
+ bound.push(token);
1017
1692
  }
1018
1693
  else {
1019
- const value = positionals[index];
1020
- if (value !== undefined) {
1021
- values.set(slot.input, value);
1022
- index += 1;
1023
- }
1694
+ values.set(slot.input, [token]);
1024
1695
  }
1025
1696
  }
1026
- if (index < positionals.length) {
1027
- throw new UnexpectedArgumentError(path, command.arguments.length, positionals.slice(index));
1028
- }
1029
1697
  return values;
1030
1698
  }
1031
- /** Consumes the globals table, then routes the remaining bare tokens to a Command. */
1032
- export function routeInvocation(graph, argv) {
1699
+ /**
1700
+ * The slot the positional at one index fills: the slot at that index, else a variadic last slot,
1701
+ * which accepts every later positional, else none. Binding and `locate` read positions through it.
1702
+ */
1703
+ export function argumentSlot(slots, position) {
1704
+ const last = slots.at(-1);
1705
+ return slots[position] ?? (last?.variadic ? last : undefined);
1706
+ }
1707
+ /**
1708
+ * Consumes the globals table, then routes the remaining bare tokens to a Command. `walked`
1709
+ * receives the path as routing extends it, the partial path of an unknown Command included.
1710
+ */
1711
+ export function routeInvocation(graph, argv, walked) {
1033
1712
  const scan = extractGlobals(graph.globals.options, [...argv]);
1034
- const routed = route(graph.root, scan.rest);
1713
+ const routed = route(graph.root, scan.rest, walked);
1035
1714
  return {
1036
1715
  command: routed.command,
1037
1716
  path: routed.path,
@@ -1060,37 +1739,104 @@ function requestOf(command, values, passthrough) {
1060
1739
  });
1061
1740
  }
1062
1741
  /**
1063
- * The phases that run ahead of the chain: the callable check, local parsing, and validation. It
1064
- * answers with the call that dispatches, so the caller records that the action was invoked at the
1065
- * moment it invokes it and no earlier failure reads as a dispatch.
1742
+ * The callable check and local parsing. A group answers no invocation of its own, so it holds the
1743
+ * missing-subcommand error with the rank the routing errors have and parses no token; a token
1744
+ * fault holds the same way.
1066
1745
  */
1067
- async function readyDispatch(graph, routed, invocation) {
1068
- const { command, path, scan } = routed;
1746
+ function parseLocal(routed) {
1747
+ const { command, path } = routed;
1069
1748
  const { dispatch } = command;
1070
- // A group answers no invocation of its own, so it fails with the routing errors above it.
1071
- if (!dispatch) {
1072
- throw new NonCallableCommandError(path, candidatesOf(command));
1749
+ try {
1750
+ if (!dispatch) {
1751
+ throw new NonCallableCommandError(path, candidatesOf(command));
1752
+ }
1753
+ const parsed = parseInputs(command.options, routed.tokens);
1754
+ const args = bindArguments(command, path, parsed.positionals);
1755
+ return {
1756
+ args,
1757
+ dispatch,
1758
+ kind: 'parsed',
1759
+ options: parsed.options,
1760
+ passthrough: parsed.passthrough,
1761
+ };
1073
1762
  }
1074
- const parsed = parseInputs(command.options, routed.tokens);
1075
- const args = bindArguments(command, path, parsed.positionals);
1763
+ catch (error) {
1764
+ return { fault: error, kind: 'held' };
1765
+ }
1766
+ }
1767
+ /**
1768
+ * The node of one option a configuration source is asked about: in the graph's globals, or among
1769
+ * the routed Command's own options. Build published every declaration, so a missing node means the
1770
+ * two readings of one graph disagree.
1771
+ */
1772
+ function requestNode(graph, path, place) {
1773
+ const options = place.global ? graph.globals : nodeAt(graph, path).options;
1774
+ const node = options.find((option) => option.name === place.name);
1775
+ if (!node) {
1776
+ throw graphMismatch(`Option "${place.name}" is not in the inspected graph.`);
1777
+ }
1778
+ return node;
1779
+ }
1780
+ /**
1781
+ * The input-source stage over this run's own copies of the parsed values. The global and plugin
1782
+ * options fill whatever local parsing held; the routed Command's own options fill only when local
1783
+ * parsing held no fault, because the request is `null` otherwise.
1784
+ */
1785
+ async function fillScope({ graph, invocation, routed }, local) {
1786
+ const globals = copyValues(routed.scan);
1787
+ const locals = copyValues(local.kind === 'parsed' ? local.options : emptyValues());
1788
+ const sources = await fillInputs({
1789
+ extensions: graph.extensions,
1790
+ globals: {
1791
+ global: true,
1792
+ inputs: [...graph.globals.inputs, ...graph.globals.plugins.flatMap((entry) => entry.inputs)],
1793
+ values: globals,
1794
+ },
1795
+ host: invocation.host,
1796
+ inspected: invocation.inspected,
1797
+ locals: local.kind === 'parsed'
1798
+ ? { global: false, inputs: optionsOf(routed.command.inputs), values: locals }
1799
+ : undefined,
1800
+ offer: invocation.offer,
1801
+ out: invocation.sourceOut,
1802
+ plugins: graph.globals.plugins,
1803
+ request: (input, global) => requestNode(invocation.inspected(), routed.path, { global, name: input.name }),
1804
+ signal: invocation.signal,
1805
+ style: invocation.style,
1806
+ });
1807
+ return { globals, locals, sources };
1808
+ }
1809
+ /**
1810
+ * Validation and the dispatch it prepares, over the values the input-source stage left. It answers
1811
+ * with the call that dispatches, so the caller records that the action was invoked at the moment
1812
+ * it invokes it and no earlier failure reads as a dispatch.
1813
+ */
1814
+ async function readyDispatch({ graph, invocation, routed }, filled) {
1815
+ const { command, path } = routed;
1816
+ const { local } = filled;
1076
1817
  const values = await validateValues({
1077
1818
  command: path,
1078
1819
  defaults: invocation.defaults,
1079
1820
  host: invocation.host,
1080
1821
  inputs: { globals: graph.globals.inputs, locals: command.inputs },
1081
- passthrough: parsed.passthrough,
1822
+ passthrough: local.passthrough,
1823
+ plugins: graph.globals.plugins.flatMap((entry) => entry.inputs),
1082
1824
  signal: invocation.signal,
1083
- supplied: { args, options: mergeValues(scan, parsed.options) },
1825
+ sources: filled.sources,
1826
+ supplied: { args: local.args, options: filled.values },
1084
1827
  });
1085
1828
  return {
1086
1829
  dispatch: async (view) => {
1087
1830
  // The channel is built at the boundary, because the view a middleware selected is read there.
1088
1831
  const channel = invocation.channel({ path, result: command.result, view });
1089
1832
  try {
1090
- await dispatch({
1833
+ await local.dispatch({
1834
+ // The run builds its graph once, so every read answers the same node.
1835
+ command: () => nodeAt(invocation.inspected(), path),
1836
+ graph: invocation.inspected,
1091
1837
  host: invocation.host,
1092
1838
  out: channel.out,
1093
- passthrough: parsed.passthrough,
1839
+ passthrough: local.passthrough,
1094
1840
  signal: invocation.signal,
1095
1841
  style: invocation.style,
1096
1842
  values,
@@ -1112,19 +1858,43 @@ async function readyDispatch(graph, routed, invocation) {
1112
1858
  throw error;
1113
1859
  }
1114
1860
  },
1115
- request: requestOf(command, values, parsed.passthrough),
1861
+ request: requestOf(command, values, local.passthrough),
1116
1862
  };
1117
1863
  }
1118
1864
  /**
1119
1865
  * Prepares one dispatch ahead of the middleware chain and holds whatever fault it found, so a
1120
1866
  * middleware reads the request before the action runs and a takeover never observes the fault.
1867
+ * The phases run in order: local parsing, the input-source stage, and validation. A local fault
1868
+ * outranks a configuration source's fault, and after either one core runs no validation.
1121
1869
  */
1122
1870
  export async function prepareDispatch(graph, routed, invocation) {
1123
1871
  const result = routed.command.result;
1872
+ const local = parseLocal(routed);
1873
+ const preparation = { graph, invocation, routed };
1874
+ const { globals, locals, sources } = await fillScope(preparation, local);
1875
+ const held = (fault) => ({
1876
+ fault,
1877
+ globals,
1878
+ kind: 'held',
1879
+ request: null,
1880
+ result,
1881
+ });
1882
+ if (local.kind === 'held') {
1883
+ return held(local.fault);
1884
+ }
1885
+ if (sources.fault) {
1886
+ return held(sources.fault);
1887
+ }
1124
1888
  try {
1125
- return { ...(await readyDispatch(graph, routed, invocation)), kind: 'ready', result };
1889
+ const ready = await readyDispatch(preparation, {
1890
+ local,
1891
+ sources,
1892
+ values: mergeValues(globals, locals),
1893
+ });
1894
+ return { ...ready, globals, kind: 'ready', result };
1126
1895
  }
1127
1896
  catch (error) {
1128
- return { fault: error, kind: 'held', request: null, result };
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));
1129
1899
  }
1130
1900
  }