@loomcli/core 0.5.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. package/NOTICE +34 -0
  2. package/dist/application.d.ts +18 -2
  3. package/dist/application.js +302 -75
  4. package/dist/bindings.d.ts +15 -10
  5. package/dist/bindings.js +34 -15
  6. package/dist/capture.d.ts +65 -0
  7. package/dist/capture.js +99 -0
  8. package/dist/chain.d.ts +15 -6
  9. package/dist/chain.js +40 -20
  10. package/dist/command-rules.d.ts +55 -0
  11. package/dist/command-rules.js +142 -0
  12. package/dist/command.d.ts +65 -23
  13. package/dist/command.js +734 -236
  14. package/dist/controls.d.ts +8 -0
  15. package/dist/controls.js +23 -0
  16. package/dist/defect.d.ts +18 -0
  17. package/dist/defect.js +272 -0
  18. package/dist/developer.d.ts +24 -0
  19. package/dist/developer.js +52 -0
  20. package/dist/diagnostic-text.d.ts +81 -0
  21. package/dist/diagnostic-text.js +283 -0
  22. package/dist/diagnostic.d.ts +11 -0
  23. package/dist/diagnostic.js +70 -0
  24. package/dist/errors.d.ts +108 -24
  25. package/dist/errors.js +324 -53
  26. package/dist/exit-codes.d.ts +45 -0
  27. package/dist/exit-codes.js +46 -0
  28. package/dist/extension.d.ts +41 -8
  29. package/dist/extension.js +142 -57
  30. package/dist/facts.d.ts +71 -11
  31. package/dist/facts.js +106 -23
  32. package/dist/globals.d.ts +29 -21
  33. package/dist/globals.js +117 -46
  34. package/dist/glyphs.generated.js +1 -1
  35. package/dist/hints.d.ts +79 -0
  36. package/dist/hints.js +247 -0
  37. package/dist/host.d.ts +13 -0
  38. package/dist/host.js +43 -1
  39. package/dist/identity.d.ts +19 -0
  40. package/dist/identity.js +72 -0
  41. package/dist/index.d.ts +12 -2
  42. package/dist/index.js +6 -0
  43. package/dist/input-rules.d.ts +68 -0
  44. package/dist/input-rules.js +152 -0
  45. package/dist/inspect.d.ts +12 -12
  46. package/dist/inspect.js +92 -48
  47. package/dist/lanes.js +1 -1
  48. package/dist/locate.js +4 -4
  49. package/dist/options.d.ts +30 -2
  50. package/dist/options.js +143 -46
  51. package/dist/output.d.ts +9 -2
  52. package/dist/output.js +18 -2
  53. package/dist/plain.d.ts +56 -0
  54. package/dist/plain.js +238 -0
  55. package/dist/plugin-rules.d.ts +68 -0
  56. package/dist/plugin-rules.js +164 -0
  57. package/dist/plugin-settings.d.ts +18 -0
  58. package/dist/plugin-settings.js +38 -0
  59. package/dist/plugin.d.ts +27 -15
  60. package/dist/plugin.js +429 -144
  61. package/dist/prototypes.d.ts +7 -0
  62. package/dist/prototypes.js +29 -0
  63. package/dist/rendering.d.ts +6 -1
  64. package/dist/rendering.js +23 -5
  65. package/dist/rules.d.ts +51 -0
  66. package/dist/rules.js +115 -0
  67. package/dist/sequence.js +6 -1
  68. package/dist/sources.d.ts +6 -4
  69. package/dist/sources.js +25 -16
  70. package/dist/style-layout.js +2 -2
  71. package/dist/style-width.d.ts +13 -0
  72. package/dist/style-width.js +170 -0
  73. package/dist/style-wire.js +1 -1
  74. package/dist/style.js +1 -1
  75. package/dist/theme.d.ts +4 -0
  76. package/dist/theme.js +25 -5
  77. package/dist/thenable.d.ts +15 -0
  78. package/dist/thenable.js +29 -0
  79. package/dist/translators.d.ts +69 -0
  80. package/dist/translators.js +253 -0
  81. package/dist/types.d.ts +13 -3
  82. package/dist/unicode.generated.d.ts +27 -0
  83. package/dist/unicode.generated.js +1036 -0
  84. package/dist/validation.d.ts +69 -7
  85. package/dist/validation.js +211 -67
  86. package/dist/view.d.ts +61 -22
  87. package/dist/view.js +168 -81
  88. package/licenses/unicode-LICENSE.txt +41 -0
  89. package/licenses/uucode-LICENSE.md +35 -0
  90. package/package.json +9 -5
