@loomcli/core 0.6.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/NOTICE +34 -0
  2. package/dist/application.d.ts +11 -6
  3. package/dist/application.js +57 -19
  4. package/dist/capture.d.ts +65 -0
  5. package/dist/capture.js +99 -0
  6. package/dist/chain.d.ts +28 -17
  7. package/dist/chain.js +11 -10
  8. package/dist/command-rules.d.ts +1 -1
  9. package/dist/command-rules.js +2 -2
  10. package/dist/command.d.ts +31 -47
  11. package/dist/command.js +204 -209
  12. package/dist/errors.d.ts +22 -21
  13. package/dist/errors.js +38 -18
  14. package/dist/facts.d.ts +5 -0
  15. package/dist/facts.js +8 -1
  16. package/dist/globals.d.ts +48 -22
  17. package/dist/globals.js +72 -29
  18. package/dist/glyphs.generated.js +1 -1
  19. package/dist/index.d.ts +5 -3
  20. package/dist/index.js +3 -1
  21. package/dist/input-rules.d.ts +20 -2
  22. package/dist/input-rules.js +47 -5
  23. package/dist/inspect.d.ts +33 -17
  24. package/dist/inspect.js +74 -87
  25. package/dist/locate.js +52 -60
  26. package/dist/options.d.ts +101 -83
  27. package/dist/options.js +311 -267
  28. package/dist/parse.d.ts +162 -0
  29. package/dist/parse.js +601 -0
  30. package/dist/plain.d.ts +52 -2
  31. package/dist/plain.js +228 -2
  32. package/dist/plugin-rules.d.ts +8 -4
  33. package/dist/plugin-rules.js +13 -9
  34. package/dist/plugin-settings.d.ts +18 -0
  35. package/dist/plugin-settings.js +38 -0
  36. package/dist/plugin.d.ts +32 -27
  37. package/dist/plugin.js +107 -89
  38. package/dist/sources.d.ts +18 -9
  39. package/dist/sources.js +50 -20
  40. package/dist/style-layout.js +2 -2
  41. package/dist/style-width.d.ts +13 -0
  42. package/dist/style-width.js +170 -0
  43. package/dist/types.d.ts +70 -30
  44. package/dist/unicode.generated.d.ts +27 -0
  45. package/dist/unicode.generated.js +1036 -0
  46. package/dist/validation.d.ts +108 -34
  47. package/dist/validation.js +282 -125
  48. package/dist/view.d.ts +16 -11
  49. package/dist/view.js +13 -4
  50. package/licenses/unicode-LICENSE.txt +41 -0
  51. package/licenses/uucode-LICENSE.md +35 -0
  52. package/package.json +9 -5
package/NOTICE ADDED
@@ -0,0 +1,34 @@
1
+ @loomcli/core
2
+
3
+ This package includes material from uucode and from the Unicode Character
4
+ Database. Its license field reads "MIT AND Unicode-3.0": the Unicode tables
5
+ named below stay under the Unicode License v3, and every other file is under
6
+ the MIT License in LICENSE or, for the ported width iterator, in
7
+ licenses/uucode-LICENSE.md. Neither license asks anything of an application
8
+ that installs this package beyond keeping these notices with the files they
9
+ cover.
10
+
11
+ Unicode Character Database
12
+ Copyright © 1991-2025 Unicode, Inc.
13
+ Licensed under the Unicode License v3.
14
+ A copy of the License is in licenses/unicode-LICENSE.txt, and at
15
+ https://www.unicode.org/license.txt
16
+
17
+ uucode
18
+ Copyright (c) 2026 Tim Culverhouse
19
+ Licensed under the MIT License.
20
+ A copy of the License is in licenses/uucode-LICENSE.md.
21
+ Source: @rockorager/uucode 2.2.1 on npm
22
+
23
+ The Unicode tables core measures text with, in dist/unicode.generated.js, are
24
+ generated by the Loom repository's scripts/generate-unicode-tables.mjs from
25
+ the data files of @rockorager/uucode 2.2.1, which derive from Unicode 17 data.
26
+ The generator writes the same values as a JavaScript module, so a bundle
27
+ carries them.
28
+
29
+ The grapheme width iterator in dist/style-width.js is ported from the
30
+ @rockorager/uucode 2.2.1 width module. Changes made to it:
31
+
32
+ - It is written in TypeScript and reads the generated tables module.
33
+ - It yields each grapheme's start, end, and width, and no longer yields the
34
+ grapheme's text or offers clone, peek, or a standalone string width.
@@ -3,10 +3,10 @@ import type { ApplicationEnvironment, applicationEnvironment } from './environme
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 { BuiltPlugin, Plugin } from './plugin.js';
6
+ import type { BuiltPlugin, InstalledOptionValues, Plugin } from './plugin.js';
7
7
  import type { RenderingPolicy } from './rendering.js';
