@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
@@ -1,17 +1,26 @@
1
1
  import { runInvocation } from './chain.js';
2
- import { attachChild, buildGraph, collectInputs, declareAction, declareExtensions, declareArgument, declareOption, declareResult, declareResultViews, freshState, recordGlobalOption, } from './command.js';
3
- import { DeclarationError, InternalError, reasonOf, toFailure } from './errors.js';
4
- import { checkDescription, checkNoListingFacts, checkVersion, isPlainObject } from './facts.js';
5
- import { declareGlobalOption, emptyGlobals } from './globals.js';
2
+ import { portableName } from './command-rules.js';
3
+ import { attachToRoot, callArguments, childNode, buildGraph, checkDeclaredOptions, collectInputs, commandPlacement, inputPlaces, declareAction, declareExtensions, declareArgument, declareOption, declareResult, declareResultViews, freshState, isPortableName, layerOf, portableNameCorrection, } from './command.js';
4
+ import { escapeControlCharacters } from './controls.js';
5
+ import { DeclarationError, exitCodeOf, InternalError, quoted, reasonOf, toFailure, } from './errors.js';
6
+ import { storeCommandLayers } from './extension.js';
7
+ import { checkDescription, checkNoListingFacts, checkVersion, partFinding, slotSite, } from './facts.js';
8
+ import { declareGlobalOption, emptyGlobals, globalSite, globalTable } from './globals.js';
9
+ import { destinationReport, reportFailure } from './hints.js';
6
10
  import { captureHost } from './host.js';
11
+ import { globalOptionAfterCommand } from './input-rules.js';
7
12
  import { inspectGraph } from './inspect.js';
8
13
  import { coreViews } from './lanes.js';
9
14
  import { Output, reportPlainly } from './output.js';
10
- import { buildPlugins, installPlugins, ownedSignals, pluginSentence } from './plugin.js';
15
+ import { isPlainObject } from './plain.js';
16
+ import { invalidPacket, notAnObject, retiredApplicationOption } from './plugin-rules.js';
17
+ import { installPlugins, ownedSignals, pluginViews } from './plugin.js';
11
18
  import { renderingPolicy } from './rendering.js';
19
+ import { brokenOutputView, runOptions, viewCorrection } from './rules.js';
12
20
  import { bracketRun, cancellationCode, isCancellationEcho } from './signals.js';
13
- import { captureConfig, checkDeclarations, prepareInputs } from './validation.js';
14
- import { buildViews, describeFailure, viewIdentities } from './view.js';
21
+ import { readTranslations, translateThrow } from './translators.js';
22
+ import { checkDeclarations, prepareInputs } from './validation.js';
23
+ import { buildViews, viewIdentities } from './view.js';
15
24
  /** The registry a failure is reported through when the application's own could not be built. */
16
25
  const noViews = [];
17
26
  /**
@@ -26,6 +35,16 @@ function silenced(thrown, signal, cancelled) {
26
35
  * `undefined` is itself throwable, so the absence is spelled here rather than borrowed from it.
27
36
  */
28
37
  const noPrimary = Symbol('no primary');
