@loomcli/core 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/LICENSE +21 -0
  2. package/dist/application.d.ts +60 -31
  3. package/dist/application.js +356 -149
  4. package/dist/bindings.d.ts +31 -0
  5. package/dist/bindings.js +64 -0
  6. package/dist/chain.d.ts +26 -12
  7. package/dist/chain.js +59 -92
  8. package/dist/command-rules.d.ts +55 -0
  9. package/dist/command-rules.js +142 -0
  10. package/dist/command.d.ts +212 -93
  11. package/dist/command.js +1224 -454
  12. package/dist/controls.d.ts +8 -0
  13. package/dist/controls.js +23 -0
  14. package/dist/defect.d.ts +18 -0
  15. package/dist/defect.js +272 -0
  16. package/dist/developer.d.ts +24 -0
  17. package/dist/developer.js +52 -0
  18. package/dist/diagnostic-text.d.ts +81 -0
  19. package/dist/diagnostic-text.js +283 -0
  20. package/dist/diagnostic.d.ts +11 -0
  21. package/dist/diagnostic.js +70 -0
  22. package/dist/errors.d.ts +115 -26
  23. package/dist/errors.js +333 -54
  24. package/dist/exit-codes.d.ts +45 -0
  25. package/dist/exit-codes.js +46 -0
  26. package/dist/extension.d.ts +44 -9
  27. package/dist/extension.js +147 -65
  28. package/dist/facts.d.ts +71 -11
  29. package/dist/facts.js +108 -25
  30. package/dist/globals.d.ts +63 -22
  31. package/dist/globals.js +164 -39
  32. package/dist/hints.d.ts +79 -0
  33. package/dist/hints.js +247 -0
  34. package/dist/host.d.ts +13 -0
  35. package/dist/host.js +43 -1
  36. package/dist/identity.d.ts +19 -0
  37. package/dist/identity.js +72 -0
  38. package/dist/index.d.ts +15 -4
  39. package/dist/index.js +6 -0
  40. package/dist/input-rules.d.ts +64 -0
  41. package/dist/input-rules.js +145 -0
  42. package/dist/inspect.d.ts +36 -5
  43. package/dist/inspect.js +125 -27
  44. package/dist/lanes.js +1 -1
  45. package/dist/locate.d.ts +41 -0
  46. package/dist/locate.js +121 -0
  47. package/dist/options.d.ts +96 -2
  48. package/dist/options.js +259 -71
  49. package/dist/output.d.ts +11 -2
  50. package/dist/output.js +23 -3
  51. package/dist/plain.d.ts +6 -0
  52. package/dist/plain.js +12 -0
  53. package/dist/plugin-rules.d.ts +62 -0
  54. package/dist/plugin-rules.js +155 -0
  55. package/dist/plugin.d.ts +121 -55
  56. package/dist/plugin.js +496 -126
  57. package/dist/prototypes.d.ts +7 -0
  58. package/dist/prototypes.js +29 -0
  59. package/dist/rendering.d.ts +6 -1
  60. package/dist/rendering.js +23 -5
  61. package/dist/rules.d.ts +51 -0
  62. package/dist/rules.js +115 -0
  63. package/dist/sequence.js +6 -1
  64. package/dist/sources.d.ts +58 -0
  65. package/dist/sources.js +258 -0
  66. package/dist/style-wire.js +1 -1
  67. package/dist/style.js +1 -1
  68. package/dist/theme.d.ts +4 -0
  69. package/dist/theme.js +25 -5
  70. package/dist/thenable.d.ts +15 -0
  71. package/dist/thenable.js +29 -0
  72. package/dist/translators.d.ts +69 -0
  73. package/dist/translators.js +253 -0
  74. package/dist/types.d.ts +76 -21
  75. package/dist/validation.d.ts +65 -10
  76. package/dist/validation.js +314 -108
  77. package/dist/view.d.ts +49 -15
  78. package/dist/view.js +157 -79
  79. package/package.json +3 -2