8
8
  import type { Translation, TranslatorRegistry } from './translators.js';
9
- 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 { Action, ArgumentConfig, ArgumentValue, declaredTypes, DeclaredTypes, DefaultConstraint, ImpliedConstraint, GlobalNameConstraint, GlobalOmissionConstraint, PerValueConstraint, NameConstraint, ExitCode, OptionConfig, OptionValue, ResultViews, ResultViewsOf, RowViews, RunOptions, ValidateOmittedConstraint } from './types.js';
10
10
  import type { ViewContributions, ViewOverride } from './view.js';
11
11
  /**
12
12
  * Every authoring call an Application can publish, beside `run()` and `name`, which always remain.
@@ -65,15 +65,15 @@ declare class ApplicationBuilder<Args, Options, Globals, State extends Applicati
65
65
  readonly [declaredTypes]: DeclaredTypes<Args, Options, Globals, Result>;
66
66
  constructor(name: string, declared: {
67
67
  config: ApplicationConfig;
68
- globals: GlobalsState<Globals>;
68
+ globals: GlobalsState;
69
69
  root: CommandState<Args, Options, Globals>;
70
70
  });
71
71
  get name(): string;
72
72
  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>;
73
- 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>;
73
+ option<const Name extends string, const Config extends OptionConfig>(name: Name, config: Config & NameConstraint<Name> & GlobalNameConstraint<Name, Globals> & NoInfer<DefaultConstraint<Config>> & NoInfer<ImpliedConstraint<Config>> & NoInfer<PerValueConstraint<Config>> & NoInfer<ValidateOmittedConstraint<Config>>): Application<Args, Options & Record<Name, OptionValue<Config>>, Globals, State, Plugins, Result>;
74
74
  globalOption<const Name extends string, const Config extends OptionConfig>(name: Name, config: Config & NameConstraint<Name> & (Name extends keyof Options ? {
75
75
  'This option name is already declared as a local option': Name;
76
- } : unknown) & NoInfer<DefaultConstraint<Config>> & NoInfer<PerValueConstraint<Config>> & NoInfer<ValidateOmittedConstraint<Config>> & NoInfer<GlobalOmissionConstraint<Config>>): Application<Args, Options, Globals & Record<Name, OptionValue<Config>>, State, Plugins, Result>;
76
+ } : unknown) & NoInfer<DefaultConstraint<Config>> & NoInfer<ImpliedConstraint<Config>> & NoInfer<PerValueConstraint<Config>> & NoInfer<ValidateOmittedConstraint<Config>> & NoInfer<GlobalOmissionConstraint<Config>>): Application<Args, Options, Globals & Record<Name, OptionValue<Config>>, State, Plugins, Result>;
77
77
  /** Registering the action closes input authoring; extension configuration remains available. */
78
78
  action(handler: Action<Args, Globals & Options, Result>): Application<Args, Options, Globals, AfterAction, Plugins, Result>;
79
79
  /** A child arrives in any type state, because its own action is the call that finished it. */
@@ -133,9 +133,14 @@ declare class ApplicationBuilder<Args, Options, Globals, State extends Applicati
133
133
  * `Application<A, O, G>` accepts an application in any state, a finished one included.
134
134
  */
135
135
  export type Application<Args = {}, Options = {}, Globals = {}, State extends ApplicationMethod = AfterAction, Plugins extends readonly Plugin[] = readonly Plugin[], Result = unknown> = Pick<ApplicationBuilder<Args, Options, Globals, State, Plugins, Result>, typeof applicationEnvironment | typeof declaredTypes | 'extend' | 'inspect' | 'name' | 'run' | State | ResultMethod<Result>>;
136
+ /**
137
+ * An Application starts with the option values its installed plugins contribute as its globals,
138
+ * computed once from the constructor's `plugins`, so every action reads them typed and each
139
+ * `globalOption()` call adds its own to them.
140
+ */
136
141
  interface ApplicationConstructor {
137
142
  new (name: string): Application<{}, {}, {}, ApplicationMethod, readonly []>;
138
- new <const Plugins extends readonly Plugin[] = readonly []>(name: string, options: ApplicationOptions<Plugins>): Application<{}, {}, {}, ApplicationMethod, Plugins>;
143
+ new <const Plugins extends readonly Plugin[] = readonly []>(name: string, options: ApplicationOptions<Plugins>): Application<{}, {}, InstalledOptionValues<Plugins>, ApplicationMethod, Plugins>;
139
144
  }
