@loomcli/core 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Drew Butler
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -1,12 +1,12 @@
1
- import type { AfterAction, AttachmentConstraint, AfterArgument, AfterCommand, AfterResult, Command, CommandMethod, CommandState, ResultMethod } from './command.js';
1
+ import type { AfterAction, AttachmentConstraint, AfterArgument, AfterCommand, AfterResult, Command, CommandMethod, CommandNodeHandle, CommandState, ResultMethod } from './command.js';
2
2
  import type { ApplicationEnvironment, applicationEnvironment } from './environment.js';
3
3
  import type { ExtensionValue } from './extension.js';
4
4
  import type { GlobalsState } from './globals.js';
5
5
  import type { CommandGraph } from './inspect.js';
6
- import type { Plugin } from './plugin.js';
6
+ import type { BuiltPlugin, Plugin } from './plugin.js';
7
7
  import type { RenderingPolicy } from './rendering.js';
8
- import type { Action, ArgumentConfig, ArgumentValue, declaredTypes, DeclaredTypes, DefaultConstraint, GlobalNameConstraint, MultipleConstraint, NameConstraint, ExitCode, OptionConfig, OptionValue, ResultViews, ResultViewsOf, RowViews, RunOptions, ValidateOmittedConstraint } from './types.js';
9
- import type { ViewOverride } from './view.js';
8
+ import type { Action, ArgumentConfig, ArgumentValue, declaredTypes, DeclaredTypes, DefaultConstraint, GlobalNameConstraint, GlobalOmissionConstraint, PerValueConstraint, NameConstraint, ExitCode, OptionConfig, OptionValue, ResultViews, ResultViewsOf, RowViews, RunOptions, ValidateOmittedConstraint } from './types.js';
9
+ import type { ViewContributions, ViewOverride } from './view.js';
10
10
  /**
11
11
  * Every authoring call an Application can publish, beside `run()` and `name`, which always remain.
12
12
  * An Application's type state is a subset of these, and each call removes the names it invalidates.
@@ -22,6 +22,23 @@ export interface ApplicationOptions<Plugins extends readonly Plugin[] = readonly
22
22
  description?: string;
23
23
  version?: string;
24
24
  }
25
+ /**
26
+ * Everything an Application holds beside its root declaration and its globals, each part checked by
27
+ * the constructor or the call that set it.
28
+ */
29
+ interface ApplicationConfig {
30
+ /** Whether the application's own `command()` or `action()` has run, which closes `globalOption()`. */
31
+ composed: boolean;
32
+ /** Each installed plugin's view contributions, in installation order. */
33
+ contributors: readonly ViewContributions[];
34
+ facts: ApplicationFacts;
35
+ /** The parent that claimed each node the graph holds, so one value attaches at one point. */
36
+ owners: ReadonlyMap<CommandNodeHandle, string | null>;
37
+ plugins: readonly BuiltPlugin[];
38
+ rendering: RenderingPolicy;
39
+ /** The application's own view overrides. */
40
+ views: ViewContributions;
41
+ }
25
42
  /**
26
43
  * The Application holds the unnamed root's declaration state and applies the same transitions a
27
44
  * Command does, so each declaration call has one typed implementation and no builder to recover.
@@ -30,19 +47,17 @@ declare class ApplicationBuilder<Args, Options, Globals, State extends Applicati
30
47
  #private;
31
48
  readonly [applicationEnvironment]: ApplicationEnvironment<Globals, Plugins>;
32
49
  readonly [declaredTypes]: DeclaredTypes<Args, Options, Globals, Result>;
33
- constructor(name: string, root: CommandState<Args, Options, Globals>, config: {
34
- declared: DeclaredFacts;
35
- views: unknown;
36
- options?: unknown;
37
- plugins: Plugins | undefined;
50
+ constructor(name: string, declared: {
51
+ config: ApplicationConfig;
38
52
  globals: GlobalsState<Globals>;
53
+ root: CommandState<Args, Options, Globals>;
39
54
  });
40
55
  get name(): string;
41
- argument<const Name extends string, const Config extends ArgumentConfig>(name: Name, config: Config & NameConstraint<Name> & NoInfer<DefaultConstraint<Config>> & NoInfer<ValidateOmittedConstraint<Config>>): Application<Args & Record<Name, ArgumentValue<Config>>, Options, Globals, AfterArgument<State>, Plugins, Result>;
42
- option<const Name extends string, const Config extends OptionConfig>(name: Name, config: Config & NameConstraint<Name> & GlobalNameConstraint<Name, Globals> & NoInfer<DefaultConstraint<Config>> & NoInfer<MultipleConstraint<Config>> & NoInfer<ValidateOmittedConstraint<Config>>): Application<Args, Options & Record<Name, OptionValue<Config>>, Globals, State, Plugins, Result>;
56
+ argument<const Name extends string, const Config extends ArgumentConfig>(name: Name, config: Config & NameConstraint<Name> & NoInfer<DefaultConstraint<Config>> & NoInfer<PerValueConstraint<Config>> & NoInfer<ValidateOmittedConstraint<Config>>): Application<Args & Record<Name, ArgumentValue<Config>>, Options, Globals, AfterArgument<State>, Plugins, Result>;
57
+ option<const Name extends string, const Config extends OptionConfig>(name: Name, config: Config & NameConstraint<Name> & GlobalNameConstraint<Name, Globals> & NoInfer<DefaultConstraint<Config>> & NoInfer<PerValueConstraint<Config>> & NoInfer<ValidateOmittedConstraint<Config>>): Application<Args, Options & Record<Name, OptionValue<Config>>, Globals, State, Plugins, Result>;
43
58
  globalOption<const Name extends string, const Config extends OptionConfig>(name: Name, config: Config & NameConstraint<Name> & (Name extends keyof Options ? {
44
59
  'This option name is already declared as a local option': Name;
45
- } : unknown) & NoInfer<DefaultConstraint<Config>> & NoInfer<MultipleConstraint<Config>> & NoInfer<ValidateOmittedConstraint<Config>>): Application<Args, Options, Globals & Record<Name, OptionValue<Config>>, State, Plugins, Result>;
60
+ } : unknown) & NoInfer<DefaultConstraint<Config>> & NoInfer<PerValueConstraint<Config>> & NoInfer<ValidateOmittedConstraint<Config>> & NoInfer<GlobalOmissionConstraint<Config>>): Application<Args, Options, Globals & Record<Name, OptionValue<Config>>, State, Plugins, Result>;
46
61
  /** Registering the action closes input authoring; extension configuration remains available. */