package/dist/errors.js CHANGED
@@ -1,3 +1,8 @@
1
+ import { escapeControlCharacters } from './controls.js';
2
+ import { diagnosticText, isDiagnosticRule, valueCode } from './diagnostic-text.js';
3
+ import { isFailureExitCode } from './exit-codes.js';
4
+ import { failureExitCode, foreignThrow, foreignThrowCorrection, resultContract, unconstructedFailure, } from './rules.js';
5
+ import { ignoreRejection, isThenable } from './thenable.js';
1
6
  /** The same subject at the start of a sentence, where a token fault names its Command. */
2
7
  function routedSentence(command) {
3
8
  const subject = routedSubject(command);
@@ -14,36 +19,85 @@ function resultMessage(kind, command) {
14
19
  if (kind === 'undeclared') {
15
20
  return `${routedSentence(command)} declares no result. Declare one with result() or rows() before action().`;
16
21
  }
17
- return `A middleware called out.results() on ${routedSubject(command)}. Only the action emits a result.`;
22
+ const caller = kind === 'source' ? 'A configuration source' : 'A middleware';
23
+ return `${caller} called out.results() on ${routedSubject(command)}. Only the action emits a result.`;
18
24
  }
19
25
  /**
20
- * The clause that offers the candidates, which is absent when there are none: every child of the
21
- * Command is hidden, so the diagnostic ends after the sentence that names the fault.
26
+ * A value a sentence quotes: raw text an operator typed, or a name or identity an author declared.
27
+ * Text is escaped, so a control character or a bidirectional control in it cannot reorder or break
28
+ * the line. A value of any other kind prints as the code a finding prints for it, because a rule
29
+ * that rejects a value that is not a string quotes that value too. The failure's public field
30
+ * keeps the raw value.
22
31
  */
23
- function offering(candidates) {
24
- return candidates.length === 0 ? '' : ` Use one of: ${candidates.join(', ')}.`;
32
+ export function quoted(value) {
33
+ return typeof value === 'string' ? `"${escapeControlCharacters(value)}"` : valueCode(value);
34
+ }
35
+ /**
36
+ * The clause that states a routing fault's fix: the candidates, or, when the Command offers none
37
+ * because every child is hidden or deprecated, the kind of name to supply.
38
+ */
39
+ function offering(candidates, kind) {
40
+ return candidates.length === 0
41
+ ? ` Supply the name of a declared ${kind}.`
42
+ : ` Use one of: ${candidates.join(', ')}.`;
43
+ }
44
+ /**
45
+ * The sentence for a group routed without a subcommand. The root has no name to repeat, and the
46
+ * default text already opens with the application name, so the root's sentence names no Command.
47
+ */
48
+ function nonCallableMessage(command, candidates) {
49
+ return command.length === 0
50
+ ? `A command is required.${offering(candidates, 'command')}`
51
+ : `${routedSentence(command)} requires a subcommand.${offering(candidates, 'subcommand')}`;
25
52
  }
26
53
  function shortGroupMessage(fault) {
27
54
  return fault.reason === 'value-position'
28
- ? `Value option "${fault.token}" must be last in its short group. Supply its value in the next token.`
29
- : `Short group "${fault.token}" mixes the global option "-${fault.global}" with "-${fault.other}", which is not a global option. Supply global options as separate tokens, and local options after their command name.`;
55
+ ? `Value option ${quoted(fault.token)} must be last in its short group. Supply its value in the next token.`
56
+ : `A short group mixes the global option ${quoted(`-${fault.global}`)} with ${quoted(`-${fault.other}`)}, which is not a global option. Supply global options as separate tokens, and local options after their command name.`;
57
+ }
58
+ /**
59
+ * The sentence for a class whose declared code no failure may exit with. The class is named by its
60
+ * constructor, because the subclass has not yet set the instance's `name`.
61
+ */
62
+ function undeclarableSentence(className, declared) {
63
+ const clause = typeof declared === 'number' && Number.isFinite(declared)
64
+ ? `declares exit code ${String(declared)}.`
65
+ : 'declares an exit code that is not a finite number.';
66
+ return `Failure class ${quoted(className)} ${clause}`;
67
+ }
68
+ /**
69
+ * The one message a distributed build shows an operator for a defect or a declaration fault: the
70
+ * application name and a fixed phrase, with no reason, class name, code, or path.
71
+ */
72
+ export function genericDefectText(application) {
73
+ return `${application}: Something went wrong.\n`;
30
74
  }
31
75
  /**
32
- * Core's own text for one failure: its message under the category prefix its class carries, with
33
- * the trailing newline every view's text carries. The four categories are disjoint branches of the
34
- * hierarchy, so one ordered test reads every class, and a class without a prefix of its own writes
35
- * the sentence alone. It is the default view of every failure class and the text the plain
36
- * fallback path writes, so it runs no application code and nothing downstream composes its newline.
76
+ * Whether one failure is only the author's to fix: a declaration fault or a defect. A development
77
+ * build shows the author its Developer Diagnostic; a distributed one shows the generic message.
37
78
  */
38
- export function defaultText(failure) {
79
+ export function isAuthorFault(failure) {
80
+ return failure instanceof DeclarationError || failure instanceof InternalError;
81
+ }
82
+ /**
83
+ * Core's own text for one failure, with the trailing newline every view's text carries. The
84
+ * application name opens every line of a usage failure's message, one line for each problem it
85
+ * reports, so the operator reads who is speaking on each. A declaration fault and a defect read
86
+ * the generic defect message, because only the author can act on their detail, and a development
87
+ * build shows that detail ahead of every view. Every other class writes its message alone, even
88
+ * one that declares a usage error's exit code. It is the default view of every failure class and
89
+ * the text the plain fallback path writes, so it runs no application code and nothing downstream
90
+ * composes its newline.
91
+ */
92
+ export function defaultText(failure, application) {
39
93
  if (failure instanceof UsageError) {
40
- return `Invalid input: ${failure.message}\n`;
41
- }
42
- if (failure instanceof DeclarationError) {
43
- return `Invalid declaration: ${failure.message}\n`;
94
+ return failure.message
95
+ .split('\n')
96
+ .map((line) => `${application}: ${line}\n`)
97
+ .join('');
44
98
  }
45
- if (failure instanceof InternalError) {
46
- return `Internal error: ${failure.message}\n`;
99
+ if (isAuthorFault(failure)) {
100
+ return genericDefectText(application);
47
101
  }
48
102
  return `${failure.message}\n`;
49
103
  }
@@ -61,32 +115,90 @@ export function commandSentence(name) {
61
115
  const subject = commandSubject(name);
62
116
  return `${subject.slice(0, 1).toUpperCase()}${subject.slice(1)}`;
63
117
  }
118
+ /**
119
+ * Each failure class's code, captured at the first construction of the class or of a subclass that
120
+ * declares none, so a static changed afterward cannot give one class a second code. Core's own
121
+ * classes are captured when this module loads, so no write to their statics reaches a failure.
122
+ */
123
+ const classCodes = new WeakMap();
124
+ /** Each constructed failure's code, which its `exitCode` reports and `run()` resolves. */
125
+ const failureCodes = new WeakMap();
126
+ /**
127
+ * The code one class exits with: the code captured for it, or else its own static when it declares
128
+ * one, or else its parent's. `constructed` names the class the diagnostic reports, the one the
129
+ * failing construction named.
130
+ */
131
+ function classCode(target, constructed) {
132
+ const captured = classCodes.get(target);
133
+ if (captured !== undefined) {
134
+ return captured;
135
+ }
136
+ const parent = Reflect.getPrototypeOf(target);
137
+ const declared = Object.hasOwn(target, 'exitCode') || parent === null
138
+ ? Reflect.get(target, 'exitCode')
139
+ : classCode(parent, constructed);
140
+ if (!isFailureExitCode(declared)) {
141
+ // A class declaration is no call, so no finding stands for it.
142
+ throw new DeclarationError(failureExitCode, {
143
+ correction: 'Declare a whole number from 1 through 125.',
144
+ sentence: undeclarableSentence(constructed.name, declared),
145
+ });
146
+ }
147
+ classCodes.set(target, declared);
148
+ return declared;
149
+ }
150
+ /**
151
+ * The code a failure exits with. A value that inherits from a failure class without having been
152
+ * constructed holds none, and `toFailure` reports it as an internal error, so it reads 1.
153
+ */
154
+ export function exitCodeOf(failure) {
155
+ return failureCodes.get(failure) ?? 1;
156
+ }
64
157
  /**
65
158
  * Every failure `run()` reports is an instance of a public class. Each class carries the facts its
66
- * sentence interpolates, so a view reads them instead of parsing prose, and the exit status is
67
- * a field of the base, so a subclass inherits it. `message` never carries a category prefix; the
68
- * default views add it.
159
+ * sentence interpolates, so a view reads them instead of parsing prose. The exit code is a static
160
+ * field the class declares, read from the nearest ancestor that declares one and captured at the
161
+ * class's first construction, so one class exits with one code and a projection reads it without
162
+ * an instance. The instance reports the same value through a read-only accessor, and no subclass
163
+ * property or assignment changes the code `run()` resolves. `message` never carries a category
164
+ * prefix; the default views add it.
69
165
  */
70
166
  export class LoomError extends Error {
71
- exitCode;
72
- constructor(message, exitCode) {
73
- super(message);
74
- this.exitCode = exitCode;
167
+ static exitCode = 1;
168
+ /**
169
+ * Reads the constructed class's code, captured at its first construction. A code outside 1
170
+ * through 125 throws a `DeclarationError` in place of the failure and captures nothing, because
171
+ * core never clamps or replaces a code. `options` is the platform's own, so a failure that
172
+ * replaces another error keeps it as `cause` only when its author passes one.
173
+ */
174
+ constructor(message, options) {
175
+ const code = classCode(new.target, new.target);
176
+ super(message, options);
177
+ failureCodes.set(this, code);
75
178
  this.name = 'LoomError';
76
179
  }
180
+ /**
181
+ * The code this failure exits with. An accessor without a setter, so a TypeScript subclass cannot
182
+ * declare it as a property, and an assignment throws in strict mode code and is ignored in sloppy
183
+ * mode code.
184
+ */
185
+ get exitCode() {
186
+ return exitCodeOf(this);
187
+ }
77
188
  }
78
189
  /** Exit 2: the invocation, not the application, is wrong. */
79
190
  export class UsageError extends LoomError {
80
- constructor(message) {
81
- super(message, 2);
191
+ static exitCode = 2;
192
+ constructor(message, options) {
193
+ super(message, options);
82
194
  this.name = 'UsageError';
83
195
  }
84
196
  }
85
197
  /** The whole validation phase in authoring order, so one failure reports every rejected input. */
86
198
  export class InputError extends UsageError {
87
199
  problems;
88
- constructor(message, problems) {
89
- super(message);
200
+ constructor(message, problems, options) {
201
+ super(message, options);
90
202
  this.name = 'InputError';
91
203
  this.problems = problems;
92
204
  }
@@ -95,7 +207,7 @@ export class UnknownCommandError extends UsageError {
95
207
  token;
96
208
  candidates;
97
209
  constructor(token, candidates) {
98
- super(`Unknown command "${token}".${offering(candidates)}`);
210
+ super(`Unknown command ${quoted(token)}.${offering(candidates, 'command')}`);
99
211
  this.candidates = candidates;
100
212
  this.name = 'UnknownCommandError';
101
213
  this.token = token;
@@ -106,7 +218,7 @@ export class NonCallableCommandError extends UsageError {
106
218
  command;
107
219
  candidates;
108
220
  constructor(command, candidates) {
109
- super(`${routedSentence(command)} requires a subcommand.${offering(candidates)}`);
221
+ super(nonCallableMessage(command, candidates));
110
222
  this.candidates = candidates;
111
223
  this.command = command;
112
224
  this.name = 'NonCallableCommandError';
@@ -129,7 +241,7 @@ export class UnexpectedArgumentError extends UsageError {
129
241
  export class UnknownOptionError extends UsageError {
130
242
  spelling;
131
243
  constructor(spelling) {
132
- super(`Unknown option "${spelling}". Supply a declared option; prefix a hyphenated path with "./".`);
244
+ super(`Unknown option ${quoted(spelling)}. Supply a declared option; prefix a hyphenated path with "./".`);
133
245
  this.name = 'UnknownOptionError';
134
246
  this.spelling = spelling;
135
247
  }
@@ -137,7 +249,7 @@ export class UnknownOptionError extends UsageError {
137
249
  export class MissingValueError extends UsageError {
138
250
  spelling;
139
251
  constructor(spelling) {
140
- super(`Option "${spelling}" requires a value. Supply a value after "${spelling}".`);
252
+ super(`Option ${quoted(spelling)} requires a value. Supply a value after ${quoted(spelling)}.`);
141
253
  this.name = 'MissingValueError';
142
254
  this.spelling = spelling;
143
255
  }
@@ -147,7 +259,7 @@ export class UnexpectedValueError extends UsageError {
147
259
  spelling;
148
260
  value;
149
261
  constructor(spelling, value) {
150
- super(`Boolean option "${spelling}" does not accept a value. Supply the flag alone.`);
262
+ super(`Boolean option ${quoted(spelling)} does not accept a value. Supply the flag alone.`);
151
263
  this.name = 'UnexpectedValueError';
152
264
  this.spelling = spelling;
153
265
  this.value = value;
@@ -156,7 +268,7 @@ export class UnexpectedValueError extends UsageError {
156
268
  export class RepeatedOptionError extends UsageError {
157
269
  spelling;
158
270
  constructor(spelling) {
159
- super(`Option "${spelling}" can be supplied only once. Remove the repeated option.`);
271
+ super(`Option ${quoted(spelling)} can be supplied only once. Remove the repeated option.`);
160
272
  this.name = 'RepeatedOptionError';
161
273
  this.spelling = spelling;
162
274
  }
@@ -171,27 +283,105 @@ export class ShortGroupError extends UsageError {
171
283
  this.token = fault.token;
172
284
  }
173
285
  }
174
- /** Exit 1: the declaration is wrong, so the author reads the diagnostic. */
286
+ /** The platform's error options one value carries, read without trusting its shape. */
287
+ function errorOptions(value) {
288
+ return typeof value === 'object' && value !== null && 'cause' in value
289
+ ? { cause: value.cause }
290
+ : undefined;
291
+ }
292
+ /** A correction as a caller supplied it: one sentence, a copied list of them, or none. */
293
+ function readCorrection(value) {
294
+ if (typeof value === 'string') {
295
+ return value;
296
+ }
297
+ return Array.isArray(value)
298
+ ? Object.freeze(value.filter((fix) => typeof fix === 'string'))
299
+ : undefined;
300
+ }
301
+ /** The findings a caller supplied, copied into a frozen list, or none for a value that holds none. */
302
+ function readFindings(value) {
303
+ return Object.freeze(Array.isArray(value) ? value.filter(isFinding) : []);
304
+ }
305
+ /** Whether one value is a finding a diagnostic can print: an object naming its call and arguments. */
306
+ function isFinding(value) {
307
+ return (typeof value === 'object' &&
308
+ value !== null &&
309
+ 'call' in value &&
310
+ typeof value.call === 'string' &&
311
+ 'arguments' in value &&
312
+ Array.isArray(value.arguments));
313
+ }
314
+ function faultParts(first, second, third) {
315
+ if (!isDiagnosticRule(first)) {
316
+ return {
317
+ correction: undefined,
318
+ findings: Object.freeze([]),
319
+ options: errorOptions(second),
320
+ rule: undefined,
321
+ sentence: String(first),
322
+ };
323
+ }
324
+ const parts = typeof second === 'object' && second !== null ? { ...second } : {};
325
+ return {
326
+ correction: readCorrection(parts.correction),
327
+ findings: readFindings(parts.findings),
328
+ options: errorOptions(third),
329
+ rule: first,
330
+ sentence: String(parts.sentence),
331
+ };
332
+ }
333
+ /**
334
+ * Exit 1: the declaration is wrong, so the author reads its Developer Diagnostic. A rule and the
335
+ * fault's own parts build it, or a sentence alone does for a fault with no rule. `message` holds
336
+ * the whole diagnostic as plain text at 80 columns, so a fault thrown at an authoring call prints
337
+ * it through the runtime's own uncaught-error output, and `sentence` holds the sentence alone.
338
+ */
175
339
  export class DeclarationError extends LoomError {
176
- constructor(message) {
177
- super(message, 1);
340
+ rule;
341
+ sentence;
342
+ findings;
343
+ correction;
344
+ constructor(first, second, third) {
345
+ const parts = faultParts(first, second, third);
346
+ super(diagnosticText({ ...parts, evidence: [], fallback: 'INVALID DECLARATION' }), parts.options);
178
347
  this.name = 'DeclarationError';
348
+ this.rule = parts.rule;
349
+ this.sentence = parts.sentence;
350
+ this.findings = parts.findings;
351
+ this.correction = parts.correction;
179
352
  }
180
353
  }
181
- /** Exit 1: the application ended the invocation itself. An application may subclass it. */
354
+ /**
355
+ * Exit 1: the application ended the invocation itself. An application may subclass it, and the
356
+ * subclass may declare its own exit code.
357
+ */
182
358
  export class FatalError extends LoomError {
183
- constructor(message) {
184
- super(message, 1);
359
+ constructor(message, options) {
360
+ super(message, options);
185
361
  this.name = 'FatalError';
186
362
  }
187
363
  }
188
- /** Exit 1: an unexpected exception, a non-error throw, or a view that could not answer. */
364
+ /**
365
+ * Exit 1: a defect, such as an unexpected exception, a non-error throw, or a view that could not
366
+ * answer. A rule and the defect's own parts build it, or a sentence and the thrown value do for a
367
+ * defect with no rule. `message` stays the sentence, because only `run()` reports a defect, and a
368
+ * development build renders its Developer Diagnostic from the parts.
369
+ */
189
370
  export class InternalError extends LoomError {
190
371
  cause;
191
- constructor(message, cause) {
192
- super(message, 1);
193
- this.cause = cause;
372
+ rule;
373
+ sentence;
374
+ correction;
375
+ constructor(first,
376
+ // The rule form's parts or the sentence form's thrown value, read by shape below.
377
+ second) {
378
+ const parts = faultParts(first, second, undefined);
379
+ super(parts.sentence);
380
+ this.cause = parts.rule === undefined ? second : errorOptions(second)?.cause;
194
381
  this.name = 'InternalError';
382
+ this.rule = parts.rule;
383
+ this.sentence = parts.sentence;
384
+ this.correction = parts.correction;
195
385
  }
196
386
  }
197
387
  /**
@@ -204,26 +394,115 @@ export class ResultError extends InternalError {
204
394
  path;
205
395
  kind;
206
396
  constructor(kind, path) {
207
- super(resultMessage(kind, path), undefined);
397
+ super(resultContract, { cause: undefined, sentence: resultMessage(kind, path) });
208
398
  this.kind = kind;
209
399
  this.name = 'ResultError';
210
400
  this.path = path;
211
401
  }
212
402
  }
213
- /** What a diagnostic says about an unexpected value, whether or not it was an Error. */
403
+ /**
404
+ * One message someone else wrote, as a sentence of its own. A schema or a plugin writes its message
405
+ * with or without a full stop, so a diagnostic supplies one only where the message carries none.
406
+ */
407
+ export function asSentence(text) {
408
+ return text.endsWith('.') ? text : `${text}.`;
409
+ }
410
+ /**
411
+ * What a diagnostic says about an unexpected value, whether or not it was an Error, with every
412
+ * control character escaped. A `DeclarationError` answers its sentence, because its message holds
413
+ * its whole diagnostic. Every sentence that quotes a thrown value reads it here, so a reason stays
414
+ * on one line and no bidirectional control reaches a terminal, whichever rule's sentence carries
415
+ * it, while the author's words around it keep their line breaks. Reading it never throws: an Error
416
+ * whose message is not a string or cannot be read, and a value whose prototype cannot be read, such
417
+ * as a proxy whose trap throws, answer one fixed sentence.
418
+ */
214
419
  export function reasonOf(thrown) {
215
- return thrown instanceof Error ? thrown.message : 'An unknown error occurred.';
420
+ return escapeControlCharacters(rawReasonOf(thrown));
421
+ }
422
+ /** The thrown value's reason as it was written, read without throwing. */
423
+ function rawReasonOf(thrown) {
424
+ const unreadableReason = 'The thrown value has no readable message.';
425
+ try {
426
+ if (!(thrown instanceof Error)) {
427
+ return 'An unknown error occurred.';
428
+ }
429
+ if (thrown instanceof DeclarationError) {
430
+ return thrown.sentence;
431
+ }
432
+ const { message } = thrown;
433
+ return typeof message === 'string' ? message : unreadableReason;
434
+ }
435
+ catch {
436
+ return unreadableReason;
437
+ }
216
438
  }
217
439
  /**
218
440
  * Why a returned value is not the text a view owes. A view is synchronous, so a returned promise is
219
- * a non-string return like any other, and its rejection is adopted and swallowed here: an
220
- * unobserved rejection would end the process before the invocation could report anything.
441
+ * a non-string return like any other: it receives a rejection handler and is otherwise ignored.
221
442
  */
222
443
  export function notTextReason(value) {
223
- void Promise.resolve(value).catch(() => undefined);
444
+ if (isThenable(value)) {
445
+ ignoreRejection(value);
446
+ }
224
447
  return `The view returned ${typeof value} instead of a string.`;
225
448
  }
226
- /** Every thrown value reaches reporting as a failure class; anything else is internal. */
449
+ /**
450
+ * Whether a thrown value is a failure class by its prototype chain, read defensively, because a
451
+ * revoked proxy or a proxy whose prototype trap throws answers no chain. Such a value is foreign.
452
+ */
453
+ function isLoomError(thrown) {
454
+ try {
455
+ return thrown instanceof LoomError;
456
+ }
457
+ catch {
458
+ return false;
459
+ }
460
+ }
461
+ /**
462
+ * Every thrown value reaches reporting as a failure class; anything else is internal. A value that
463
+ * inherits from a failure class without having been constructed holds no code, so it is internal
464
+ * too.
465
+ */
227
466
  export function toFailure(thrown) {
228
- return thrown instanceof LoomError ? thrown : new InternalError(reasonOf(thrown), thrown);
467
+ if (!isLoomError(thrown)) {
468
+ return foreignFailure(thrown);
469
+ }
470
+ return failureCodes.has(thrown)
471
+ ? thrown
472
+ : new InternalError(unconstructedFailure, {
473
+ cause: thrown,
474
+ correction: 'Construct the failure with new before throwing it.',
475
+ sentence: 'A thrown value inherits from a failure class but was never constructed as one.',
476
+ });
477
+ }
478
+ /**
479
+ * The defect a foreign throw reports: its reason as the sentence, and the thrown value as the
480
+ * cause a development build's diagnostic shows.
481
+ */
482
+ export function foreignFailure(thrown) {
483
+ return new InternalError(foreignThrow, {
484
+ cause: thrown,
485
+ correction: foreignThrowCorrection,
486
+ sentence: reasonOf(thrown),
487
+ });
488
+ }
489
+ // Core's own classes are captured now, before any application code can write their statics.
490
+ for (const Class of [
491
+ LoomError,
492
+ UsageError,
493
+ InputError,
494
+ UnknownCommandError,
495
+ NonCallableCommandError,
496
+ UnexpectedArgumentError,
497
+ UnknownOptionError,
498
+ MissingValueError,
499
+ UnexpectedValueError,
500
+ RepeatedOptionError,
501
+ ShortGroupError,
502
+ DeclarationError,
503
+ FatalError,
504
+ InternalError,
505
+ ResultError,
506
+ ]) {
507
+ classCodes.set(Class, Class.exitCode);
229
508
  }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * The first code no failure class may declare. 126 and 127 belong to the shell, and 128 plus a
3
+ * signal number reports a signal, which covers 130 and 143.
4
+ */
5
+ declare const firstReserved = 126;
6
+ /** Every whole number below `Bound`, counted up from 0, as one union of literal types. */
7
+ type WholeNumbersBelow<Bound extends number, Counted extends number[] = []> = Counted['length'] extends Bound ? Counted[number] : WholeNumbersBelow<Bound, [...Counted, Counted['length']]>;
8
+ /** The codes a failure class may declare, 1 through 125. 0 is success, and the rest are reserved. */
9
+ export type FailureExitCode = Exclude<WholeNumbersBelow<typeof firstReserved>, 0>;
10
+ /**
11
+ * Whether one declared value is a code a failure class may exit with. A JavaScript class, or one
12
+ * that escaped the type check, may declare any value at all, so the check reads it as unknown.
13
+ */
14
+ export declare function isFailureExitCode(value: unknown): value is FailureExitCode;
15
+ /** The command was used incorrectly. */
16
+ export declare const EX_USAGE = 64;
17
+ /** The input data was incorrect. */
18
+ export declare const EX_DATAERR = 65;
19
+ /** An input file did not exist or could not be read. */
20
+ export declare const EX_NOINPUT = 66;
21
+ /** An addressee is unknown. */
22
+ export declare const EX_NOUSER = 67;
23
+ /** A host name is unknown. */
24
+ export declare const EX_NOHOST = 68;
25
+ /** A service is unavailable. */
26
+ export declare const EX_UNAVAILABLE = 69;
27
+ /** An internal software error. */
28
+ export declare const EX_SOFTWARE = 70;
29
+ /** An operating system error. */
30
+ export declare const EX_OSERR = 71;
31
+ /** A system file is missing or malformed. */
32
+ export declare const EX_OSFILE = 72;
33
+ /** An output file cannot be created. */
34
+ export declare const EX_CANTCREAT = 73;
35
+ /** An input or output error. */
36
+ export declare const EX_IOERR = 74;
37
+ /** A temporary failure; a retry may succeed. */
38
+ export declare const EX_TEMPFAIL = 75;
39
+ /** A remote system broke the protocol. */
40
+ export declare const EX_PROTOCOL = 76;
41
+ /** Permission is insufficient. */
42
+ export declare const EX_NOPERM = 77;
43
+ /** The configuration is wrong. */
44
+ export declare const EX_CONFIG = 78;
45
+ export {};
@@ -0,0 +1,46 @@
1
+ /**
2
+ * The first code no failure class may declare. 126 and 127 belong to the shell, and 128 plus a
3
+ * signal number reports a signal, which covers 130 and 143.
4
+ */
5
+ const firstReserved = 126;
6
+ /**
7
+ * Whether one declared value is a code a failure class may exit with. A JavaScript class, or one
8
+ * that escaped the type check, may declare any value at all, so the check reads it as unknown.
9
+ */
10
+ export function isFailureExitCode(value) {
11
+ return (typeof value === 'number' && Number.isInteger(value) && value >= 1 && value < firstReserved);
12
+ }
13
+ // The failure codes BSD's `sysexits.h` names, as flat constants with literal types.
14
+ // A failure class declares one by name: `static override readonly exitCode = EX_UNAVAILABLE;`.
15
+ // `EX_OK` is not here, because no failure declares 0.
16
+ // Core raises invalid input as 2 and never raises `EX_USAGE`.
17
+ /** The command was used incorrectly. */
18
+ export const EX_USAGE = 64;
19
+ /** The input data was incorrect. */
20
+ export const EX_DATAERR = 65;
21
+ /** An input file did not exist or could not be read. */
22
+ export const EX_NOINPUT = 66;
23
+ /** An addressee is unknown. */
24
+ export const EX_NOUSER = 67;
25
+ /** A host name is unknown. */
26
+ export const EX_NOHOST = 68;
27
+ /** A service is unavailable. */
28
+ export const EX_UNAVAILABLE = 69;
29
+ /** An internal software error. */
30
+ export const EX_SOFTWARE = 70;
31
+ /** An operating system error. */
32
+ export const EX_OSERR = 71;
33
+ /** A system file is missing or malformed. */
34
+ export const EX_OSFILE = 72;
35
+ /** An output file cannot be created. */
36
+ export const EX_CANTCREAT = 73;
37
+ /** An input or output error. */
38
+ export const EX_IOERR = 74;
39
+ /** A temporary failure; a retry may succeed. */
40
+ export const EX_TEMPFAIL = 75;
41
+ /** A remote system broke the protocol. */
42
+ export const EX_PROTOCOL = 76;
43
+ /** Permission is insufficient. */
44
+ export const EX_NOPERM = 77;
45
+ /** The configuration is wrong. */
46
+ export const EX_CONFIG = 78;