@loomcli/core 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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.
@@ -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,7 +14,7 @@ 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';
@@ -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
  });
@@ -481,7 +489,10 @@ function runRendering(rendering) {
481
489
  subject: 'The run',
482
490
  };
483
491
  }
484
- /** Reject obsolete wiring before silently losing options that invocations depend on. */
492
+ /**
493
+ * Reject obsolete wiring before silently losing options that invocations depend on. `options` is the
494
+ * copy `captureOptions` took, or `undefined` for an Application declared without options.
495
+ */
485
496
  function checkOptions(name, options) {
486
497
  const site = {
487
498
  at: '1',
@@ -491,13 +502,6 @@ function checkOptions(name, options) {
491
502
  if (options === undefined) {
492
503
  return { description: undefined, version: checkVersion(site, undefined) };
493
504
  }
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
505
  for (const [key, correction] of retired) {
502
506
  if (key in options) {
503
507
  const retiredSite = optionSite(name, key, Reflect.get(options, key));
@@ -546,15 +550,43 @@ function readPacket(name, packet) {
546
550
  sentence: found,
547
551
  });
548
552
  }
553
+ /** The parts of the Application options that are lists, each copied with its entries as built. */
554
+ const optionLists = ['plugins', 'views', 'translators', 'extensions'];
555
+ /**
556
+ * The one copy of the options that `new Application(name, options)` reads: the options object, its
557
+ * packet and rendering policy, and each list it holds. An entry of a list is what its factory built
558
+ * and is not copied. A read that throws is the unreadable fault of the options, and a value that is
559
+ * not a plain object is the not-an-object fault. An Application declared without options has none
560
+ * to copy.
561
+ */
562
+ function captureOptions(name, options) {
563
+ if (options === undefined) {
564
+ return undefined;
565
+ }
566
+ return captureDeclaration(options, (copy, read) => {
567
+ read.nested(copy, 'packet', shallowRecord);
568
+ read.nested(copy, 'rendering', shallowRecord);
569
+ for (const list of optionLists) {
570
+ read.nested(copy, list, shallowList);
571
+ }
572
+ }, {
573
+ notAnObject: () => new DeclarationError(notAnObject, {
574
+ correction: 'Supply an Application options object.',
575
+ findings: [{ arguments: [name, options], call: 'new Application', mark: '1' }],
576
+ sentence: 'The Application declares options that are not an object.',
577
+ }),
578
+ unreadable: unreadableArgument({ call: 'new Application', named: name, subject: 'The Application' }, 'options'),
579
+ });
580
+ }
549
581
  /**
550
582
  * Every rule `new Application(name, options)` applies, in the order it reads the slot: the
551
583
  * application's own view overrides, its translations, the rendering policy, the options slot and
552
584
  * its facts, the installed list and every rule between two plugins, the root's extension values,
553
585
  * and then each plugin's Commands, which attach to the root first, in installation order and list
554
- * order.
586
+ * order. Each reads the one copy of the options `captureOptions` takes.
555
587
  */
556
- function declareApplication(name, options) {
557
- const slot = isPlainObject(options) ? options : undefined;
588
+ function declareApplication(name, declared) {
589
+ const slot = captureOptions(name, declared);
558
590
  const identities = viewIdentities(coreViews);
559
591
  const views = buildViews({
560
592
  declares: false,
@@ -563,7 +595,7 @@ function declareApplication(name, options) {
563
595
  }, slot?.views, identities);
564
596
  const translations = readTranslations(optionSite(name, 'translators', slot?.translators), slot?.translators);
565
597
  const rendering = renderingPolicy(slot?.rendering, optionSite(name, 'rendering', slot?.rendering));
566
- const facts = checkOptions(name, options);
598
+ const facts = checkOptions(name, slot);
567
599
  const development = readPacket(name, slot?.packet);
568
600
  const installed = installPlugins(name, slot?.plugins ?? []);
569
601
  const { plugins } = installed;
@@ -624,7 +656,7 @@ class ApplicationDeclaration extends ApplicationBuilder {
624
656
  constructor(name, options) {
625
657
  // The arguments evaluate in order, so the name is checked before any option is read.
626
658
  const checked = checkApplicationName(name);
627
- super(checked, declareApplication(checked, options));
659
+ super(checked, declaring(() => declareApplication(checked, options)));
628
660
  }
629
661
  }
630
662
  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/command.js CHANGED
@@ -1,18 +1,19 @@
1
1
  var _a, _b;
2
2
  import { checkEnvBinding, checkNoArgumentBinding } from './bindings.js';
3
+ import { captureDeclaration, unreadableArgument } from './capture.js';
3
4
  import { aliasWithoutNames, argumentDeclaredTwice, argumentsBesideChildren, commandAttachedTwice, commandGlobals, commandWithoutAction, declaredAfterAction, declaredName, groupOption, multipleActions, multipleResults, nestingDepth, notACommand, optionalArgumentLast, portableName, repeatedAlias, resultWithoutAction, resultWithoutViews, rowViewOnValue, siblingNameTaken, unknownDefaultView, variadicArgumentLast, viewName, viewShape, viewsWithoutResult, } from './command-rules.js';
4
- import { quoteString, spelled } from './diagnostic-text.js';
5
+ import { elided, quoteString, spelled } from './diagnostic-text.js';
5
6
  import { asSentence, commandSentence, commandSubject, DeclarationError, NonCallableCommandError, quoted, ResultError, toFailure, UnexpectedArgumentError, UnknownCommandError, reasonOf, } from './errors.js';
6
7
  import { buildExtensions, extendStore, publishStore, registerDescriptor, storeCommandLayers, validateLayer, } from './extension.js';
7
- import { checkDeprecated, checkDescription, checkHidden, checkNoListingFacts, siteFinding, } from './facts.js';
8
+ import { checkDeprecated, checkDescription, checkHidden, checkNoListingFacts, declarerNote, siteFinding, } from './facts.js';
8
9
  import { buildGlobals, checkLocalOptions } from './globals.js';
9
10
  import { nameSharedAcrossKinds, optionDeclaredTwice, spellingTaken } from './input-rules.js';
10
- import { graphMismatch, nodeAt, resultNode, snapshot } from './inspect.js';
11
+ import { graphMismatch, nodeAt, resultNode } from './inspect.js';
11
12
  import { checkOptionName, compileOptions, copyValues, emptyValues, extractGlobals, isOptionToken, mergeValues, parseInputs, spellingMark, } from './options.js';
12
- import { isPlainObject } from './plain.js';
13
+ import { declaring, isPlainObject, shallowList, snapshot } from './plain.js';
13
14
  import { brokenAttachHook, notAnObject } from './plugin-rules.js';
14
15
  import { fillInputs } from './sources.js';
15
- import { captureConfig, checkInputConfig, checkDeclarations, declaringSite, inputPlace, validateValues, } from './validation.js';
16
+ import { captureInputConfig, configUnread, checkDeclarations, declaringSite, inputPlace, validateValues, } from './validation.js';
16
17
  /**
17
18
  * The deepest level below the root a Command may sit at, and the word its diagnostic spells it with.
18
19
  * A child of the root sits at level 1. Raising the cap relaxes a rule and breaks no application.
@@ -73,16 +74,30 @@ export function commandPlacement(path, child) {
73
74
  function constructorFinding(name, options, mark) {
74
75
  return { arguments: callArguments(name, options), call: 'new Command', mark };
75
76
  }
76
- /** A named Command's options slot holds a plain options object, and never retired globals wiring. */
77
- function checkCommandOptions(name, options) {
78
- if (options !== undefined && !isPlainObject(options)) {
79
- throw new DeclarationError(notAnObject, {
77
+ /**
78
+ * The one copy of the options that `new Command(name, options)` reads, taken once the name is
79
+ * judged: the options object and its `extensions` list, whose entries are what their factory built
80
+ * and are not copied. A read that throws is the unreadable fault of the options, a value that is not
81
+ * a plain object is the not-an-object fault, and a Command declared without options has none.
82
+ */
83
+ function captureCommandOptions(name, options) {
84
+ if (options === undefined) {
85
+ return undefined;
86
+ }
87
+ return captureDeclaration(options, (copy, read) => {
88
+ read.nested(copy, 'extensions', shallowList);
89
+ }, {
90
+ notAnObject: () => new DeclarationError(notAnObject, {
80
91
  correction: 'Supply a Command options object.',
81
92
  findings: [constructorFinding(name, options, '1')],
82
93
  sentence: `${commandSentence(name)} declares options that are not an object.`,
83
- });
84
- }
85
- if (isPlainObject(options) && 'globals' in options) {
94
+ }),
95
+ unreadable: unreadableArgument({ call: 'new Command', named: name, subject: commandSentence(name) }, 'options'),
96
+ });
97
+ }
98
+ /** A named Command's options never carry the retired globals wiring. */
99
+ function checkCommandOptions(name, options) {
100
+ if ('globals' in options) {
86
101
  throw new DeclarationError(commandGlobals, {
87
102
  correction: 'Declare globals on the Application and register its environment.',
88
103
  findings: [constructorFinding(name, options, '1.globals')],
@@ -149,23 +164,28 @@ export function freshState(declaration) {
149
164
  };
150
165
  }
151
166
  /**
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.
167
+ * A named Command's own declaration, checked before the value exists: the name, then the one copy
168
+ * of the options slot, its core facts, and the extension values it carries. A later change to the
169
+ * options object the author passed changes nothing the declaration holds.
155
170
  */
156
171
  function namedState(name, options) {
157
172
  if (!isPortableName(name)) {
173
+ // The options are read after the name is judged, so the name's finding prints them elided.
158
174
  throw new DeclarationError(portableName, {
159
175
  correction: portableNameCorrection,
160
- findings: [constructorFinding(name, options, '0')],
176
+ findings: [
177
+ constructorFinding(name, options === undefined ? undefined : spelled(elided), '0'),
178
+ ],
161
179
  sentence: `Command name ${quoted(name)} is invalid.`,
162
180
  });
163
181
  }
164
- checkCommandOptions(name, options);
165
- const slot = isPlainObject(options) ? options : undefined;
182
+ const slot = captureCommandOptions(name, options);
183
+ if (slot !== undefined) {
184
+ checkCommandOptions(name, slot);
185
+ }
166
186
  const site = {
167
187
  at: '1',
168
- declaration: { arguments: [name, options], call: 'new Command' },
188
+ declaration: { arguments: [name, slot], call: 'new Command' },
169
189
  subject: commandSentence(name),
170
190
  };
171
191
  // The facts are read in the order their diagnostics have always ranked.
@@ -221,6 +241,14 @@ function inputFinding(path, input, note) {
221
241
  const site = declaringSite(input, inputPlace(input, { global: false, path }));
222
242
  return siteFinding(site, site.named, note);
223
243
  }
244
+ /**
245
+ * The finding for an input whose name is invalid, marking the name. The config is read after the
246
+ * name is judged, so it prints elided.
247
+ */
248
+ function nameFinding(path, input) {
249
+ const site = configUnread(declaringSite(input, inputPlace(input, { global: false, path })));
250
+ return siteFinding(site, site.named);
251
+ }
224
252
  /** The scope one Command's own options compile under: its subject, and each option's call. */
225
253
  function localScope(name, path) {
226
254
  return { siteOf: (input) => inputSite(name, path, input), subject: commandSubject(name) };
@@ -297,7 +325,7 @@ function checkArgumentName(command, declared, input) {
297
325
  if (!isDeclaredName(input.name)) {
298
326
  throw new DeclarationError(declaredName, {
299
327
  correction: 'Use a nonempty name without a leading hyphen, whitespace, or "=".',
300
- findings: [inputFinding(path, input)],
328
+ findings: [nameFinding(path, input)],
301
329
  sentence: `${commandSentence(command.name)} declares an argument named ${quoted(input.name)}.`,
302
330
  });
303
331
  }
@@ -317,11 +345,13 @@ function checkArgumentName(command, declared, input) {
317
345
  export function declareArgument(state, declared) {
318
346
  // The call's own input is judged before the receiver's state, as alias() judges its names.
319
347
  // 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.
348
+ // The config is read once right after its name is judged, and every later check reads that copy.
321
349
  const { name } = state;
322
350
  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) };
351
+ const input = {
352
+ ...declared,
353
+ config: captureInputConfig(declared, { call: 'argument', path: pathOf(name) }),
354
+ };
325
355
  checkOpen(state, {
326
356
  arguments: [input.name, input.config],
327
357
  call: 'argument',
@@ -350,11 +380,14 @@ const noGlobals = { names: new Map(), options: new Map(), variables: new Map() }
350
380
  */
351
381
  export function declareOption(state, declared, table = noGlobals) {
352
382
  // 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);
383
+ // The config is read once right after its name is judged, and every later check reads that copy.
384
+ const path = pathOf(state.name);
385
+ checkOptionName(declared.name, configUnread(inputSite(state.name, path, declared)));
386
+ const input = {
387
+ ...declared,
388
+ config: captureInputConfig(declared, { call: 'option', path }),
389
+ };
390
+ const site = inputSite(state.name, path, input);
358
391
  checkOpen(state, {
359
392
  arguments: [input.name, input.config],
360
393
  call: 'option',
@@ -632,7 +665,7 @@ function checkGroup(state, place) {
632
665
  throw new DeclarationError(groupOption, {
633
666
  correction: 'Register an action or remove the option.',
634
667
  findings: [inputFinding(place.path, option, 'no action reads it')],
635
- sentence: `${commandSentence(name)} declares option "${option.name}" but registers no action to receive it.`,
668
+ sentence: `${commandSentence(name)} declares option ${quoted(option.name)} but registers no action to receive it.`,
636
669
  });
637
670
  }
638
671
  }
@@ -1045,13 +1078,13 @@ class AttachedCommandValue {
1045
1078
  checkArgumentName({ name, path, subject: commandSubject(name) }, [], raw);
1046
1079
  }
1047
1080
  else {
1048
- checkOptionName(raw.name, inputSite(declared.name, path, raw));
1081
+ checkOptionName(raw.name, configUnread(inputSite(declared.name, path, raw)));
1049
1082
  }
1050
- checkInputConfig(raw, { call: raw.kind, path });
1051
1083
  // Each kind captures its own config, so the input keeps the pairing its kind declares.
1084
+ const place = { call: raw.kind, path };
1052
1085
  const input = raw.kind === 'argument'
1053
- ? { ...raw, config: captureConfig(raw.config) }
1054
- : { ...raw, config: captureConfig(raw.config) };
1086
+ ? { ...raw, config: captureInputConfig(raw, place) }
1087
+ : { ...raw, config: captureInputConfig(raw, place) };
1055
1088
  return this.#derive({ identity, input, kind: 'input' }, { ...declared, inputs: [...declared.inputs, input] });
1056
1089
  }
1057
1090
  #derive(call, declared) {
@@ -1166,16 +1199,12 @@ function nameCollisionRule(declared, held) {
1166
1199
  }
1167
1200
  return declared === 'option' ? optionDeclaredTwice : argumentDeclaredTwice;
1168
1201
  }
1169
- /** The note a finding for one hook-declared input carries. */
1170
- function hookNote(identity) {
1171
- return `declared by plugin ${quoted(identity)}`;
1172
- }
1173
1202
  /** The clause and the remedy an input another plugin's hook already declared earns. */
1174
1203
  function hookClause(earlier, path) {
1175
1204
  const { identity, input } = earlier;
1176
1205
  return {
1177
1206
  clause: `an ${input.kind} plugin ${quoted(identity)} declared through onCommandAttach`,
1178
- held: inputFinding(path, input, hookNote(identity)),
1207
+ held: inputFinding(path, input, declarerNote(identity)),
1179
1208
  kind: input.kind,
1180
1209
  remedy: 'Install one of them.',
1181
1210
  };
@@ -1285,14 +1314,14 @@ function checkAttachedSpelling(declared, named) {
1285
1314
  throw new DeclarationError(spellingTaken, {
1286
1315
  correction: 'Change one of the two spellings or omit the plugin.',
1287
1316
  findings: [
1288
- spellingPlace(site, option.role, hookNote(identity)),
1317
+ spellingPlace(site, option.role, declarerNote(identity)),
1289
1318
  ...(used.finding === undefined ? [] : [used.finding]),
1290
1319
  ],
1291
1320
  sentence: `Plugin ${quoted(identity)} declares option ${quoted(input.name)} with spelling ${quoted(spelling)} on ${subject}, which ${quoted(used.form)} already uses.`,
1292
1321
  });
1293
1322
  }
1294
1323
  }
1295
- readSpellings(table, claimed, (_name, role) => spellingPlace(site, role, hookNote(identity)));
1324
+ readSpellings(table, claimed, (_name, role) => spellingPlace(site, role, declarerNote(identity)));
1296
1325
  }
1297
1326
  /** The declarations of one kind, keyed by name, the first of each name winning. */
1298
1327
  function byName(inputs) {
@@ -1314,7 +1343,7 @@ function tableSpellings(globals) {
1314
1343
  const { owner, site } = entry;
1315
1344
  const note = owner.kind === 'plugin'
1316
1345
  ? `an option of plugin ${quoted(owner.identity)}`
1317
- : `the global option "${name}"`;
1346
+ : `the global option ${quoted(name)}`;
1318
1347
  return spellingPlace(site, role, note);
1319
1348
  };
1320
1349
  }
@@ -1346,7 +1375,7 @@ function checkAttachedInputs(declared, attached, place) {
1346
1375
  const local = locals.get(name);
1347
1376
  return local === undefined || local.kind !== 'option'
1348
1377
  ? undefined
1349
- : spellingPlace(scope.siteOf(local), role, `the local option "${name}"`);
1378
+ : spellingPlace(scope.siteOf(local), role, `the local option ${quoted(name)}`);
1350
1379
  });
1351
1380
  for (const entry of attached) {
1352
1381
  const { identity, input } = entry;
@@ -1354,7 +1383,7 @@ function checkAttachedInputs(declared, attached, place) {
1354
1383
  if (collision) {
1355
1384
  throw new DeclarationError(nameCollisionRule(input.kind, collision.kind), {
1356
1385
  correction: collision.remedy,
1357
- findings: [inputFinding(path, input, hookNote(identity)), collision.held],
1386
+ findings: [inputFinding(path, input, declarerNote(identity)), collision.held],
1358
1387
  sentence: `Plugin ${quoted(identity)} declares ${input.kind} ${quoted(input.name)} on ${subject}, which is already declared as ${collision.clause}.`,
1359
1388
  });
1360
1389
  }
@@ -1594,7 +1623,7 @@ _b = CommandBuilder;
1594
1623
  */
1595
1624
  class CommandDeclaration extends CommandBuilder {
1596
1625
  constructor(name, options) {
1597
- const declared = namedState(name, options);
1626
+ const declared = declaring(() => namedState(name, options));
1598
1627
  super(declared.name, declared.state);
1599
1628
  }
1600
1629
  }
package/dist/facts.d.ts CHANGED
@@ -9,6 +9,11 @@ export interface FactSite {
9
9
  readonly declaration: Omit<Finding, 'mark' | 'note'>;
10
10
  readonly at: string;
11
11
  }
12
+ /**
13
+ * The note a finding carries for what a plugin declared on the author's behalf, such as an input
14
+ * its hook declares or the settings its factory takes, so the diagnostic names the declarer.
15
+ */
16
+ export declare function declarerNote(identity: string): string;
12
17
  /** The finding for the call one site holds, marking one part of it, with a note when given. */
13
18
  export declare function siteFinding(site: FactSite, mark: string, note?: string): Finding;
14
19
  /**
package/dist/facts.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { misplacedListingFact, notOneLine } from './command-rules.js';
2
- import { DeclarationError } from './errors.js';
2
+ import { DeclarationError, quoted } from './errors.js';
3
3
  import { flagNotBoolean } from './input-rules.js';
4
4
  /**
5
5
  * One character outside Unicode `White_Space`, so a fact holds prose and not only spacing.
@@ -12,6 +12,13 @@ const prose = /\P{White_Space}/u;
12
12
  * The seven are LF, VT, FF, CR, NEL, LS, and PS, each of them `White_Space` too.
13
13
  */
14
14
  const lineTerminator = /[\n\v\f\r\u0085\u2028\u2029]/u;
15
+ /**
16
+ * The note a finding carries for what a plugin declared on the author's behalf, such as an input
17
+ * its hook declares or the settings its factory takes, so the diagnostic names the declarer.
18
+ */
19
+ export function declarerNote(identity) {
20
+ return `declared by plugin ${quoted(identity)}`;
21
+ }
15
22
  /** The finding for the call one site holds, marking one part of it, with a note when given. */
16
23
  export function siteFinding(site, mark, note) {
17
24
  const finding = { ...site.declaration, mark };