38
+ /**
39
+ * The write state a run reports by. A destination whose write failure the action let propagate,
40
+ * and a translator answered, is reported as that translated failure, so its failure is no longer
41
+ * the destination fault that forces 1.
42
+ */
43
+ function answeredWrite(writes, answered) {
44
+ return writes.kind === 'failed' && answered !== undefined && writes.error === answered
45
+ ? { kind: 'ok' }
46
+ : writes;
47
+ }
29
48
  /**
30
49
  * Whether the primary outcome carries one recorded cause already: the value itself, or a failure
31
50
  * that wraps it at any depth, which an action that caught a source failure and rethrew its own
@@ -53,10 +72,31 @@ function checkSignal(signal) {
53
72
  return undefined;
54
73
  }
55
74
  if (!(signal instanceof AbortSignal)) {
56
- throw new InternalError('run() received a signal that is not an AbortSignal. Supply the signal of an AbortController.', undefined);
75
+ throw new InternalError(runOptions, {
76
+ cause: undefined,
77
+ correction: 'Supply the signal of an AbortController.',
78
+ sentence: 'run() received a signal that is not an AbortSignal.',
79
+ });
57
80
  }
58
81
  return signal;
59
82
  }
83
+ /**
84
+ * Whether a throw in a cancelled run echoes its cancellation, so no translator is offered it. A
85
+ * value that cannot be read, such as an Error whose `name` getter throws, counts as an echo, so
86
+ * it keeps its cancellation code and no translator replaces it.
87
+ */
88
+ function echoesCancellation(thrown, controller) {
89
+ const { signal } = controller;
90
+ if (!signal.aborted) {
91
+ return false;
92
+ }
93
+ try {
94
+ return isCancellationEcho(thrown, signal.reason);
95
+ }
96
+ catch {
97
+ return true;
98
+ }
99
+ }
60
100
  /**
61
101
  * The Application holds the unnamed root's declaration state and applies the same transitions a
62
102
  * Command does, so each declaration call has one typed implementation and no builder to recover.
@@ -64,31 +104,20 @@ function checkSignal(signal) {
64
104
  class ApplicationBuilder {
65
105
  #name;
66
106
  #root;
67
- // The override list the constructor read out of the options slot, unexamined until build.
68
- #views;
69
- #plugins;
70
107
  #globals;
71
- // The constructor's raw options argument, kept for the slot's own shape rules.
72
- // The options-slot rules answer at the same point every other authoring fault does:
73
- // `inspect()` and `run()`.
74
- #options;
75
- // The facts the constructor read out of that slot, unexamined until build.
76
- #declared;
77
- constructor(name, root, config) {
78
- this.#declared = config.declared;
79
- this.#views = config.views;
108
+ #config;
109
+ constructor(name, declared) {
80
110
  this.#name = name;
81
- this.#options = config.options;
82
- this.#plugins = config.plugins;
83
- this.#globals = config.globals;
84
- this.#root = root;
111
+ this.#config = declared.config;
112
+ this.#globals = declared.globals;
113
+ this.#root = declared.root;
85
114
  }
86
115
  get name() {
87
116
  return this.#name;
88
117
  }
89
118
  argument(name, config) {
90
119
  const input = {
91
- config: captureConfig(config),
120
+ config,
92
121
  kind: 'argument',
93
122
  name,
94
123
  };
@@ -96,33 +125,49 @@ class ApplicationBuilder {
96
125
  }
97
126
  option(name, config) {
98
127
  const input = {
99
- config: captureConfig(config),
128
+ config,
100
129
  kind: 'option',
101
130
  name,
102
131
  };
103
- return this.derive(declareOption(this.#root, input));
132
+ return this.derive(declareOption(this.#root, input, this.table()));
104
133
  }
105
134
  globalOption(name, config) {
135
+ // The plugins' Commands attach at construction, so only the application's own calls close it.
136
+ if (this.#config.composed) {
137
+ throw new DeclarationError(globalOptionAfterCommand, {
138
+ correction: 'Declare global options before attaching Commands or registering an action.',
139
+ findings: [
140
+ { arguments: callArguments(name, config), call: 'globalOption', mark: '0', path: [] },
141
+ ],
142
+ sentence: `The Application declares global option ${quoted(name)} after command() or action().`,
143
+ });
144
+ }
106
145
  const input = {
107
- config: captureConfig(config),
146
+ config,
108
147
  kind: 'option',
109
148
  name,
110
149
  };
111
- return new ApplicationBuilder(this.#name, recordGlobalOption(this.#root, name), {
112
- declared: this.#declared,
113
- globals: declareGlobalOption(this.#globals, input),
114
- options: this.#options,
115
- plugins: this.#plugins,
116
- views: this.#views,
117
- });
150
+ const descriptors = new Map(this.#root.descriptors);
151
+ const { input: captured, state: globals } = declareGlobalOption(this.#globals, input, descriptors);
152
+ const root = { ...this.#root, descriptors };
153
+ checkDeclaredOptions(root, globalTable(globals.inputs, this.#config.plugins));
154
+ checkDeclarations([{ input: captured, site: globalSite(captured) }]);
155
+ return new ApplicationBuilder(this.#name, { config: this.#config, globals, root });
118
156
  }
119
157
  /** Registering the action closes input authoring; extension configuration remains available. */