package/dist/facts.js CHANGED
@@ -1,4 +1,6 @@
1
- import { DeclarationError } from './errors.js';
1
+ import { misplacedListingFact, notOneLine } from './command-rules.js';
2
+ import { DeclarationError, quoted } from './errors.js';
3
+ import { flagNotBoolean } from './input-rules.js';
2
4
  /**
3
5
  * One character outside Unicode `White_Space`, so a fact holds prose and not only spacing.
4
6
  * The class covers the tab, the space, the line terminators, the no-break space, and every other
@@ -10,6 +12,83 @@ const prose = /\P{White_Space}/u;
10
12
  * The seven are LF, VT, FF, CR, NEL, LS, and PS, each of them `White_Space` too.
11
13
  */
12
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
+ }
22
+ /** The finding for the call one site holds, marking one part of it, with a note when given. */
23
+ export function siteFinding(site, mark, note) {
24
+ const finding = { ...site.declaration, mark };
25
+ return note === undefined ? finding : { ...finding, note };
26
+ }
27
+ /**
28
+ * Where one key of a declaration's options object sits, rebuilt with that key alone, as
29
+ * `plugin(identity, { middleware })` or `new Application(name, { plugins })`, at `1.<key>`. A fault
30
+ * about one slot shows the slot, not every other one the author declared beside it.
31
+ */
32
+ export function slotSite(declaration, key, value) {
33
+ const { call, named, subject } = declaration;
34
+ // `Object.fromEntries` defines the key as an own property whatever its name.
35
+ return {
36
+ at: `1.${key}`,
37
+ declaration: { arguments: [named, Object.fromEntries([[key, value]])], call },
38
+ subject,
39
+ };
40
+ }
41
+ /**
42
+ * The dotted path to one part inside the value a site holds, such as `1.middleware.activate` for
43
+ * `activate` under a site at `1.middleware`. A site at the call's own arguments has an empty path.
44
+ */
45
+ export function partOf(site, ...keys) {
46
+ return [site.at, ...keys.map(String)].filter((key) => key !== '').join('.');
47
+ }
48
+ /** The finding that marks one part inside the value a site holds, with a note when given. */
49
+ export function partFinding(site, keys, note) {
50
+ return siteFinding(site, partOf(site, ...keys), note);
51
+ }
52
+ /**
53
+ * One fact's fault, which marks the fact inside the call that declared it. Every rule about one key
54
+ * of a declaration's config object reports through it, so each marks the key the same way.
55
+ */
56
+ export function factFault(rule, site, parts) {
57
+ const { correction, fact, sentence } = parts;
58
+ return new DeclarationError(rule, {
59
+ correction,
60
+ findings: [siteFinding(site, `${site.at}.${fact}`)],
61
+ sentence,
62
+ });
63
+ }
64
+ /**
65
+ * One yes-or-no declaration key that holds a value other than a Boolean, such as `required` or
66
+ * `hidden`. Every such key reports under one rule, in one sentence and with one correction.
67
+ */
68
+ export function flagFault(site, flag) {
69
+ return factFault(flagNotBoolean, site, {
70
+ correction: 'Use true or false.',
71
+ fact: flag,
72
+ sentence: `${site.subject} declares ${flag} that is not a Boolean.`,
73
+ });
74
+ }
75
+ /** The site of an input a call declared as `call(name, config)`, such as `option()`. */
76
+ export function callSite(subject, declaration) {
77
+ return { at: '1', declaration, named: '0', subject };
78
+ }
79
+ /**
80
+ * The site of one option a plugin declared, rebuilt as `plugin(identity, { options })` from the
81
+ * options the plugin holds. The option's entry in the record stands for its name.
82
+ */
83
+ export function pluginOptionSite(plugin, name, subject) {
84
+ const at = `1.options.${name}`;
85
+ return {
86
+ at,
87
+ declaration: { arguments: [plugin.identity, { options: plugin.options }], call: 'plugin' },
88
+ named: at,
89
+ subject,
90
+ };
91
+ }
13
92
  /**
14
93
  * One line of prose: a string that holds a character other than whitespace and no line terminator.
15
94
  * Every one-line fact reads this rule, and so does the label a configuration source answers with.
@@ -22,12 +101,16 @@ export function isProseLine(value) {
22
101
  * string fails the same way a blank one does, because the author reads one rule for one fact.
23
102
  * An omitted description is absent, not a fault, so it passes through as `undefined`.
24
103
  */