47
62
  action(handler: Action<Args, Globals & Options, Result>): Application<Args, Options, Globals, AfterAction, Plugins, Result>;
48
63
  /** A child arrives in any type state, because its own action is the call that finished it. */
@@ -69,27 +84,26 @@ declare class ApplicationBuilder<Args, Options, Globals, State extends Applicati
69
84
  default?: string;
70
85
  }): Application<Args, Options, Globals, State, Plugins, Result>;
71
86
  extend(...values: readonly ExtensionValue<'command'>[]): Application<Args, Options, Globals, State, Plugins, Result>;
87
+ /** The globals table the root's options and every joining subtree meet. */
88
+ private table;
72
89
  /**
73
- * Root declaration calls preserve the Application configuration. The next state
74
- * travels through this call: each method names its transition in its return type, and the
75
- * wrapper publishes the same runtime value in exactly that state.
90
+ * Root declaration calls preserve the Application configuration, with the parts a call changed.
91
+ * The next state travels through this call: each method names its transition in its return type,
92
+ * and the wrapper publishes the same runtime value in exactly that state.
76
93
  */
77
94
  private derive;
78
95
  /**
79
- * Every rule that reads the declarations alone, in the order `run()` reads them: the
80
- * application's own view overrides, the options slot, the installed list, each plugin's
81
- * declarations, then the whole Command graph. The application's overrides are read and published
82
- * first, because they need core's identities and nothing else, so every later declaration error
83
- * reaches them, while a fault in that list itself reports through core's own text. The merged
84
- * registry is published once the whole build has succeeded, so a build-time fault never resolves
85
- * through a plugin's overrides, which build has not yet validated.
96
+ * The graph build, which applies the rules no earlier moment could know: the root's
97
+ * finished-Command rules and every lifecycle hook's contribution. The application's own overrides
98
+ * are published first, so a build fault reports through them. The merged registry is published
99
+ * once the build has succeeded, so a build fault never resolves through a plugin's overrides.
86
100
  */