120
158
  action(handler) {
121
- return this.derive(declareAction(this.#root, handler));
159
+ return this.derive(declareAction(this.#root, handler), { composed: true });
122
160
  }
123
161
  /** A child arrives in any type state, because its own action is the call that finished it. */
124
162
  command(child) {
125
- return this.derive(attachChild(this.#root, child));
163
+ const scope = {
164
+ descriptors: new Map(this.#root.descriptors),
165
+ owners: new Map(this.#config.owners),
166
+ table: this.table(),
167
+ };
168
+ const node = childNode(null, child);
169
+ const root = attachToRoot(this.#root, { node, placement: commandPlacement([], node.name) }, scope);
170
+ return this.derive(root, { composed: true, owners: scope.owners });
126
171
  }
127
172
  /**
128
173
  * The value the root action produces for its consumer. The type argument is stated by the
@@ -142,50 +187,38 @@ class ApplicationBuilder {
142
187
  extend(...values) {
143
188
  return this.derive(declareExtensions(this.#root, values));
144
189
  }
190
+ /** The globals table the root's options and every joining subtree meet. */
191
+ table() {
192
+ return globalTable(this.#globals.inputs, this.#config.plugins);
193
+ }
145
194
  /**
146
- * Root declaration calls preserve the Application configuration. The next state
147
- * travels through this call: each method names its transition in its return type, and the
148
- * wrapper publishes the same runtime value in exactly that state.
195
+ * Root declaration calls preserve the Application configuration, with the parts a call changed.
196
+ * The next state travels through this call: each method names its transition in its return type,
197
+ * and the wrapper publishes the same runtime value in exactly that state.
149
198
  */
150
- derive(root) {
151
- return new ApplicationBuilder(this.#name, root, {
152
- declared: this.#declared,
153
- globals: this.#globals,
154
- options: this.#options,
155
- plugins: this.#plugins,
156
- views: this.#views,
157
- });
199
+ derive(root, changed = {}) {
200
+ return new ApplicationBuilder(this.#name, { config: { ...this.#config, ...changed }, globals: this.#globals, root });
158
201
  }
159
202
  /**
160
- * Every rule that reads the declarations alone, in the order `run()` reads them: the
161
- * application's own view overrides, the options slot, the installed list, each plugin's
162
- * declarations, then the whole Command graph. The application's overrides are read and published
163
- * first, because they need core's identities and nothing else, so every later declaration error
164
- * reaches them, while a fault in that list itself reports through core's own text. The merged
165
- * registry is published once the whole build has succeeded, so a build-time fault never resolves
166
- * through a plugin's overrides, which build has not yet validated.
203
+ * The graph build, which applies the rules no earlier moment could know: the root's
204
+ * finished-Command rules and every lifecycle hook's contribution. The application's own overrides
205
+ * are published first, so a build fault reports through them. The merged registry is published
206
+ * once the build has succeeded, so a build fault never resolves through a plugin's overrides.
167
207
  */
168
208
  prepare(stage) {
169
- const identities = viewIdentities(coreViews);
170
- const application = buildViews({ declares: false, sentence: 'The Application' }, this.#views, identities);
171
- stage.views([application]);
172
- stage.rendering(renderingPolicy(this.#declared.rendering));
173
- const facts = checkOptions(this.#options, this.#declared);
174
- const installed = installPlugins(this.#plugins ?? []);
175
- const install = { descriptors: new Map(), extensions: new Map() };
176
- const plugins = buildPlugins(installed, install);
209
+ const { contributors, facts, plugins, rendering, views } = this.#config;
210
+ stage.views([views]);
211
+ stage.rendering(rendering);
177
212
  stage.plugins(plugins);
178
- const contributors = plugins.map((entry) => buildViews({ declares: true, sentence: pluginSentence(entry.identity) }, entry.views, identities));
179
- const graph = buildGraph(this.#root, this.#globals, { ...install, plugins });
180
- checkDeclarations([...graph.globals.inputs, ...collectInputs(graph.root)]);
181
- stage.views([application, ...contributors]);
213
+ const graph = buildGraph(this.#root, this.#globals, plugins);
214
+ stage.views([views, ...contributors]);
182
215
  return { facts, graph, plugins };
183
216
  }
184
217
  /**
185
- * The built graph as plain, frozen data. It applies every rule `run()` applies without a schema,
186
- * in the order `run()` applies them, and throws `DeclarationError` when one fails. Validating a
187
- * declared default through its schema can be asynchronous, so that one rule stays in `run()`.
188
- * Nothing is cached: each call builds the graph anew.
218
+ * The built graph as plain, frozen data. It builds the graph as `run()` does and throws
219
+ * `DeclarationError` for the same build faults. Validating a declared default through its schema
220
+ * can be asynchronous, so that one rule stays in `run()`. Nothing is cached: each call builds the
221
+ * graph anew.
189
222
  */
190
223
  inspect() {
191
224
  const built = this.prepare({
@@ -193,7 +226,10 @@ class ApplicationBuilder {
193
226
  rendering: () => undefined,
194
227
  views: () => undefined,
195
228
  });
196
- return inspectGraph(this.#name, built.graph, built.facts);
229
+ return inspectGraph(this.#name, built.graph, {
230
+ ...built.facts,
231
+ development: this.#config.development,
232
+ });
197
233
  }
198
234
  async run(options) {
199
235
  let stderr = process.stderr;
@@ -206,6 +242,27 @@ class ApplicationBuilder {
206
242
  const faults = [];
207
243
  // The failure this run reports as its primary outcome, so nothing reports it a second time.
208
244
  let primary = noPrimary;
245
+ // Each failure a translator answered, keyed to the foreign throw it replaced.
246
+ const translatedFrom = new Map();
247
+ // Where a failure happened: the path routing walked, and what the hooks read once the graph built.
248
+ let walked = Object.freeze([]);
249
+ let reached = undefined;
250
+ // The host a failure's report reads, once it is captured; before that, the process's own.
251
+ let reportHost = undefined;
252
+ const scene = () => ({
253
+ application: this.#name,
254
+ built: reached,
255
+ host: (reportHost ??= captureHost(undefined, stderr)),
256
+ path: walked,
257
+ });
258
+ // What this run's build decides about its reports, shared by every report the run writes.
259
+ const build = {
260
+ development: this.#config.development,
261
+ generic: false,
262
+ reported: false,
263
+ };
264
+ // What broke the run's reporting: a destination's write error, or a throw while reporting.
265
+ let reportingCause = undefined;
209
266
  // One private controller per run, subscribed to the caller's signal at run entry.
210
267
  const controller = new AbortController();
211
268
  /**
@@ -220,6 +277,20 @@ class ApplicationBuilder {
220
277
  const reason = graphBuilt ? signals?.reason() : undefined;
221
278
  return reason ? cancellationCode(reason) : undefined;
222
279
  };
280
+ /**
281
+ * Offers one throw from the application's work to the translators. A view's failure and a
282
+ * cancellation echo are never offered, because each already names what it is.
283
+ */
284
+ const offer = (thrown) => {
285
+ if (output?.raisedByView(thrown) === true || echoesCancellation(thrown, controller)) {
286
+ return undefined;
287
+ }
288
+ const failure = translateThrow(this.#config.translators, thrown);
289
+ if (failure !== undefined) {
290
+ translatedFrom.set(failure, thrown);
291
+ }
292
+ return failure;
293
+ };
223
294
  /**
224
295
  * Every exit path of the run leaves through the removal below, the one place it is written,
225
296
  * so no listener this run installed outlives it however the run ends.
@@ -229,10 +300,10 @@ class ApplicationBuilder {
229
300
  const overrides = options?.host;
230
301
  stderr = overrides?.stderr ?? stderr;
231
302
  const host = captureHost(overrides, stderr);
303
+ reportHost = host;
232
304
  const invocationOutput = new Output(host, controller.signal);
233
305
  output = invocationOutput;
234
- // The policy is read inside the build, after the application's own overrides are published.
235
- // A faulty rendering declaration then reports through the view the application listed.
306
+ // The constructor validated the declared policy, which the build hands over after the overrides.
236
307
  let policy = {};
237
308
  signals = bracketRun(controller, checkSignal(options?.signal));
238
309
  const built = this.prepare({
@@ -240,7 +311,8 @@ class ApplicationBuilder {
240
311
  invocationOutput.configure(policy, plugins.find((entry) => entry.theme !== undefined)?.theme ?? new Map());
241
312
  },
242
313
  rendering: (declared) => {
243
- policy = { ...declared, ...renderingPolicy(options?.rendering) };
314
+ const rendering = options?.rendering;
315
+ policy = { ...declared, ...renderingPolicy(rendering, runRendering(rendering)) };
244
316
  invocationOutput.configure(policy, new Map());
245
317
  },
246
318
  views: (value) => {
@@ -249,8 +321,17 @@ class ApplicationBuilder {
249
321
  },
250
322
  });
251
323
  const { graph } = built;
324
+ // The graph `inspect()` returns, built at most once for the run, whoever reads it first.
325
+ let inspectedGraph = undefined;
326
+ const { development } = this.#config;
327
+ const inspected = () => (inspectedGraph ??= inspectGraph(this.#name, graph, { ...built.facts, development }));
328
+ reached = { inspected, plugins: built.plugins };
329
+ if (development) {
330
+ // A development build asks every converter at build, so its check runs on every run.
331
+ inspected();
332
+ }
252
333
  const inputs = { globals: graph.globals.inputs, locals: collectInputs(graph.root) };
253
- const defaults = await prepareInputs(inputs, host);
334
+ const defaults = await prepareInputs(inputs, host, inputPlaces(graph));
254
335
  graphBuilt = true;
255
336
  if (!controller.signal.aborted) {
256
337
  /**
@@ -261,17 +342,19 @@ class ApplicationBuilder {
261
342
  await runInvocation({
262
343
  channel: (binding) => invocationOutput.channel(binding),
263
344
  defaults,
264
- facts: built.facts,
265
345
  graph,
266
346
  host,
267
- name: this.#name,
347
+ inspected,
348
+ offer,
268
349
  out: output.out,
269
350
  plugins: built.plugins,
270
351
  report: (fault) => faults.push(fault),
271
352
  route: (path) => {
353
+ walked = path;
272
354
  invocationOutput.useRoute(path);
273
355
  },
274
356
  signal: controller.signal,
357
+ sourceOut: output.sourceOut,
275
358
  style: output.style,
276
359
  });
277
360
  }
@@ -281,80 +364,96 @@ class ApplicationBuilder {
281
364
  const fault = output.fault;
282
365
  if (fault) {
283
366
  // The action returned, so the view failure is this invocation's own failure.
284
- throw new InternalError(`Rendering output failed: ${reasonOf(fault.cause)}`, fault.cause);
367
+ throw new InternalError(brokenOutputView, {
368
+ cause: fault.cause,
369
+ correction: viewCorrection,
370
+ sentence: `Rendering output failed: ${reasonOf(fault.cause)}`,
371
+ });
285
372
  }
286
373
  }
287
374
  catch (error) {
288
375
  primary = error;
289
376
  try {
290
377
  const failure = toFailure(error);
291
- code = failure.exitCode;
378
+ code = exitCodeOf(failure);
292
379
  output ??= new Output(captureHost(undefined, stderr), controller.signal);
293
- const writes = await output.settle();
380
+ const writes = answeredWrite(await output.settle(), translatedFrom.get(failure));
294
381
  if (writes.kind === 'ok' && !silenced(error, controller.signal, cancellation())) {
295
- const report = describeFailure(registry ?? noViews, failure, output.context('stderr'));
296
- if (report.kind === 'rendered') {
297
- // The view owns the trailing newline; output resolves its marked text.
298
- await output.report(report.text);
299
- }
300
- else {
382
+ // A broken failure view or onFailure hook forces 1 over the failure's own code.
383
+ const sink = { build, output, registry: registry ?? noViews, stderr };
384
+ if (await reportFailure(sink, failure, scene())) {
301
385
  code = 1;
302
- // `report.text` is core's default text, which already ends in `\n`.
303
- await reportPlainly(stderr, `${report.text}Internal error: Rendering the failure failed: ${report.reason}\n`);
304
386
  }
305
387
  }
306
388
  }
307
- catch {
389
+ catch (reportError) {
308
390
  code = 1;
309
391
  reportingFailed = true;
392
+ reportingCause = reportError;
310
393
  }
311
394
  }
312
395
  /**
313
396
  * A sequence that stopped on its own source reports the same way: the call the action never
314
397
  * awaited observed nothing, and a failure the action let propagate is the primary outcome
315
- * already, so the one it raised is not reported twice.
398
+ * already, so the one it raised is not reported twice. A throw a translator replaced is
399
+ * carried by the failure it became, whether or not that failure keeps it as its cause. A
400
+ * foreign throw that is not carried is a deferred fault, offered to the translators here,
401
+ * where core would otherwise wrap it as an internal error.
316
402
  */
403
+ // A primary no translator answered replaced nothing, so even a thrown `undefined` is reported.
404
+ const replaced = translatedFrom.get(primary) ?? noPrimary;
405
+ const deferred = new Set();
317
406
  for (const cause of output?.stopped ?? []) {
318
- if (!carried(primary, cause)) {
319
- faults.push(toFailure(cause));
407
+ if (!carried(primary, cause) && cause !== replaced) {
408
+ // A failure is never offered, so only a foreign throw can be translated here.
409
+ const translated = offer(cause);
410
+ if (translated !== undefined) {
411
+ deferred.add(translated);
412
+ }
413
+ faults.push(translated ?? toFailure(cause));
320
414
  }
321
415
  }
322
416
  // A plugin's own fault is reported after the primary outcome and turns a would-be 0 into 1.
417
+ // A deferred fault a translator answered turns it into that failure's own code instead.
323
418
  // The primary outcome keeps its code, the way a view failure leaves it alone.
324
419
  // It is reported the way the primary failure is, so an override answers its class.
325
420
  for (const fault of faults) {
326
421
  if (!silenced(fault, controller.signal, cancellation())) {
327
- code = code === 0 ? 1 : code;
422
+ const own = deferred.has(fault) ? exitCodeOf(fault) : 1;
423
+ code = code === 0 ? own : code;
328
424
  try {
329
- const report = describeFailure(registry ?? noViews, fault, output?.context('stderr'));
330
- if (report.kind === 'rendered') {
331
- await output?.report(report.text);
332
- }
333
- else {
425
+ const sink = output && { build, output, registry: registry ?? noViews, stderr };
426
+ if (sink && (await reportFailure(sink, fault, scene()))) {
334
427
  code = 1;
335
- await reportPlainly(stderr, `${report.text}Internal error: Rendering the failure failed: ${report.reason}\n`);
336
428
  }
337
429
  }
338
- catch {
430
+ catch (reportError) {
339
431
  reportingFailed = true;
432
+ reportingCause ??= reportError;
340
433
  }
341
434
  }
342
435
  }
343
436
  if (output) {
344
- const writes = await output.settle();
437
+ const writes = answeredWrite(await output.settle(), translatedFrom.get(primary));
345
438
  if (writes.kind === 'failed') {
346
439
  code = 1;
347
440
  reportingFailed = true;
441
+ reportingCause ??= writes.error;
348
442
  }
349
443
  output.dispose();
350
444
  }
351
445
  if (reportingFailed) {
352
- await reportPlainly(stderr, 'Internal error: Could not write invocation output.\n');
446
+ const { application, host } = scene();
447
+ const text = destinationReport(build, reportingCause, { application, host });
448
+ if (text !== '') {
449
+ await reportPlainly(stderr, text);
450
+ }
353
451
  }
354
452
  /**
355
453
  * One rule orders every code: a cancelled run resolves its signal's code, and a broken
356
- * failure view or destination in that run is reported as text without changing it. The
357
- * signal decides the code whatever the action did afterward, so this reading comes last.
454
+ * failure view, onFailure hook, or destination in that run is reported as text without
455
+ * changing it. The signal decides the code whatever the action did afterward, so this
456
+ * reading comes last.
358
457
  */
359
458
  code = cancellation() ?? code;
360
459
  process.exitCode = code;
@@ -365,59 +464,167 @@ class ApplicationBuilder {
365
464
  }
366
465
  }
367
466
  }
467
+ /** The retired Application options, in the order they are rejected, each with its fix. */
468
+ const retired = [
469
+ ['globals', 'Declare them with globalOption(name, config).'],
470
+ ['failures', 'Declare view overrides under views with override(key, view).'],
471
+ ];
472
+ /** Where one Application option sits, rebuilt as `new Application(name, { key })`. */
473
+ function optionSite(name, key, value) {
474
+ return slotSite({ call: 'new Application', named: name, subject: 'The Application' }, key, value);
475
+ }
476
+ /** Where `run()`'s own rendering policy sits: the options object of the call on the Application. */
477
+ function runRendering(rendering) {
478
+ return {
479
+ at: '0.rendering',
480
+ declaration: { arguments: [{ rendering }], call: 'run', path: [] },
481
+ subject: 'The run',
482
+ };
483
+ }
368
484
  /** Reject obsolete wiring before silently losing options that invocations depend on. */
369
- function checkOptions(options, declared) {
370
- if (options !== undefined) {
371
- if (!isPlainObject(options)) {
372
- throw new DeclarationError('The Application options must be an object. Supply an Application options object.');
373
- }
374
- if ('globals' in options) {
375
- throw new DeclarationError('The Application options contain globals. Declare them with globalOption(name, config).');
376
- }
377
- if ('failures' in options) {
378
- throw new DeclarationError('The Application options contain failures. Declare view overrides under views with override(key, view).');
485
+ function checkOptions(name, options) {
486
+ const site = {
487
+ at: '1',
488
+ declaration: { arguments: callArguments(name, options), call: 'new Application' },
489
+ subject: 'The Application',
490
+ };
491
+ if (options === undefined) {
492
+ return { description: undefined, version: checkVersion(site, undefined) };
493
+ }
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
+ for (const [key, correction] of retired) {
502
+ if (key in options) {
503
+ const retiredSite = optionSite(name, key, Reflect.get(options, key));
504
+ throw new DeclarationError(retiredApplicationOption, {
505
+ correction,
506
+ findings: [partFinding(retiredSite, [])],
507
+ sentence: `The Application options contain ${key}.`,
508
+ });
379
509
  }
380
- // The root is every page's entry point, so it carries neither listing fact.
381
- // A key that may not be there is a fault of the slot, so it answers with the slot's shape.
382
- checkNoListingFacts('The Application', options);
510
+ }
511
+ // The root is every page's entry point, so it carries neither listing fact.
512
+ // A key that may not be there is a fault of the slot, so it answers with the slot's shape.
513
+ checkNoListingFacts(site, options);
514
+ return {
515
+ description: checkDescription(site, options.description),
516
+ version: checkVersion(site, options.version),
517
+ };
518
+ }
519
+ /**
520
+ * Whether the packet an Application received reads `development`. No packet is distributed, so an
521
+ * application that never opted in cannot show an operator the author's detail. The build is read
522
+ * once, here, so a later change to the imported object changes no run.
523
+ */
524
+ function readPacket(name, packet) {
525
+ if (packet === undefined) {
526
+ return false;
527
+ }
528
+ const site = optionSite(name, 'packet', packet);
529
+ if (!isPlainObject(packet)) {
530
+ throw new DeclarationError(invalidPacket, {
531
+ correction: 'Import loom.packet.json and pass it as packet.',
532
+ findings: [partFinding(site, [])],
533
+ sentence: 'The Application packet must be an object.',
534
+ });
535
+ }
536
+ const { build } = packet;
537
+ if (build === 'development' || build === 'distributed') {
538
+ return build === 'development';
539
+ }
540
+ const found = build === undefined
541
+ ? 'The packet has no build.'
542
+ : `The packet's build is ${typeof build === 'string' ? `"${escapeControlCharacters(build)}"` : 'not a string'}.`;
543
+ throw new DeclarationError(invalidPacket, {
544
+ correction: 'Set build to "development" or "distributed".',
545
+ findings: [partFinding(site, 'build' in packet ? ['build'] : [])],
546
+ sentence: found,
547
+ });
548
+ }
549
+ /**
550
+ * Every rule `new Application(name, options)` applies, in the order it reads the slot: the
551
+ * application's own view overrides, its translations, the rendering policy, the options slot and
552
+ * its facts, the installed list and every rule between two plugins, the root's extension values,
553
+ * and then each plugin's Commands, which attach to the root first, in installation order and list
554
+ * order.
555
+ */
556
+ function declareApplication(name, options) {
557
+ const slot = isPlainObject(options) ? options : undefined;
558
+ const identities = viewIdentities(coreViews);
559
+ const views = buildViews({
560
+ declares: false,
561
+ owner: { call: 'new Application', named: name },
562
+ sentence: 'The Application',
563
+ }, slot?.views, identities);
564
+ const translations = readTranslations(optionSite(name, 'translators', slot?.translators), slot?.translators);
565
+ const rendering = renderingPolicy(slot?.rendering, optionSite(name, 'rendering', slot?.rendering));
566
+ const facts = checkOptions(name, options);
567
+ const development = readPacket(name, slot?.packet);
568
+ const installed = installPlugins(name, slot?.plugins ?? []);
569
+ const { plugins } = installed;
570
+ const contributors = plugins.map((entry) => buildViews(pluginViews(entry.identity), entry.views, identities));
571
+ const table = globalTable([], plugins);
572
+ const descriptors = installed.descriptors;
573
+ const extensions = storeCommandLayers({
574
+ descriptors,
575
+ layers: [slot?.extensions],
576
+ site: optionSite(name, 'extensions', slot?.extensions),
577
+ subject: layerOf(null),
578
+ });
579
+ // The Application checks its own facts, so the root carries none.
580
+ // Its diagnostics name the Application rather than the root Command.
581
+ let root = freshState({
582
+ descriptors,
583
+ extensions,
584
+ facts: { deprecated: undefined, description: undefined, hidden: false },
585
+ name: null,
586
+ });
587
+ const owners = new Map();
588
+ for (const command of plugins.flatMap((entry) => entry.commands)) {
589
+ root = attachToRoot(root, command, {
590
+ descriptors: new Map(root.descriptors),
591
+ owners,
592
+ table,
593
+ });
383
594
  }
384
595
  return {
385
- description: checkDescription('The Application', declared.description),
386
- version: checkVersion(declared.version),
596
+ config: {
597
+ composed: false,
598
+ contributors,
599
+ development,
600
+ facts,
601
+ owners,
602
+ plugins,
603
+ rendering,
604
+ translators: [translations, ...plugins.map((entry) => entry.translators)],
605
+ views,
606
+ },
607
+ globals: emptyGlobals(),
608
+ root,
387
609
  };
388
610
  }
611
+ /** The application name is typed as a command at the prompt, so it answers to the portable rule. */
612
+ function checkApplicationName(name) {
613
+ if (!isPortableName(name)) {
614
+ throw new DeclarationError(portableName, {
615
+ correction: portableNameCorrection,
616
+ findings: [{ arguments: [name], call: 'new Application', mark: '0' }],
617
+ sentence: `Application name ${quoted(name)} is invalid.`,
618
+ });
619
+ }
620
+ return name;
621
+ }
389
622
  /** Constructor inference preserves the installed plugin tuple; globals start empty. */
390
623
  class ApplicationDeclaration extends ApplicationBuilder {
391
624
  constructor(name, options) {
392
- // The options slot is read defensively, never inspected.
393
- // An invalid value still yields `views` and the facts of some kind.
394
- // `checkOptions` reports such a value at build.
395
- // The root's own slot and core facts stay empty, because the Application checks its own slot.
396
- // Its diagnostics name the Application rather than the root Command.
397
- super(name, freshState({
398
- deprecated: undefined,
399
- description: undefined,
400
- extensions: options?.extensions,
401
- hidden: undefined,
402
- name: null,
403
- options: undefined,
404
- }), {
405
- declared: {
406
- description: options?.description,
407
- rendering: isPlainObject(options?.rendering)
408
- ? { ...options.rendering }
409
- : options?.rendering,
410
- version: options?.version,
411
- },
412
- globals: emptyGlobals(),
413
- options,
414
- plugins: options?.plugins,
415
- // The read is loose because the slot is reachable from JavaScript with any value at all.
416
- // An Application value passed here answers `views` with its own authoring method.
417
- // The public `ApplicationOptions.views` stays exactly `readonly ViewOverride[]`.
418
- // An options slot that is no plain object carries no override list, and its own rule reports it.
419
- views: isPlainObject(options) ? options.views : undefined,
420
- });
625
+ // The arguments evaluate in order, so the name is checked before any option is read.
626
+ const checked = checkApplicationName(name);
627
+ super(checked, declareApplication(checked, options));
421
628
  }
422
629
  }
423
630
  export const Application = ApplicationDeclaration;