25
- export function checkDescription(subject, value) {
104
+ export function checkDescription(site, value) {
26
105
  if (value === undefined) {
27
106
  return undefined;
28
107
  }
29
108
  if (!isProseLine(value)) {
30
- throw new DeclarationError(`${subject} description must hold a character other than whitespace and no line terminator. Supply a one-line summary.`);
109
+ throw factFault(notOneLine, site, {
110
+ correction: 'Supply a one-line summary.',
111
+ fact: 'description',
112
+ sentence: `${site.subject} description must hold a character other than whitespace and no line terminator.`,
113
+ });
31
114
  }
32
115
  return value;
33
116
  }
@@ -36,12 +119,12 @@ export function checkDescription(subject, value) {
36
119
  * asks one question of it, and an omitted declaration reads `false`. Routing selects and parsing
37
120
  * binds without reading it, so a hidden member behaves as any other.
38
121
  */
39
- export function checkHidden(subject, value) {
122
+ export function checkHidden(site, value) {
40
123
  if (value === undefined) {
41
124
  return false;
42
125
  }
43
126
  if (typeof value !== 'boolean') {
44
- throw new DeclarationError(`${subject} hidden must be a Boolean. Supply true or false, or omit it.`);
127
+ throw flagFault(site, 'hidden');
45
128
  }
46
129
  return value;
47
130
  }
@@ -51,12 +134,16 @@ export function checkHidden(subject, value) {
51
134
  * prints. A bare `true` is rejected with every other value that is not prose: a deprecation with
52
135
  * no migration path leaves an operator or an agent with nothing to do.
53
136
  */
54
- export function checkDeprecated(subject, value) {
137
+ export function checkDeprecated(site, value) {
55
138
  if (value === undefined) {
56
139
  return undefined;
57
140
  }
58
141
  if (!isProseLine(value)) {
59
- throw new DeclarationError(`${subject} deprecated message must hold a character other than whitespace and no line terminator. Supply a one-line migration path, such as "Use get instead.".`);
142
+ throw factFault(notOneLine, site, {
143
+ correction: 'Supply a one-line migration path, such as "Use get instead.".',
144
+ fact: 'deprecated',
145
+ sentence: `${site.subject} deprecated message must hold a character other than whitespace and no line terminator.`,
146
+ });
60
147
  }
61
148
  return value;
62
149
  }
@@ -66,10 +153,14 @@ export function checkDeprecated(subject, value) {
66
153
  * a JavaScript author, or a TypeScript author whose argument config is inferred from a value,
67
154
  * reaches this rule instead.
68
155
  */
69
- export function checkNoListingFacts(subject, declared) {
156
+ export function checkNoListingFacts(site, declared) {
70
157
  for (const fact of ['hidden', 'deprecated']) {
71
158
  if (fact in declared) {
72
- throw new DeclarationError(`${subject} declares ${fact}, which applies to named Commands and options alone. Remove it.`);
159
+ throw factFault(misplacedListingFact, site, {
160
+ correction: 'Remove it.',
161
+ fact,
162
+ sentence: `${site.subject} declares ${fact}, which applies to named Commands and options alone.`,
163
+ });
73
164
  }
74
165
  }
75
166
  }
@@ -79,24 +170,16 @@ export function checkNoListingFacts(subject, declared) {
79
170
  * omitted version is `0.0.0`, which means unversioned, and core keeps no record of which one the
80
171
  * author wrote.
81
172
  */
82
- export function checkVersion(value) {
173
+ export function checkVersion(site, value) {
83
174
  if (value === undefined) {
84
175
  return '0.0.0';
85
176
  }
86
177
  if (!isProseLine(value)) {
87
- throw new DeclarationError('The Application version must be a string that holds a character other than whitespace and no line terminator. Supply a string such as "1.2.0".');
178
+ throw factFault(notOneLine, site, {
179
+ correction: 'Supply a string such as "1.2.0".',
180
+ fact: 'version',
181
+ sentence: `${site.subject} version must be a string that holds a character other than whitespace and no line terminator.`,
182
+ });
88
183
  }
89
184
  return value;
90
185
  }
91
- /**
92
- * A structural value core reads as plain data: an object literal, and never a declaration that
93
- * carries state of its own. The options slots read it to reject a value that is not an options
94
- * object, and inspection reads it to copy a declared value faithfully.
95
- */
96
- export function isPlainObject(value) {
97
- if (value === null || typeof value !== 'object') {
98
- return false;
99
- }
100
- const prototype = Object.getPrototypeOf(value);
101
- return prototype === Object.prototype || prototype === null;
102
- }
package/dist/globals.d.ts CHANGED
@@ -1,7 +1,8 @@
1
1
  import type { BoundOption } from './bindings.js';
2
- import { DeclarationError } from './errors.js';
3
2
  import type { DescriptorRegistry } from './extension.js';
3
+ import type { InputSite } from './facts.js';
4
4
  import { compileOptions } from './options.js';
5
+ import type { CompileScope } from './options.js';
5
6
  import type { BuiltPlugin } from './plugin.js';
6
7
  import type { OptionConfig, OptionValue } from './types.js';
7
8
  import type { InputDeclaration, OptionInput, ValidatedInputs } from './validation.js';
@@ -20,26 +21,22 @@ type OptionOwner = {
20
21
  identity: string;
21
22
  order: number;
22
23
  };
23
- /** One side of a collision: the option's declared name under the scope that declared it. */
24
- interface OptionSite {
25
- name: string;
24
+ /** One option the globals table holds: the scope that declared it, and where it was declared. */
25
+ interface TableEntry {
26
26
  owner: OptionOwner;
27
+ site: InputSite;
27
28
  }
28
- /** One key claimed twice, whichever two scopes claimed it. */
29
- declare function keyCollision(name: string, first: OptionOwner, second: OptionOwner): DeclarationError;
30
- /** One spelling claimed twice, whichever two scopes claimed it. */
31
- declare function spellingCollision(spelling: string, first: OptionSite, second: OptionSite): DeclarationError;
32
29
  /**
33
30
  * The globals table the pre-scan reads, with every rule that pairs two of its options settled: one
34
- * spelling map, the scope that owns each key, and the variable each option binds. It holds the
35
- * application's global options and every installed plugin's options, because the pre-scan reads one
36
- * table.
31
+ * spelling map, the scope that owns each key and where it was declared, and the variable each
32
+ * option binds. It holds the application's global options and every installed plugin's options,
33
+ * because the pre-scan reads one table.
37
34
  */
38
35
  interface GlobalTable {
39
- names: ReadonlyMap<string, OptionOwner>;
36
+ names: ReadonlyMap<string, TableEntry>;
40
37
  options: ReturnType<typeof compileOptions>;
41
- /** The variable each option in the table binds, under the phrase a duplicate names it by. */
42
- variables: ReadonlyMap<string, string>;
38
+ /** The variable each option in the table binds, with the option that binds it. */
39
+ variables: ReadonlyMap<string, BoundOption>;
43
40
  }
44
41
  /**
45
42
  * The compiled table one graph build shares. `inputs` holds the application's declarations alone,
@@ -62,12 +59,22 @@ interface GlobalsState<Globals = unknown> {
62
59
  records: InputRecords;
63
60
  }
64
61
  declare function emptyGlobals(): GlobalsState<{}>;
62
+ /** Where one global option was declared: its `globalOption()` call on the Application. */
63
+ declare function globalSite(input: OptionInput): InputSite;
64
+ /**
65
+ * Where each of one plugin's options was declared: its entry in the `options` record of the
66
+ * plugin's `plugin()` call, which is rebuilt from every option the plugin holds.
67
+ */
68
+ declare function pluginSites(identity: string, inputs: readonly OptionInput[]): (input: OptionInput) => InputSite;
65
69
  /**
66
70
  * One global option's own facts, binding, and extension values, checked at its `globalOption()`
67
71
  * call against the Application's descriptors. The rules that pair it with another option belong to
68
72
  * the table, which the caller rebuilds with it.
69
73
  */
70
- declare function declareGlobalOption<Globals, Name extends string, Config extends OptionConfig>(state: GlobalsState<Globals>, input: OptionInput<Name, Config>, descriptors: DescriptorRegistry): GlobalsState<Globals & Record<Name, OptionValue<Config>>>;
74
+ declare function declareGlobalOption<Globals, Name extends string, Config extends OptionConfig>(state: GlobalsState<Globals>, declared: OptionInput<Name, Config>, descriptors: DescriptorRegistry): {
75
+ readonly input: OptionInput<Name, Config>;
76
+ readonly state: GlobalsState<Globals & Record<Name, OptionValue<Config>>>;
77
+ };
71
78
  /**
72
79
  * The one table the pre-scan reads: the application's global options in authoring order, then each
73
80
  * installed plugin's options in installation order. Every collision between the two scopes, by key
@@ -81,10 +88,11 @@ declare function buildGlobals(node: GlobalsState, plugins: readonly BuiltPlugin[
81
88
  * application's globals and every plugin option, so a local collision reads the same sentence
82
89
  * whichever scope on the other side claimed the name, the spelling, or the variable. One
83
90
  * invocation's scope is this Command's own options and the table, so a variable binds one option
84
- * there, while a sibling Command may bind it again.
91
+ * there, while a sibling Command may bind it again. `scope` names the Command and places each of
92
+ * its options.
85
93
  */
86
- declare function checkLocalOptions(declarations: readonly OptionInput[], table: GlobalTable, subject: string): ReturnType<typeof compileOptions>;
87
- /** The options in one list that bind a variable, each named by the phrase its scope gives it. */
88
- declare function boundOptions(inputs: readonly OptionInput[], site: (name: string) => string): BoundOption[];
89
- export type { BuiltGlobals, GlobalsState, GlobalTable, InputRecords, OptionOwner, OptionSite };
90
- export { boundOptions, buildGlobals, checkLocalOptions, declareGlobalOption, emptyGlobals, globalTable, keyCollision, spellingCollision, };
94
+ declare function checkLocalOptions(declarations: readonly OptionInput[], table: GlobalTable, scope: CompileScope<OptionInput>): ReturnType<typeof compileOptions>;
95
+ /** The options in one list that bind a variable, each named and placed by its scope. */
96
+ declare function boundOptions(inputs: readonly OptionInput[], describe: (input: OptionInput) => Omit<BoundOption, 'variable'>): BoundOption[];
97
+ export type { BuiltGlobals, GlobalsState, GlobalTable, InputRecords, OptionOwner, TableEntry };
98
+ export { boundOptions, buildGlobals, checkLocalOptions, declareGlobalOption, emptyGlobals, globalSite, globalTable, pluginSites, };
package/dist/globals.js CHANGED
@@ -1,8 +1,10 @@
1
1
  import { checkEnvBinding, claimVariables } from './bindings.js';
2
- import { DeclarationError } from './errors.js';
2
+ import { DeclarationError, quoted } from './errors.js';
3
3
  import { buildExtensions } from './extension.js';
4
- import { checkDeprecated, checkDescription, checkHidden } from './facts.js';
5
- import { compileOptions } from './options.js';
4
+ import { callSite, checkDeprecated, checkDescription, checkHidden, factFault, pluginOptionSite, siteFinding, } from './facts.js';
5
+ import { globalPresenceRule, optionDeclaredTwice, spellingTaken } from './input-rules.js';
6
+ import { checkOptionName, compileOptions, spellingMark } from './options.js';
7
+ import { captureInputConfig, configUnread } from './validation.js';
6
8
  const globalSubject = 'the global options';
7
9
  /**
8
10
  * The presence keys a global option may not declare, in the order its diagnostic names them. A
@@ -29,7 +31,7 @@ function ordered(first, second) {
29
31
  /** How one owner reads in a key collision. A repeated preposition is dropped after the first. */
30
32
  function declaredBy(owner, leading) {
31
33
  if (owner.kind === 'plugin') {
32
- return `${leading ? 'by ' : ''}plugin "${owner.identity}"`;
34
+ return `${leading ? 'by ' : ''}plugin ${quoted(owner.identity)}`;
33
35
  }
34
36
  return owner.kind === 'application'
35
37
  ? 'as a global option'
@@ -38,11 +40,11 @@ function declaredBy(owner, leading) {
38
40
  /** How one owner reads in a spelling collision, where each side names its own option. */
39
41
  function usedBy({ name, owner }) {
40
42
  if (owner.kind === 'plugin') {
41
- return `plugin "${owner.identity}" option "${name}"`;
43
+ return `plugin ${quoted(owner.identity)} option ${quoted(name)}`;
42
44
  }
43
45
  return owner.kind === 'application'
44
- ? `the global option "${name}"`
45
- : `the local option "${name}" on ${owner.subject}`;
46
+ ? `the global option ${quoted(name)}`
47
+ : `the local option ${quoted(name)} on ${owner.subject}`;
46
48
  }
47
49
  /** The correction each pair earns: a local is renamed, and two plugins are chosen between. */
48
50
  function correction(first, second) {
@@ -53,45 +55,93 @@ function correction(first, second) {
53
55
  ? 'Install one of them or rename the option.'
54
56
  : 'Rename one declaration.';
55
57
  }
58
+ /** The note beside one side's finding, which says which scope declared it. */
59
+ const sideNotes = {
60
+ application: 'the global option',
61
+ local: 'the local option',
62
+ plugin: 'the plugin option',
63
+ };
56
64
  /** One key claimed twice, whichever two scopes claimed it. */
57
- function keyCollision(name, first, second) {
58
- const [leading, trailing] = ordered({ name, owner: first }, { name, owner: second });
59
- return new DeclarationError(`Option "${name}" is declared ${declaredBy(leading.owner, true)} and ${declaredBy(trailing.owner, false)}. ${correction(leading.owner, trailing.owner)}`);
65
+ function keyCollision(first, second) {
66
+ const pair = ordered(first, second);
67
+ const [leading, trailing] = pair;
68
+ return new DeclarationError(optionDeclaredTwice, {
69
+ correction: correction(leading.owner, trailing.owner),
70
+ findings: pair.map(({ owner, site }) => siteFinding(site, site.named, sideNotes[owner.kind])),
71
+ sentence: `Option ${quoted(leading.name)} is declared ${declaredBy(leading.owner, true)} and ${declaredBy(trailing.owner, false)}.`,
72
+ });
60
73
  }
61
74
  /** One spelling claimed twice, whichever two scopes claimed it. */
62
75
  function spellingCollision(spelling, first, second) {
63
- const [leading, trailing] = ordered(first, second);
64
- return new DeclarationError(`Option spelling "${spelling}" is used by ${usedBy(leading)} and ${usedBy(trailing)}. Change one declaration.`);
76
+ const pair = ordered(first, second);
77
+ const [leading, trailing] = pair;
78
+ return new DeclarationError(spellingTaken, {
79
+ correction: 'Change one declaration.',
80
+ findings: pair.map(({ owner, role, site }) => siteFinding(site, spellingMark(site, role), sideNotes[owner.kind])),
81
+ sentence: `Option spelling ${quoted(spelling)} is used by ${usedBy(leading)} and ${usedBy(trailing)}.`,
82
+ });
65
83
  }
66
84
  function emptyGlobals() {
67
85
  return { bind: () => ({}), inputs: [], records: new Map() };
68
86
  }
87
+ /** Where one global option was declared: its `globalOption()` call on the Application. */
88
+ function globalSite(input) {
89
+ // A global option belongs to the application, not to one Command, so its facts read that way.
90
+ return callSite(`Global option ${quoted(input.name)}`, {
91
+ arguments: [input.name, input.config],
92
+ call: 'globalOption',
93
+ path: [],
94
+ });
95
+ }
96
+ /**
97
+ * Where each of one plugin's options was declared: its entry in the `options` record of the
98
+ * plugin's `plugin()` call, which is rebuilt from every option the plugin holds.
99
+ */
100
+ function pluginSites(identity, inputs) {
101
+ const options = Object.fromEntries(inputs.map(({ config, name }) => [name, config]));
102
+ return (input) => pluginOptionSite({ identity, options }, input.name, `Plugin ${quoted(identity)} option ${quoted(input.name)}`);
103
+ }
69
104
  /**
70
105
  * One global option's own facts, binding, and extension values, checked at its `globalOption()`
71
106
  * call against the Application's descriptors. The rules that pair it with another option belong to
72
107
  * the table, which the caller rebuilds with it.
73
108
  */
74
- function declareGlobalOption(state, input, descriptors) {
75
- // A global option belongs to the application, not to one Command, so its facts read that way.
76
- const sentence = `Global option "${input.name}"`;
109
+ function declareGlobalOption(state, declared, descriptors) {
110
+ // The name is judged before the config, as every other declaration judges its own name first.
111
+ checkOptionName(declared.name, configUnread(globalSite(declared)));
112
+ const input = {
113
+ ...declared,
114
+ config: captureInputConfig(declared, { call: 'globalOption', path: [] }),
115
+ };
116
+ const site = globalSite(input);
117
+ const sentence = site.subject;
77
118
  const rejected = omissionRules.find((key) => key in input.config);
78
119
  if (rejected !== undefined) {
79
- throw new DeclarationError(`${sentence} declares ${rejected}. Remove it; an omitted global option is absent, and a Command that needs its value checks for it.`);
120
+ throw factFault(globalPresenceRule, site, {
121
+ correction: `Remove ${rejected}, and check for the value in each Command that needs it.`,
122
+ fact: rejected,
123
+ sentence: `${sentence} declares ${rejected}.`,
124
+ });
80
125
  }
81
- checkDescription(sentence, input.config.description);
82
- checkHidden(sentence, input.config.hidden);
83
- checkDeprecated(sentence, input.config.deprecated);
84
- checkEnvBinding(sentence, input.config);
126
+ checkDescription(site, input.config.description);
127
+ checkHidden(site, input.config.hidden);
128
+ checkDeprecated(site, input.config.deprecated);
129
+ checkEnvBinding(site, input.config);
85
130
  const record = buildExtensions({
86
131
  declared: input.config.extensions,
87
132
  descriptors,
88
- subject: { phrase: `on the global option "${input.name}"`, sentence },
133
+ site: { ...site, at: '1.extensions' },
134
+ subject: { phrase: `on the global option ${quoted(input.name)}`, sentence },
89
135
  target: 'option',
90
136
  });
137
+ // The caller validates the captured input this state stores, so both read one capture.
91
138
  return {
92
- bind: (values) => ({ ...state.bind(values), ...values.option(input) }),
93
- inputs: [...state.inputs, input],
94
- records: new Map([...state.records, [input, record]]),
139
+ input,
140
+ state: {
141
+ bind: (values) => ({ ...state.bind(values), ...values.option(input) }),
142
+ inputs: [...state.inputs, input],
143
+ records: new Map([...state.records, [input, record]]),
144
+ },
95
145
  };
96
146
  }
97
147
  /**
@@ -103,9 +153,9 @@ function globalTable(inputs, plugins) {
103
153
  const names = new Map();
104
154
  const application = { kind: 'application' };
105
155
  for (const input of inputs) {
106
- names.set(input.name, application);
156
+ names.set(input.name, { owner: application, site: globalSite(input) });
107
157
  }
108
- const options = compileOptions(inputs, globalSubject);
158
+ const options = compileOptions(inputs, { siteOf: globalSite, subject: globalSubject });
109
159
  plugins.forEach((installed, order) => {
110
160
  join({ identity: installed.identity, kind: 'plugin', order }, installed.inputs, {
111
161
  names,
@@ -113,8 +163,17 @@ function globalTable(inputs, plugins) {
113
163
  });
114
164
  });
115
165
  const variables = claimVariables([
116
- ...boundOptions(inputs, (name) => `global option "${name}"`),
117
- ...plugins.flatMap((installed) => boundOptions(installed.inputs, (name) => `plugin "${installed.identity}" option "${name}"`)),
166
+ ...boundOptions(inputs, (input) => ({
167
+ phrase: `global option ${quoted(input.name)}`,
168
+ site: globalSite(input),
169
+ })),
170
+ ...plugins.flatMap((installed) => {
171
+ const siteOf = pluginSites(installed.identity, installed.inputs);
172
+ return boundOptions(installed.inputs, (input) => ({
173
+ phrase: `plugin ${quoted(installed.identity)} option ${quoted(input.name)}`,
174
+ site: siteOf(input),
175
+ }));
176
+ }),
118
177
  ]);
119
178
  return { names, options, variables };
120
179
  }
@@ -127,47 +186,59 @@ function buildGlobals(node, plugins) {
127
186
  * application's globals and every plugin option, so a local collision reads the same sentence
128
187
  * whichever scope on the other side claimed the name, the spelling, or the variable. One
129
188
  * invocation's scope is this Command's own options and the table, so a variable binds one option
130
- * there, while a sibling Command may bind it again.
189
+ * there, while a sibling Command may bind it again. `scope` names the Command and places each of
190
+ * its options.
131
191
  */
132
- function checkLocalOptions(declarations, table, subject) {
192
+ function checkLocalOptions(declarations, table, scope) {
193
+ const { siteOf, subject } = scope;
133
194
  const local = { kind: 'local', subject };
134
- const application = { kind: 'application' };
135
195
  for (const declaration of declarations) {
136
196
  const claimed = table.names.get(declaration.name);
137
197
  if (claimed) {
138
- throw keyCollision(declaration.name, claimed, local);
198
+ throw keyCollision({ ...claimed, name: declaration.name }, { name: declaration.name, owner: local, site: siteOf(declaration) });
139
199
  }
140
200
  }
141
- const options = compileOptions(declarations, subject);
201
+ const options = compileOptions(declarations, scope);
142
202
  for (const [spelling, option] of options) {
143
203
  const global = table.options.get(spelling);
144
- if (global) {
145
- throw spellingCollision(spelling, { name: global.name, owner: table.names.get(global.name) ?? application }, { name: option.name, owner: local });
204
+ const claimed = global && table.names.get(global.name);
205
+ const declaration = declarations.find((input) => input.name === option.name);
206
+ if (global && claimed && declaration) {
207
+ throw spellingCollision(spelling, { ...claimed, name: global.name, role: global.role }, { name: option.name, owner: local, role: option.role, site: siteOf(declaration) });
146
208
  }
147
209
  }
148
- claimVariables(boundOptions(declarations, (option) => `${subject} option "${option}"`), table.variables);
210
+ claimVariables(boundOptions(declarations, (input) => ({
211
+ phrase: `${subject} option ${quoted(input.name)}`,
212
+ site: siteOf(input),
213
+ })), table.variables);
149
214
  return options;
150
215
  }
151
- /** The options in one list that bind a variable, each named by the phrase its scope gives it. */
152
- function boundOptions(inputs, site) {
153
- return inputs.flatMap(({ config, name }) => config.env === undefined ? [] : [{ site: site(name), variable: config.env }]);
216
+ /** The options in one list that bind a variable, each named and placed by its scope. */
217
+ function boundOptions(inputs, describe) {
218
+ return inputs.flatMap((input) => input.config.env === undefined ? [] : [{ ...describe(input), variable: input.config.env }]);
154
219
  }
155
220
  /** One plugin's options joining the table the application's globals already hold. */
156
221
  function join(owner, inputs, table) {
157
222
  const { names, options } = table;
223
+ const siteOf = pluginSites(owner.identity, inputs);
158
224
  for (const input of inputs) {
159
225
  const claimed = names.get(input.name);
160
226
  if (claimed) {
161
- throw keyCollision(input.name, owner, claimed);
227
+ throw keyCollision({ name: input.name, owner, site: siteOf(input) }, { ...claimed, name: input.name });
162
228
  }
163
- names.set(input.name, owner);
229
+ names.set(input.name, { owner, site: siteOf(input) });
164
230
  }
165
- for (const [spelling, option] of compileOptions(inputs, `plugin "${owner.identity}"`)) {
166
- const claimed = options.get(spelling);
167
- if (claimed) {
168
- throw spellingCollision(spelling, { name: option.name, owner }, { name: claimed.name, owner: names.get(claimed.name) ?? { kind: 'application' } });
231
+ for (const [spelling, option] of compileOptions(inputs, {
232
+ siteOf,
233
+ subject: `plugin ${quoted(owner.identity)}`,
234
+ })) {
235
+ const existing = options.get(spelling);
236
+ const claimed = existing && names.get(existing.name);
237
+ const own = names.get(option.name);
238
+ if (existing && claimed && own) {
239
+ throw spellingCollision(spelling, { ...own, name: option.name, role: option.role }, { ...claimed, name: existing.name, role: existing.role });
169
240
  }
170
241
  options.set(spelling, option);
171
242
  }
172
243
  }
173
- export { boundOptions, buildGlobals, checkLocalOptions, declareGlobalOption, emptyGlobals, globalTable, keyCollision, spellingCollision, };
244
+ export { boundOptions, buildGlobals, checkLocalOptions, declareGlobalOption, emptyGlobals, globalSite, globalTable, pluginSites, };
@@ -1,5 +1,5 @@
1
1
  // Generated by scripts/check-glyph-catalog.py from Inquirer a446ea9e273864a3653a26943f59b4fbe8003796.
2
- /*
2
+ /*!
3
3
  Copyright (c) 2025 Simon Boudrias
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person
@@ -0,0 +1,79 @@
1
+ import type { Writable } from 'node:stream';
2
+ import type { DeveloperScene } from './developer.js';
3
+ import type { LoomError } from './errors.js';
4
+ import type { CommandGraph, CommandNode } from './inspect.js';
5
+ import type { Output } from './output.js';
6
+ import type { BuiltPlugin } from './plugin.js';
7
+ import type { ContextualStyle } from './style.js';
8
+ import type { ViewRegistry } from './view.js';
9
+ /**
10
+ * What one `onFailure` hook reads beside the failure. `application` and `path` are the values the
11
+ * failure view reads, and `style` is the contextual style for stderr, so a hook escapes text the
12
+ * operator typed. `graph` is the frozen graph `inspect()` returns for the run, and `command` is the
13
+ * node at `path` inside it; reading either builds the run's graph once.
14
+ */
15
+ interface FailureHookContext {
16
+ readonly application: string;
17
+ readonly path: readonly string[];
18
+ readonly style: ContextualStyle;
19
+ readonly graph: CommandGraph;
20
+ readonly command: CommandNode;
21
+ }
22
+ /**
23
+ * A plugin's `onFailure` hook: a synchronous function that returns one hint, a list of hints, or
24
+ * nothing for a failure `run()` renders after graph build. It receives the failure typed read-only,
25
+ * as a failure view does. Core does not freeze the failure, and it reads the exit code before any
26
+ * hook runs, so a working hook cannot change the code.
27
+ */
28
+ type FailureHook = (failure: Readonly<LoomError>, context: FailureHookContext) => string | readonly string[] | undefined;
29
+ /** What the hooks can read once the graph has built: the installed plugins and the run's graph. */
30
+ interface BuiltRun {
31
+ plugins: readonly BuiltPlugin[];
32
+ inspected: () => CommandGraph;
33
+ }
34
+ /**
35
+ * Where one failure happened, filled where `run()` catches it. `built` is absent for a failure
36
+ * raised at or before graph build, which no hook runs for, because no valid graph exists.
37
+ */
38
+ interface FailureScene {
39
+ application: string;
40
+ path: readonly string[];
41
+ built: BuiltRun | undefined;
42
+ }
43
+ /**
44
+ * What one run's build decides about its reports. A development build shows the author a
45
+ * Developer Diagnostic for every fault only the author can fix, each after the first report of the
46
+ * run opening with one blank line; a distributed build shows the generic defect message at most
47
+ * once per run.
48
+ */
49
+ interface BuildReports {
50
+ readonly development: boolean;
51
+ /** Whether this run has written the generic defect message already. */
52
+ generic: boolean;
53
+ /** Whether this run has written a failure report already. */
54
+ reported: boolean;
55
+ }
56
+ /** Where one failure report is written, the registry its view resolves through, and the build. */
57
+ interface FailureSink {
58
+ output: Output;
59
+ registry: ViewRegistry;
60
+ stderr: Writable;
61
+ build: BuildReports;
62
+ }
63
+ /**
64
+ * Reports one failure. The hooks run first, so the diagnostic or the view receives their hints. A
65
+ * view that breaks leaves core's default text without hints on the plain fallback path, and each
66
+ * broken contract's report follows that whole diagnostic. It answers whether a view or a hook
67
+ * broke, which forces the run's code to 1 outside a cancelled run.
68
+ */
69
+ declare function reportFailure(sink: FailureSink, failure: LoomError, scene: FailureScene & {
70
+ host: DeveloperScene['host'];
71
+ }): Promise<boolean>;
72
+ /**
73
+ * What a run writes on the plain fallback path when a destination failed a write or reporting
74
+ * itself failed: the Developer Diagnostic of the broken destination in a development build, and
75
+ * the generic defect message, at most once per run, in a distributed one.
76
+ */
77
+ declare function destinationReport(build: BuildReports, cause: unknown, scene: DeveloperScene): string;
78
+ export type { BuildReports, BuiltRun, FailureHook, FailureHookContext, FailureScene };
79
+ export { destinationReport, reportFailure };