87
101
  private prepare;
88
102
  /**
89
- * The built graph as plain, frozen data. It applies every rule `run()` applies without a schema,
90
- * in the order `run()` applies them, and throws `DeclarationError` when one fails. Validating a
91
- * declared default through its schema can be asynchronous, so that one rule stays in `run()`.
92
- * Nothing is cached: each call builds the graph anew.
103
+ * The built graph as plain, frozen data. It builds the graph as `run()` does and throws
104
+ * `DeclarationError` for the same build faults. Validating a declared default through its schema
105
+ * can be asynchronous, so that one rule stays in `run()`. Nothing is cached: each call builds the
106
+ * graph anew.
93
107
  */
94
108
  inspect(): CommandGraph;
95
109
  run(options?: RunOptions): Promise<ExitCode>;
@@ -107,11 +121,10 @@ interface ApplicationConstructor {
107
121
  new (name: string): Application<{}, {}, {}, ApplicationMethod, readonly []>;
108
122
  new <const Plugins extends readonly Plugin[] = readonly []>(name: string, options: ApplicationOptions<Plugins>): Application<{}, {}, {}, ApplicationMethod, Plugins>;
109
123
  }
110
- /** The same facts as the constructor captured them, before any rule has read them. */
111
- interface DeclaredFacts {
112
- rendering: unknown;
113
- description: unknown;
114
- version: unknown;
124
+ /** The core facts one Application declares, validated at construction and reported by `inspect()`. */
125
+ interface ApplicationFacts {
126
+ description: string | undefined;
127
+ version: string;
115
128
  }
116
129
  export declare const Application: ApplicationConstructor;
117
130
  export {};
@@ -1,13 +1,14 @@
1
1
  import { runInvocation } from './chain.js';
2
- import { attachChild, buildGraph, collectInputs, declareAction, declareExtensions, declareArgument, declareOption, declareResult, declareResultViews, freshState, recordGlobalOption, } from './command.js';
2
+ import { attachToRoot, childNode, buildGraph, checkDeclaredOptions, collectInputs, declareAction, declareExtensions, declareArgument, declareOption, declareResult, declareResultViews, freshState, isPortableName, layerOf, portableNameCorrection, } from './command.js';
3
3
  import { DeclarationError, InternalError, reasonOf, toFailure } from './errors.js';
4
+ import { storeCommandLayers } from './extension.js';
4
5
  import { checkDescription, checkNoListingFacts, checkVersion, isPlainObject } from './facts.js';
5
- import { declareGlobalOption, emptyGlobals } from './globals.js';
6
+ import { declareGlobalOption, emptyGlobals, globalTable } from './globals.js';
6
7
  import { captureHost } from './host.js';
7
8
  import { inspectGraph } from './inspect.js';
8
9
  import { coreViews } from './lanes.js';
9
10
  import { Output, reportPlainly } from './output.js';
10
- import { buildPlugins, installPlugins, ownedSignals, pluginSentence } from './plugin.js';
11
+ import { installPlugins, ownedSignals, pluginSentence } from './plugin.js';
11
12
  import { renderingPolicy } from './rendering.js';
12
13
  import { bracketRun, cancellationCode, isCancellationEcho } from './signals.js';
13
14
  import { captureConfig, checkDeclarations, prepareInputs } from './validation.js';
@@ -64,24 +65,13 @@ function checkSignal(signal) {
64
65
  class ApplicationBuilder {
65
66
  #name;
66
67
  #root;
67
- // The override list the constructor read out of the options slot, unexamined until build.
68
- #views;
69
- #plugins;
70
68
  #globals;
71
- // The constructor's raw options argument, kept for the slot's own shape rules.
72
- // The options-slot rules answer at the same point every other authoring fault does:
73
- // `inspect()` and `run()`.
74
- #options;
75
- // The facts the constructor read out of that slot, unexamined until build.
76
- #declared;
77
- constructor(name, root, config) {
78
- this.#declared = config.declared;
79
- this.#views = config.views;
69
+ #config;
70
+ constructor(name, declared) {
80
71
  this.#name = name;
81
- this.#options = config.options;
82
- this.#plugins = config.plugins;
83
- this.#globals = config.globals;
84
- this.#root = root;
72
+ this.#config = declared.config;
73
+ this.#globals = declared.globals;
74
+ this.#root = declared.root;
85
75
  }
86
76
  get name() {
87
77
  return this.#name;
@@ -100,29 +90,38 @@ class ApplicationBuilder {
100
90
  kind: 'option',
101
91
  name,
102
92
  };
103
- return this.derive(declareOption(this.#root, input));
93
+ return this.derive(declareOption(this.#root, input, this.table()));
104
94
  }
105
95
  globalOption(name, config) {
96
+ // The plugins' Commands attach at construction, so only the application's own calls close it.
97
+ if (this.#config.composed) {
98
+ throw new DeclarationError(`The Application declares global option "${name}" after command() or action(). Declare global options before attaching Commands or registering an action.`);
99
+ }
106
100
  const input = {
107
101
  config: captureConfig(config),
108
102
  kind: 'option',
109
103
  name,
110
104
  };
111
- return new ApplicationBuilder(this.#name, recordGlobalOption(this.#root, name), {
112
- declared: this.#declared,
113
- globals: declareGlobalOption(this.#globals, input),
114
- options: this.#options,
115
- plugins: this.#plugins,
116
- views: this.#views,
117
- });
105
+ const descriptors = new Map(this.#root.descriptors);
106
+ const globals = declareGlobalOption(this.#globals, input, descriptors);
107
+ const root = { ...this.#root, descriptors };
108
+ checkDeclaredOptions(root, globalTable(globals.inputs, this.#config.plugins));
109
+ checkDeclarations([input]);
110
+ return new ApplicationBuilder(this.#name, { config: this.#config, globals, root });
118
111
  }
119
112
  /** Registering the action closes input authoring; extension configuration remains available. */
120
113
  action(handler) {
121
- return this.derive(declareAction(this.#root, handler));
114
+ return this.derive(declareAction(this.#root, handler), { composed: true });
122
115
  }
123
116
  /** A child arrives in any type state, because its own action is the call that finished it. */
124
117
  command(child) {
125
- return this.derive(attachChild(this.#root, child));
118
+ const scope = {
119
+ descriptors: new Map(this.#root.descriptors),
120
+ owners: new Map(this.#config.owners),
121
+ table: this.table(),
122
+ };
123
+ const root = attachToRoot(this.#root, childNode(null, child), scope);
124
+ return this.derive(root, { composed: true, owners: scope.owners });
126
125
  }
127
126
  /**
128
127
  * The value the root action produces for its consumer. The type argument is stated by the
@@ -142,50 +141,38 @@ class ApplicationBuilder {
142
141
  extend(...values) {
143
142
  return this.derive(declareExtensions(this.#root, values));
144
143
  }
144
+ /** The globals table the root's options and every joining subtree meet. */
145
+ table() {
146
+ return globalTable(this.#globals.inputs, this.#config.plugins);
147
+ }
145
148
  /**
146
- * Root declaration calls preserve the Application configuration. The next state
147
- * travels through this call: each method names its transition in its return type, and the
148
- * wrapper publishes the same runtime value in exactly that state.
149
+ * Root declaration calls preserve the Application configuration, with the parts a call changed.
150
+ * The next state travels through this call: each method names its transition in its return type,
151
+ * and the wrapper publishes the same runtime value in exactly that state.
149
152
  */
150
- derive(root) {
151
- return new ApplicationBuilder(this.#name, root, {
152
- declared: this.#declared,
153
- globals: this.#globals,
154
- options: this.#options,
155
- plugins: this.#plugins,
156
- views: this.#views,
157
- });
153
+ derive(root, changed = {}) {
154
+ return new ApplicationBuilder(this.#name, { config: { ...this.#config, ...changed }, globals: this.#globals, root });
158
155
  }
159
156
  /**
160
- * Every rule that reads the declarations alone, in the order `run()` reads them: the
161
- * application's own view overrides, the options slot, the installed list, each plugin's
162
- * declarations, then the whole Command graph. The application's overrides are read and published
163
- * first, because they need core's identities and nothing else, so every later declaration error
164
- * reaches them, while a fault in that list itself reports through core's own text. The merged
165
- * registry is published once the whole build has succeeded, so a build-time fault never resolves
166
- * through a plugin's overrides, which build has not yet validated.
157
+ * The graph build, which applies the rules no earlier moment could know: the root's
158
+ * finished-Command rules and every lifecycle hook's contribution. The application's own overrides
159
+ * are published first, so a build fault reports through them. The merged registry is published
160
+ * once the build has succeeded, so a build fault never resolves through a plugin's overrides.
167
161
  */
168
162
  prepare(stage) {
169
- const identities = viewIdentities(coreViews);
170
- const application = buildViews({ declares: false, sentence: 'The Application' }, this.#views, identities);
171
- stage.views([application]);
172
- stage.rendering(renderingPolicy(this.#declared.rendering));
173
- const facts = checkOptions(this.#options, this.#declared);
174
- const installed = installPlugins(this.#plugins ?? []);
175
- const install = { descriptors: new Map(), extensions: new Map() };
176
- const plugins = buildPlugins(installed, install);
163
+ const { contributors, facts, plugins, rendering, views } = this.#config;
164
+ stage.views([views]);
165
+ stage.rendering(rendering);
177
166
  stage.plugins(plugins);
178
- const contributors = plugins.map((entry) => buildViews({ declares: true, sentence: pluginSentence(entry.identity) }, entry.views, identities));
179
- const graph = buildGraph(this.#root, this.#globals, { ...install, plugins });
180
- checkDeclarations([...graph.globals.inputs, ...collectInputs(graph.root)]);
181
- stage.views([application, ...contributors]);
167
+ const graph = buildGraph(this.#root, this.#globals, plugins);
168
+ stage.views([views, ...contributors]);
182
169
  return { facts, graph, plugins };
183
170
  }
184
171
  /**
185
- * The built graph as plain, frozen data. It applies every rule `run()` applies without a schema,
186
- * in the order `run()` applies them, and throws `DeclarationError` when one fails. Validating a
187
- * declared default through its schema can be asynchronous, so that one rule stays in `run()`.
188
- * Nothing is cached: each call builds the graph anew.
172
+ * The built graph as plain, frozen data. It builds the graph as `run()` does and throws
173
+ * `DeclarationError` for the same build faults. Validating a declared default through its schema
174
+ * can be asynchronous, so that one rule stays in `run()`. Nothing is cached: each call builds the
175
+ * graph anew.
189
176
  */
190
177
  inspect() {
191
178
  const built = this.prepare({
@@ -231,8 +218,7 @@ class ApplicationBuilder {
231
218
  const host = captureHost(overrides, stderr);
232
219
  const invocationOutput = new Output(host, controller.signal);
233
220
  output = invocationOutput;
234
- // The policy is read inside the build, after the application's own overrides are published.
235
- // A faulty rendering declaration then reports through the view the application listed.
221
+ // The constructor validated the declared policy, which the build hands over after the overrides.
236
222
  let policy = {};
237
223
  signals = bracketRun(controller, checkSignal(options?.signal));
238
224
  const built = this.prepare({
@@ -272,6 +258,7 @@ class ApplicationBuilder {
272
258
  invocationOutput.useRoute(path);
273
259
  },
274
260
  signal: controller.signal,
261
+ sourceOut: output.sourceOut,
275
262
  style: output.style,
276
263
  });
277
264
  }
@@ -366,58 +353,83 @@ class ApplicationBuilder {
366
353
  }
367
354
  }
368
355
  /** Reject obsolete wiring before silently losing options that invocations depend on. */
369
- function checkOptions(options, declared) {
370
- if (options !== undefined) {
371
- if (!isPlainObject(options)) {
372
- throw new DeclarationError('The Application options must be an object. Supply an Application options object.');
373
- }
374
- if ('globals' in options) {
375
- throw new DeclarationError('The Application options contain globals. Declare them with globalOption(name, config).');
376
- }
377
- if ('failures' in options) {
378
- throw new DeclarationError('The Application options contain failures. Declare view overrides under views with override(key, view).');
379
- }
380
- // The root is every page's entry point, so it carries neither listing fact.
381
- // A key that may not be there is a fault of the slot, so it answers with the slot's shape.
382
- checkNoListingFacts('The Application', options);
356
+ function checkOptions(options) {
357
+ if (options === undefined) {
358
+ return { description: undefined, version: checkVersion(undefined) };
359
+ }
360
+ if (!isPlainObject(options)) {
361
+ throw new DeclarationError('The Application options must be an object. Supply an Application options object.');
362
+ }
363
+ if ('globals' in options) {
364
+ throw new DeclarationError('The Application options contain globals. Declare them with globalOption(name, config).');
365
+ }
366
+ if ('failures' in options) {
367
+ throw new DeclarationError('The Application options contain failures. Declare view overrides under views with override(key, view).');
383
368
  }
369
+ // The root is every page's entry point, so it carries neither listing fact.
370
+ // A key that may not be there is a fault of the slot, so it answers with the slot's shape.
371
+ checkNoListingFacts('The Application', options);
384
372
  return {
385
- description: checkDescription('The Application', declared.description),
386
- version: checkVersion(declared.version),
373
+ description: checkDescription('The Application', options.description),
374
+ version: checkVersion(options.version),
387
375
  };
388
376
  }
377
+ /**
378
+ * Every rule `new Application(name, options)` applies, in the order it reads the slot: the
379
+ * application's own view overrides, the rendering policy, the options slot and its facts, the
380
+ * installed list and every rule between two plugins, the root's extension values, and then each
381
+ * plugin's Commands, which attach to the root first, in installation order and list order.
382
+ */
383
+ function declareApplication(options) {
384
+ const slot = isPlainObject(options) ? options : undefined;
385
+ const identities = viewIdentities(coreViews);
386
+ const views = buildViews({ declares: false, sentence: 'The Application' }, slot?.views, identities);
387
+ const rendering = renderingPolicy(slot?.rendering);
388
+ const facts = checkOptions(options);
389
+ const installed = installPlugins(slot?.plugins ?? []);
390
+ const { plugins } = installed;
391
+ const contributors = plugins.map((entry) => buildViews({ declares: true, sentence: pluginSentence(entry.identity) }, entry.views, identities));
392
+ const table = globalTable([], plugins);
393
+ const descriptors = installed.descriptors;
394
+ const extensions = storeCommandLayers({
395
+ descriptors,
396
+ layers: [slot?.extensions],
397
+ subject: layerOf(null),
398
+ });
399
+ // The Application checks its own facts, so the root carries none.
400
+ // Its diagnostics name the Application rather than the root Command.
401
+ let root = freshState({
402
+ descriptors,
403
+ extensions,
404
+ facts: { deprecated: undefined, description: undefined, hidden: false },
405
+ name: null,
406
+ });
407
+ const owners = new Map();
408
+ for (const command of plugins.flatMap((entry) => entry.commands)) {
409
+ root = attachToRoot(root, command.node, {
410
+ descriptors: new Map(root.descriptors),
411
+ owners,
412
+ table,
413
+ });
414
+ }
415
+ return {
416
+ config: { composed: false, contributors, facts, owners, plugins, rendering, views },
417
+ globals: emptyGlobals(),
418
+ root,
419
+ };
420
+ }
421
+ /** The application name is typed as a command at the prompt, so it answers to the portable rule. */
422
+ function checkApplicationName(name) {
423
+ if (!isPortableName(name)) {
424
+ throw new DeclarationError(`Application name "${String(name)}" is invalid. ${portableNameCorrection}`);
425
+ }
426
+ return name;
427
+ }
389
428
  /** Constructor inference preserves the installed plugin tuple; globals start empty. */
390
429
  class ApplicationDeclaration extends ApplicationBuilder {
391
430
  constructor(name, options) {
392
- // The options slot is read defensively, never inspected.
393
- // An invalid value still yields `views` and the facts of some kind.
394
- // `checkOptions` reports such a value at build.
395
- // The root's own slot and core facts stay empty, because the Application checks its own slot.
396
- // Its diagnostics name the Application rather than the root Command.
397
- super(name, freshState({
398
- deprecated: undefined,
399
- description: undefined,
400
- extensions: options?.extensions,
401
- hidden: undefined,
402
- name: null,
403
- options: undefined,
404
- }), {
405
- declared: {
406
- description: options?.description,
407
- rendering: isPlainObject(options?.rendering)
408
- ? { ...options.rendering }
409
- : options?.rendering,
410
- version: options?.version,
411
- },
412
- globals: emptyGlobals(),
413
- options,
414
- plugins: options?.plugins,
415
- // The read is loose because the slot is reachable from JavaScript with any value at all.
416
- // An Application value passed here answers `views` with its own authoring method.
417
- // The public `ApplicationOptions.views` stays exactly `readonly ViewOverride[]`.
418
- // An options slot that is no plain object carries no override list, and its own rule reports it.
419
- views: isPlainObject(options) ? options.views : undefined,
420
- });
431
+ // The arguments evaluate in order, so the name is checked before any option is read.
432
+ super(checkApplicationName(name), declareApplication(options));
421
433
  }
422
434
  }
423
435
  export const Application = ApplicationDeclaration;
@@ -0,0 +1,26 @@
1
+ /** The two keys the binding rules read, whichever scope declared the option. */
2
+ interface BindingConfig {
3
+ readonly env?: unknown;
4
+ readonly multiple?: unknown;
5
+ }
6
+ /**
7
+ * The environment binding one option declares, or `undefined` when it declares none. A multiple
8
+ * option takes its list from the configuration source, so it cannot bind, and a bound name must be
9
+ * a variable name. `sentence` names the declaration at the start of the diagnostic.
10
+ */
11
+ export declare function checkEnvBinding(sentence: string, config: BindingConfig): string | undefined;
12
+ /** An argument is identified by its place among bare tokens, so no variable can stand in for it. */
13
+ export declare function checkNoArgumentBinding(sentence: string, config: object): void;
14
+ /** One option bound to a variable, with the phrase a duplicate-variable diagnostic names it by. */
15
+ export interface BoundOption {
16
+ readonly site: string;
17
+ readonly variable: string;
18
+ }
19
+ /**
20
+ * The variables one scope binds, each to the phrase of the option that binds it. Within one
21
+ * invocation's scope a variable binds one option, so a second binder is a declaration error that
22
+ * names the first binder, in scope order, and then the second. `held` is what an enclosing scope
23
+ * already binds, such as the globals table under a Command's own options.
24
+ */
25
+ export declare function claimVariables(bound: readonly BoundOption[], held?: ReadonlyMap<string, string>): ReadonlyMap<string, string>;
26
+ export {};
@@ -0,0 +1,45 @@
1
+ import { DeclarationError } from './errors.js';
2
+ /** A variable name: a letter or an underscore, then letters, digits, or underscores. */
3
+ const variableName = /^[A-Za-z_][A-Za-z0-9_]*$/u;
4
+ /**
5
+ * The environment binding one option declares, or `undefined` when it declares none. A multiple
6
+ * option takes its list from the configuration source, so it cannot bind, and a bound name must be
7
+ * a variable name. `sentence` names the declaration at the start of the diagnostic.
8
+ */
9
+ export function checkEnvBinding(sentence, config) {
10
+ const env = config.env;
11
+ if (env === undefined) {
12
+ return undefined;
13
+ }
14
+ if (config.multiple === true) {
15
+ throw new DeclarationError(`${sentence} is a multiple option and declares env. Remove env; a list comes from the configuration source.`);
16
+ }
17
+ if (typeof env !== 'string' || !variableName.test(env)) {
18
+ const quoted = typeof env === 'string' ? ` "${env}"` : '';
19
+ throw new DeclarationError(`${sentence} env${quoted} is not a variable name. Use a letter or an underscore, then letters, digits, or underscores.`);
20
+ }
21
+ return env;
22
+ }
23
+ /** An argument is identified by its place among bare tokens, so no variable can stand in for it. */
24
+ export function checkNoArgumentBinding(sentence, config) {
25
+ if ('env' in config) {
26
+ throw new DeclarationError(`${sentence} declares env, which applies to options alone. Remove it.`);
27
+ }
28
+ }
29
+ /**
30
+ * The variables one scope binds, each to the phrase of the option that binds it. Within one
31
+ * invocation's scope a variable binds one option, so a second binder is a declaration error that
32
+ * names the first binder, in scope order, and then the second. `held` is what an enclosing scope
33
+ * already binds, such as the globals table under a Command's own options.
34
+ */
35
+ export function claimVariables(bound, held = new Map()) {
36
+ const claimed = new Map(held);
37
+ for (const { site, variable } of bound) {
38
+ const owner = claimed.get(variable);
39
+ if (owner !== undefined) {
40
+ throw new DeclarationError(`Variable "${variable}" is bound by ${owner} and ${site}. Bind each variable to one option.`);
41
+ }
42
+ claimed.set(variable, site);
43
+ }
44
+ return claimed;
45
+ }
package/dist/chain.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import type { BuiltGraph } from './command.js';
2
2
  import type { LoomError } from './errors.js';
3
3
  import type { CommandGraph, CommandNode } from './inspect.js';
4
- import type { BuiltPlugin, PluginOptions, PluginOptionValues } from './plugin.js';
4
+ import type { BuiltPlugin, PluginOptions, PluginOptionSpellings, PluginOptionValues } from './plugin.js';
5
5
  import type { ContextualStyle } from './style.js';
6
6
  import type { ActionChannel, Host, OpenResult, Out, Request, ResultBinding } from './types.js';
7
7
  import type { DefaultValues } from './validation.js';
@@ -13,14 +13,17 @@ type ChainOutcome = 'cancelled' | 'dispatched' | 'taken-over';
13
13
  /**
14
14
  * What one middleware receives. `graph` is the frozen graph `inspect()` returns, built once for the
15
15
  * run, and `command` is the routed node inside it. `options` holds this plugin's own option values
16
- * and never another plugin's or the application's globals. `request` is the routed Command's
17
- * invocation, parsed and validated ahead of the chain, and `null` while core holds a fault and on a
18
- * group. `view` names the view the result renders through: it reads as the declaration's default
19
- * until a middleware assigns one, and as `null` on a Command that declares none. The last
20
- * assignment before the dispatch boundary wins, and one made after it changes nothing.
16
+ * and never another plugin's or the application's globals, and `spellings` holds the spelling that
17
+ * supplied each of them as a token, which a filled or defaulted option never has. `request` is the
18
+ * routed Command's invocation, parsed and validated ahead of the chain, and `null` while core holds
19
+ * a fault and on a group. `view` names the view the result renders through: it reads as the
20
+ * declaration's default until a middleware assigns one, and as `null` on a Command that declares
21
+ * none. The last assignment before the dispatch boundary wins, and one made after it changes
22
+ * nothing.
21
23
  */
22
24
  interface MiddlewareContext<Options extends PluginOptions = PluginOptions> {
23
25
  readonly options: PluginOptionValues<Options>;
26
+ readonly spellings: PluginOptionSpellings<Options>;
24
27
  readonly graph: CommandGraph;
25
28
  readonly command: CommandNode;
26
29
  readonly request: Request | null;
@@ -49,6 +52,8 @@ interface Invocation {
49
52
  * receives the channel the results lane builds for the Command that was routed.
50
53
  */
51
54
  out: Out<OpenResult>;
55
+ /** The channel a configuration source receives, whose results call names the source. */
56
+ sourceOut: Out<OpenResult>;
52
57
  plugins: readonly BuiltPlugin[];
53
58
  /** A fault reported after the primary outcome, which turns a would-be 0 into 1. */
54
59
  report: (fault: LoomError) => void;