140
145
  /** The core facts one Application declares, validated at construction and reported by `inspect()`. */
141
146
  interface ApplicationFacts {
@@ -1,7 +1,9 @@
1
+ import { captureDeclaration, unreadableArgument } from './capture.js';
1
2
  import { runInvocation } from './chain.js';
2
3
  import { portableName } from './command-rules.js';
3
4
  import { attachToRoot, callArguments, childNode, buildGraph, checkDeclaredOptions, collectInputs, commandPlacement, inputPlaces, declareAction, declareExtensions, declareArgument, declareOption, declareResult, declareResultViews, freshState, isPortableName, layerOf, portableNameCorrection, } from './command.js';
4
5
  import { escapeControlCharacters } from './controls.js';
6
+ import { elided, spelled } from './diagnostic-text.js';
5
7
  import { DeclarationError, exitCodeOf, InternalError, quoted, reasonOf, toFailure, } from './errors.js';
6
8
  import { storeCommandLayers } from './extension.js';
7
9
  import { checkDescription, checkNoListingFacts, checkVersion, partFinding, slotSite, } from './facts.js';
@@ -12,14 +14,14 @@ import { globalOptionAfterCommand } from './input-rules.js';
12
14
  import { inspectGraph } from './inspect.js';
13
15
  import { coreViews } from './lanes.js';
14
16
  import { Output, reportPlainly } from './output.js';
15
- import { isPlainObject } from './plain.js';
17
+ import { declaring, isPlainObject, shallowList, shallowRecord } from './plain.js';
16
18
  import { invalidPacket, notAnObject, retiredApplicationOption } from './plugin-rules.js';
17
19
  import { installPlugins, ownedSignals, pluginViews } from './plugin.js';
18
20
  import { renderingPolicy } from './rendering.js';
19
21
  import { brokenOutputView, runOptions, viewCorrection } from './rules.js';
20
22
  import { bracketRun, cancellationCode, isCancellationEcho } from './signals.js';
21
23
  import { readTranslations, translateThrow } from './translators.js';
22
- import { checkDeclarations, prepareInputs } from './validation.js';
24
+ import { checkDeclarations, prepareDeclaredValues } from './validation.js';
23
25
  import { buildViews, viewIdentities } from './view.js';
24
26
  /** The registry a failure is reported through when the application's own could not be built. */
25
27
  const noViews = [];
@@ -133,11 +135,17 @@ class ApplicationBuilder {
133
135
  }
134
136
  globalOption(name, config) {
135
137
  // The plugins' Commands attach at construction, so only the application's own calls close it.
138
+ // The config is judged after the order, so the finding prints it elided and reads none of it.
136
139
  if (this.#config.composed) {
137
140
  throw new DeclarationError(globalOptionAfterCommand, {
138
141
  correction: 'Declare global options before attaching Commands or registering an action.',
139
142
  findings: [
140
- { arguments: callArguments(name, config), call: 'globalOption', mark: '0', path: [] },
143
+ {
144
+ arguments: callArguments(name, config === undefined ? undefined : spelled(elided)),
145
+ call: 'globalOption',
146
+ mark: '0',
147
+ path: [],
148
+ },
141
149
  ],
142
150
  sentence: `The Application declares global option ${quoted(name)} after command() or action().`,
143
151
  });
@@ -331,7 +339,8 @@ class ApplicationBuilder {
331
339
  inspected();
332
340
  }
333
341
  const inputs = { globals: graph.globals.inputs, locals: collectInputs(graph.root) };
334
- const defaults = await prepareInputs(inputs, host, inputPlaces(graph));
342
+ const places = inputPlaces(graph);
343
+ const declaredValues = await prepareDeclaredValues(inputs, host, places);
335
344
  graphBuilt = true;
336
345
  if (!controller.signal.aborted) {
337
346
  /**
@@ -341,12 +350,13 @@ class ApplicationBuilder {
341
350
  signals.install(ownedSignals(built.plugins));
342
351
  await runInvocation({
343
352
  channel: (binding) => invocationOutput.channel(binding),
344
- defaults,
353
+ declaredValues,
345
354
  graph,
346
355
  host,
347
356
  inspected,
348
357
  offer,
349
358
  out: output.out,
359
+ places,
350
360
  plugins: built.plugins,
351
361
  report: (fault) => faults.push(fault),
352
362
  route: (path) => {
@@ -481,7 +491,10 @@ function runRendering(rendering) {
481
491
  subject: 'The run',
482
492
  };
483
493
  }
484
- /** Reject obsolete wiring before silently losing options that invocations depend on. */
494
+ /**
495
+ * Reject obsolete wiring before silently losing options that invocations depend on. `options` is the
496
+ * copy `captureOptions` took, or `undefined` for an Application declared without options.
497
+ */
485
498
  function checkOptions(name, options) {
486
499
  const site = {
487
500
  at: '1',
@@ -491,13 +504,6 @@ function checkOptions(name, options) {
491
504
  if (options === undefined) {
492
505
  return { description: undefined, version: checkVersion(site, undefined) };
493
506
  }
494
- if (!isPlainObject(options)) {
495
- throw new DeclarationError(notAnObject, {
496
- correction: 'Supply an Application options object.',
497
- findings: [{ ...site.declaration, mark: '1' }],
498
- sentence: 'The Application declares options that are not an object.',
499
- });
500
- }
501
507
  for (const [key, correction] of retired) {
502
508
  if (key in options) {
503
509
  const retiredSite = optionSite(name, key, Reflect.get(options, key));
@@ -546,15 +552,44 @@ function readPacket(name, packet) {
546
552
  sentence: found,
547
553
  });
548
554
  }
555
+ /** The parts of the Application options that are lists, each copied with its entries as built. */
556
+ const optionLists = ['plugins', 'views', 'translators', 'extensions'];
557
+ /**
558
+ * The one copy of the options that `new Application(name, options)` reads: the options object, its
559
+ * packet and rendering policy, and each list it holds. An entry of a list is what its factory built
560
+ * and is not copied. A read that throws is the unreadable fault of the options, and a value that is
561
+ * not a plain object is the not-an-object fault. An Application declared without options has none
562
+ * to copy.
563
+ */
564
+ function captureOptions(name, options) {
565
+ if (options === undefined) {
566
+ return undefined;
567
+ }
568
+ return captureDeclaration(options, (copy, read) => {
569
+ read.nested(copy, 'packet', shallowRecord);
570
+ read.nested(copy, 'rendering', shallowRecord);
571
+ for (const list of optionLists) {
572
+ read.nested(copy, list, shallowList);
573
+ }
574
+ }, {
575
+ notAnObject: () => new DeclarationError(notAnObject, {
576
+ correction: 'Supply an Application options object.',
577
+ findings: [{ arguments: [name, options], call: 'new Application', mark: '1' }],
578
+ sentence: 'The Application declares options that are not an object.',
579
+ }),
580
+ unreadable: unreadableArgument({ call: 'new Application', named: name, subject: 'The Application' }, 'options'),
581
+ });
582
+ }
549
583
  /**
550
584
  * Every rule `new Application(name, options)` applies, in the order it reads the slot: the
551
585
  * application's own view overrides, its translations, the rendering policy, the options slot and
552
586
  * its facts, the installed list and every rule between two plugins, the root's extension values,
553
587
  * and then each plugin's Commands, which attach to the root first, in installation order and list
554
- * order.
588
+ * order. Each reads the one copy of the options `captureOptions` takes. The root's globals are
589
+ * the option values the declared `plugins` tuple contributes.
555
590
  */
556
- function declareApplication(name, options) {
557
- const slot = isPlainObject(options) ? options : undefined;
591
+ function declareApplication(name, declared) {
592
+ const slot = captureOptions(name, declared);
558
593
  const identities = viewIdentities(coreViews);
559
594
  const views = buildViews({
560
595
  declares: false,
@@ -563,7 +598,7 @@ function declareApplication(name, options) {
563
598
  }, slot?.views, identities);
564
599
  const translations = readTranslations(optionSite(name, 'translators', slot?.translators), slot?.translators);
565
600
  const rendering = renderingPolicy(slot?.rendering, optionSite(name, 'rendering', slot?.rendering));
566
- const facts = checkOptions(name, options);
601
+ const facts = checkOptions(name, slot);
567
602
  const development = readPacket(name, slot?.packet);
568
603
  const installed = installPlugins(name, slot?.plugins ?? []);
569
604
  const { plugins } = installed;
@@ -619,12 +654,15 @@ function checkApplicationName(name) {
619
654
  }
620
655
  return name;
621
656
  }
622
- /** Constructor inference preserves the installed plugin tuple; globals start empty. */
657
+ /**
658
+ * Constructor inference preserves the installed plugin tuple, and the globals start as the option
659
+ * values the installed plugins contribute.
660
+ */
623
661
  class ApplicationDeclaration extends ApplicationBuilder {
624
662
  constructor(name, options) {
625
663
  // The arguments evaluate in order, so the name is checked before any option is read.
626
664
  const checked = checkApplicationName(name);
627
- super(checked, declareApplication(checked, options));
665
+ super(checked, declaring(() => declareApplication(checked, options)));
628
666
  }
629
667
  }
630
668
  export const Application = ApplicationDeclaration;
@@ -0,0 +1,65 @@
1
+ import type { Finding } from './diagnostic-text.js';
2
+ import { DeclarationError } from './errors.js';
3
+ /**
4
+ * One declaring call's read of the object an author passed it, under way. It remembers the slot it
5
+ * is reading, so a read that throws names the slot it threw in, or no slot when the object itself
6
+ * threw.
7
+ */
8
+ declare class DeclarationRead {
9
+ /** The top-level key being read, or `undefined` while the object itself is read. */
10
+ slot: string | undefined;
11
+ /** The copy of every own string key of one object, each read under its own slot. */
12
+ record(declared: object): Record<string, unknown>;
13
+ /**
14
+ * Replaces the value one key of a copy holds with `copy` of it, read under that key's slot. An
15
+ * absent key stays absent, so a finding prints the copy with the keys the author wrote.
16
+ */
17
+ nested(captured: Record<string, unknown>, key: string, copy: (value: unknown) => unknown): void;
18
+ }
19
+ /** The two faults one declaring call raises when it cannot capture what it was given. */
20
+ interface CaptureFaults {
21
+ /** The value is not a plain object, so it has no keys for core to read. */
22
+ readonly notAnObject: () => DeclarationError;
23
+ /** A read threw the value it receives, in the top-level slot it names, or in the object itself. */
24
+ readonly unreadable: (thrown: unknown, slot: string | undefined) => DeclarationError;
25
+ }
26
+ /**
27
+ * Authoring's one read of an object a declaring call receives, inside one try: the verdict on its
28
+ * prototype, the copy of every own string key, and the copies `nested` takes of the parts it holds,
29
+ * each judged plain once by `decidePlain`. Every later check, stored value, and finding reads that
30
+ * copy and never the author's object again, so a getter runs once and a read that threw is never
31
+ * repeated. A value that is not a plain object is the not-an-object fault, and a read that throws,
32
+ * from a getter or a proxy trap, is the unreadable fault, named by the slot it threw in.
33
+ */
34
+ declare function captureDeclaration<Declared>(declared: Declared, nested: (copy: Record<string, unknown>, read: DeclarationRead) => void, faults: CaptureFaults): Declared & Record<string, unknown>;
35
+ /**
36
+ * What a finding prints for a declaration whose read threw: the slot it threw in, elided, under the
37
+ * keys that lead to it, or the whole declaration elided. A read that threw is never repeated.
38
+ */
39
+ declare function elidedRead(slot: string | undefined): {
40
+ keys: readonly string[];
41
+ shown: unknown;
42
+ };
43
+ /** What one unreadable declaration is: an input's config, a plugin's definition, or an options object. */
44
+ type Unreadable = 'config' | 'definition' | 'options';
45
+ /**
46
+ * The fault for a declaration whose read threw, named by `subject` and marked by `findings`. The
47
+ * thrown value's reason ends the sentence and the value itself is the fault's cause.
48
+ */
49
+ declare function unreadableFault(report: {
50
+ readonly declared: Unreadable;
51
+ readonly findings: readonly Finding[];
52
+ readonly subject: string;
53
+ }, thrown: unknown): DeclarationError;
54
+ /**
55
+ * The unreadable fault of the object a `plugin()` call or a constructor receives as its second
56
+ * argument. Its finding marks the top-level slot whose read threw, or the whole argument when the
57
+ * object itself threw, and prints that part elided.
58
+ */
59
+ declare function unreadableArgument(declaration: {
60
+ readonly call: string;
61
+ readonly named: unknown;
62
+ readonly subject: string;
63
+ }, declared: 'definition' | 'options'): (thrown: unknown, slot: string | undefined) => DeclarationError;
64
+ export type { CaptureFaults };
65
+ export { captureDeclaration, elidedRead, unreadableArgument, unreadableFault };
@@ -0,0 +1,99 @@
1
+ import { elided, spelled } from './diagnostic-text.js';
2
+ import { asSentence, DeclarationError, reasonOf } from './errors.js';
3
+ import { copyOwnKeys, decidePlain } from './plain.js';
4
+ import { unreadableDeclaration } from './plugin-rules.js';
5
+ /**
6
+ * One declaring call's read of the object an author passed it, under way. It remembers the slot it
7
+ * is reading, so a read that throws names the slot it threw in, or no slot when the object itself
8
+ * threw.
9
+ */
10
+ class DeclarationRead {
11
+ /** The top-level key being read, or `undefined` while the object itself is read. */
12
+ slot = undefined;
13
+ /** The copy of every own string key of one object, each read under its own slot. */
14
+ record(declared) {
15
+ const copy = copyOwnKeys(declared, (key) => {
16
+ this.slot = key;
17
+ });
18
+ this.slot = undefined;
19
+ return copy;
20
+ }
21
+ /**
22
+ * Replaces the value one key of a copy holds with `copy` of it, read under that key's slot. An
23
+ * absent key stays absent, so a finding prints the copy with the keys the author wrote.
24
+ */
25
+ nested(captured, key, copy) {
26
+ if (Object.hasOwn(captured, key)) {
27
+ this.slot = key;
28
+ captured[key] = copy(captured[key]);
29
+ this.slot = undefined;
30
+ }
31
+ }
32
+ }
33
+ /**
34
+ * Authoring's one read of an object a declaring call receives, inside one try: the verdict on its
35
+ * prototype, the copy of every own string key, and the copies `nested` takes of the parts it holds,
36
+ * each judged plain once by `decidePlain`. Every later check, stored value, and finding reads that
37
+ * copy and never the author's object again, so a getter runs once and a read that threw is never
38
+ * repeated. A value that is not a plain object is the not-an-object fault, and a read that throws,
39
+ * from a getter or a proxy trap, is the unreadable fault, named by the slot it threw in.
40
+ */
41
+ function captureDeclaration(declared, nested, faults) {
42
+ const read = new DeclarationRead();
43
+ let copy = undefined;
44
+ try {
45
+ if (decidePlain(declared)) {
46
+ copy = read.record(declared);
47
+ nested(copy, read);
48
+ }
49
+ }
50
+ catch (error) {
51
+ throw faults.unreadable(error, read.slot);
52
+ }
53
+ if (copy === undefined) {
54
+ throw faults.notAnObject();
55
+ }
56
+ // Last resort: no typed path exists.
57
+ // The copy is built key by key, so the compiler types it as a record of unknown values.
58
+ // A spread would keep the declared type, but it reads enumerable keys alone and names no slot.
59
+ // It holds because the copy holds every own string key of the declared plain object.
60
+ // Each holds the value read from it once, or that value's copy of the same kind.
61
+ // No declaration type declares a symbol key, and none reads its prototype.
62
+ // oxlint-disable-next-line typescript/no-unsafe-type-assertion
63
+ return copy;
64
+ }
65
+ /**
66
+ * What a finding prints for a declaration whose read threw: the slot it threw in, elided, under the
67
+ * keys that lead to it, or the whole declaration elided. A read that threw is never repeated.
68
+ */
69
+ function elidedRead(slot) {
70
+ return slot === undefined
71
+ ? { keys: [], shown: spelled(elided) }
72
+ : { keys: [slot], shown: Object.fromEntries([[slot, spelled(elided)]]) };
73
+ }
74
+ /**
75
+ * The fault for a declaration whose read threw, named by `subject` and marked by `findings`. The
76
+ * thrown value's reason ends the sentence and the value itself is the fault's cause.
77
+ */
78
+ function unreadableFault(report, thrown) {
79
+ const { declared, findings, subject } = report;
80
+ return new DeclarationError(unreadableDeclaration, {
81
+ correction: `Declare the ${declared} as a plain object literal whose properties read without throwing.`,
82
+ findings,
83
+ sentence: `${subject} ${declared} could not be read: ${asSentence(reasonOf(thrown))}`,
84
+ }, { cause: thrown });
85
+ }
86
+ /**
87
+ * The unreadable fault of the object a `plugin()` call or a constructor receives as its second
88
+ * argument. Its finding marks the top-level slot whose read threw, or the whole argument when the
89
+ * object itself threw, and prints that part elided.
90
+ */
91
+ function unreadableArgument(declaration, declared) {
92
+ const { call, named, subject } = declaration;
93
+ return (thrown, slot) => {
94
+ const { keys, shown } = elidedRead(slot);
95
+ const finding = { arguments: [named, shown], call, mark: ['1', ...keys].join('.') };
96
+ return unreadableFault({ declared, findings: [finding], subject }, thrown);
97
+ };
98
+ }
99
+ export { captureDeclaration, elidedRead, unreadableArgument, unreadableFault };
package/dist/chain.d.ts CHANGED
@@ -4,7 +4,7 @@ import type { CommandGraph, CommandNode } from './inspect.js';
4
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
- import type { DefaultValues } from './validation.js';
7
+ import type { InputPlaces, DeclaredValues } from './validation.js';
8
8
  /**
9
9
  * What the rest of one chain did: the action ran, a later middleware took over by returning without
10
10
  * calling its own `next()`, or the run was cancelled before the action ran.
@@ -12,17 +12,21 @@ import type { DefaultValues } from './validation.js';
12
12
  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
- * 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, 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.
15
+ * run, and `command` is the routed node inside it. `options` holds the validated value of every
16
+ * global option, the application's and every plugin's, keyed by declared name: the plugin's own
17
+ * options are typed from its declaration and every other key reads `unknown`, because a plugin
18
+ * compiles without the Application that installs it. It is `null` when a global option has a
19
+ * structural fault or was rejected, so a middleware never reads a global value no validator
20
+ * accepted, and a fault on a local option alone leaves it set. `spellings` holds the spelling that
21
+ * supplied each of the plugin's own options as a token, which a filled or defaulted option never
22
+ * has. `request` is the routed Command's invocation, parsed and validated ahead of the chain, and
23
+ * `null` while core holds a fault and on a group. `view` names the view the result renders
24
+ * through: it reads as the declaration's default until a middleware assigns one, and as `null` on
25
+ * a Command that declares none. The last assignment before the dispatch boundary wins, and one
26
+ * made after it changes nothing.
23
27
  */
24
28
  interface MiddlewareContext<Options extends PluginOptions = PluginOptions> {
25
- readonly options: PluginOptionValues<Options>;
29
+ readonly options: (PluginOptionValues<Options> & Readonly<Record<string, unknown>>) | null;
26
30
  readonly spellings: PluginOptionSpellings<Options>;
27
31
  readonly graph: CommandGraph;
28
32
  readonly command: CommandNode;
@@ -34,12 +38,14 @@ interface MiddlewareContext<Options extends PluginOptions = PluginOptions> {
34
38
  readonly signal: AbortSignal;
35
39
  readonly next: () => Promise<ChainOutcome>;
36
40
  }
37
- /** Everything one invocation needs after its graph is built and its defaults are validated. */
41
+ /**
42
+ * Everything one invocation needs after its graph is built and its defaults and implied values are
43
+ * validated.
44
+ */
38
45
  interface Invocation {
39
46
  style: ContextualStyle;
40
47
  /** The action's own channel, built from the routed Command's declaration when it dispatches. */
41
48
  channel: (binding: ResultBinding) => ActionChannel;
42
- defaults: DefaultValues;
43
49
  graph: BuiltGraph;
44
50
  host: Host;
45
51
  /**
@@ -60,7 +66,11 @@ interface Invocation {
60
66
  out: Out<OpenResult>;
61
67
  /** The channel a configuration source receives, whose results call names the source. */
62
68
  sourceOut: Out<OpenResult>;
69
+ /** Where every input of the graph was declared, which a broken validator's finding rebuilds. */
70
+ places: InputPlaces;
63
71
  plugins: readonly BuiltPlugin[];
72
+ /** Every declared default and implied value, validated before any token was read. */
73
+ declaredValues: DeclaredValues;
64
74
  /** A fault reported after the primary outcome, which turns a would-be 0 into 1. */
65
75
  report: (fault: LoomError) => void;
66
76
  /**
@@ -71,11 +81,12 @@ interface Invocation {
71
81
  signal: AbortSignal;
72
82
  }
73
83
  /**
74
- * Runs one invocation: the global pre-scan, routing, the dispatch this invocation prepares, and the
75
- * middleware chain it then runs. Local parsing and validation run ahead of the chain so that a
76
- * middleware reads the request, and the fault they find is held until the dispatch boundary. A
77
- * middleware that returns without calling `next()` has taken over, so the held fault is never
78
- * raised and nothing later in the chain runs.
84
+ * Runs one invocation: routing on the global options and a parent's own options, the routed
85
+ * Command's words read against its table, the dispatch this invocation prepares, and the
86
+ * middleware chain it then runs. Only an unknown Command is raised before the chain. Parsing and validation run ahead of the chain so
87
+ * that a middleware reads the request, and every other fault they find is held until the dispatch
88
+ * boundary. A middleware that returns without calling `next()` has taken over, so the held fault is
89
+ * never raised and nothing later in the chain runs.
79
90
  */
80
91
  declare function runInvocation(invocation: Invocation): Promise<void>;
81
92
  export type { ChainOutcome, Invocation, MiddlewareContext };
package/dist/chain.js CHANGED
@@ -1,8 +1,9 @@
1
- import { prepareDispatch, routeInvocation } from './command.js';
1
+ import { prepareDispatch } from './command.js';
2
2
  import { foreignFailure, InternalError, routedSubject } from './errors.js';
3
3
  import { nodeAt } from './inspect.js';
4
4
  import { isSupplied } from './options.js';
5
- import { loadDefault, pluginSentence, pluginSpellings, pluginValues } from './plugin.js';
5
+ import { parseInvocation } from './parse.js';
6
+ import { loadDefault, pluginSentence, pluginSpellings } from './plugin.js';
6
7
  import { nextMisuse, viewSelection, viewSelectionCorrection } from './rules.js';
7
8
  /**
8
9
  * The view one run selects, which is one value whichever middleware wrote it. The assignment is
@@ -96,7 +97,6 @@ function activatedEntries(plugins, values) {
96
97
  identity: installed.identity,
97
98
  // Activation proved the middleware exists, so the empty loader is never the one core calls.
98
99
  load: installed.middleware?.load ?? (() => undefined),
99
- options: pluginValues(installed.inputs, values),
100
100
  spellings: pluginSpellings(installed.inputs, values),
101
101
  }));
102
102
  }
@@ -276,7 +276,7 @@ async function runChain(invocation, routed, prepared) {
276
276
  graph,
277
277
  host: invocation.host,
278
278
  next,
279
- options: entry.options,
279
+ options: prepared.options,
280
280
  out: invocation.out,
281
281
  request: prepared.request,
282
282
  signal: invocation.signal,
@@ -312,14 +312,15 @@ async function runChain(invocation, routed, prepared) {
312
312
  return run.raised;
313
313
  }
314
314
  /**
315
- * Runs one invocation: the global pre-scan, routing, the dispatch this invocation prepares, and the
316
- * middleware chain it then runs. Local parsing and validation run ahead of the chain so that a
317
- * middleware reads the request, and the fault they find is held until the dispatch boundary. A
318
- * middleware that returns without calling `next()` has taken over, so the held fault is never
319
- * raised and nothing later in the chain runs.
315
+ * Runs one invocation: routing on the global options and a parent's own options, the routed
316
+ * Command's words read against its table, the dispatch this invocation prepares, and the
317
+ * middleware chain it then runs. Only an unknown Command is raised before the chain. Parsing and validation run ahead of the chain so
318
+ * that a middleware reads the request, and every other fault they find is held until the dispatch
319
+ * boundary. A middleware that returns without calling `next()` has taken over, so the held fault is
320
+ * never raised and nothing later in the chain runs.
320
321
  */
321
322
  async function runInvocation(invocation) {
322
- const routed = routeInvocation(invocation.graph, invocation.host.argv, invocation.route);
323
+ const routed = parseInvocation(invocation.graph, invocation.host.argv, invocation.route);
323
324
  const prepared = await prepareDispatch(invocation.graph, routed, invocation);
324
325
  let raised = undefined;
325
326
  try {
@@ -18,7 +18,7 @@ declare const variadicArgumentLast: import("./diagnostic-text.js").DiagnosticRul
18
18
  declare const optionalArgumentLast: import("./diagnostic-text.js").DiagnosticRule;
19
19
  /** An `alias()` call that names no alias. */
20
20
  declare const aliasWithoutNames: import("./diagnostic-text.js").DiagnosticRule;
21
- /** An alias that repeats its own Command's name or another of its aliases. */
21
+ /** An alias that repeats its own Command's or option's name or another of its aliases. */
22
22
  declare const repeatedAlias: import("./diagnostic-text.js").DiagnosticRule;
23
23
  /** A child whose name or alias repeats a sibling's name or alias. */
24
24
  declare const siblingNameTaken: import("./diagnostic-text.js").DiagnosticRule;
@@ -54,9 +54,9 @@ const aliasWithoutNames = registerRule('@loomcli/core/alias-without-names', {
54
54
  explanation: 'alias() adds each name it receives to the names that route to its Command, so a call with none adds nothing.',
55
55
  headline: 'Alias with no names',
56
56
  });
57
- /** An alias that repeats its own Command's name or another of its aliases. */
57
+ /** An alias that repeats its own Command's or option's name or another of its aliases. */
58
58
  const repeatedAlias = registerRule('@loomcli/core/repeated-alias', {
59
- explanation: 'A Command answers to its name and to each of its aliases, so an alias that repeats one of them routes nothing new.',
59
+ explanation: 'A Command or an option answers to its name and to each of its aliases, so an alias that repeats one of them adds nothing new.',
60
60
  headline: 'Alias repeats a name',
61
61
  });
62
62
  /** A child whose name or alias repeats a sibling's name or alias. */