@loomcli/core 0.5.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 (77) hide show
  1. package/dist/application.d.ts +18 -2
  2. package/dist/application.js +266 -71
  3. package/dist/bindings.d.ts +15 -10
  4. package/dist/bindings.js +34 -15
  5. package/dist/chain.d.ts +15 -6
  6. package/dist/chain.js +40 -20
  7. package/dist/command-rules.d.ts +55 -0
  8. package/dist/command-rules.js +142 -0
  9. package/dist/command.d.ts +65 -23
  10. package/dist/command.js +695 -226
  11. package/dist/controls.d.ts +8 -0
  12. package/dist/controls.js +23 -0
  13. package/dist/defect.d.ts +18 -0
  14. package/dist/defect.js +272 -0
  15. package/dist/developer.d.ts +24 -0
  16. package/dist/developer.js +52 -0
  17. package/dist/diagnostic-text.d.ts +81 -0
  18. package/dist/diagnostic-text.js +283 -0
  19. package/dist/diagnostic.d.ts +11 -0
  20. package/dist/diagnostic.js +70 -0
  21. package/dist/errors.d.ts +108 -24
  22. package/dist/errors.js +324 -53
  23. package/dist/exit-codes.d.ts +45 -0
  24. package/dist/exit-codes.js +46 -0
  25. package/dist/extension.d.ts +41 -8
  26. package/dist/extension.js +142 -57
  27. package/dist/facts.d.ts +66 -11
  28. package/dist/facts.js +98 -22
  29. package/dist/globals.d.ts +29 -21
  30. package/dist/globals.js +115 -46
  31. package/dist/hints.d.ts +79 -0
  32. package/dist/hints.js +247 -0
  33. package/dist/host.d.ts +13 -0
  34. package/dist/host.js +43 -1
  35. package/dist/identity.d.ts +19 -0
  36. package/dist/identity.js +72 -0
  37. package/dist/index.d.ts +11 -2
  38. package/dist/index.js +5 -0
  39. package/dist/input-rules.d.ts +64 -0
  40. package/dist/input-rules.js +145 -0
  41. package/dist/inspect.d.ts +12 -4
  42. package/dist/inspect.js +88 -28
  43. package/dist/lanes.js +1 -1
  44. package/dist/locate.js +4 -4
  45. package/dist/options.d.ts +23 -2
  46. package/dist/options.js +135 -44
  47. package/dist/output.d.ts +9 -2
  48. package/dist/output.js +18 -2
  49. package/dist/plain.d.ts +6 -0
  50. package/dist/plain.js +12 -0
  51. package/dist/plugin-rules.d.ts +62 -0
  52. package/dist/plugin-rules.js +155 -0
  53. package/dist/plugin.d.ts +22 -10
  54. package/dist/plugin.js +348 -116
  55. package/dist/prototypes.d.ts +7 -0
  56. package/dist/prototypes.js +29 -0
  57. package/dist/rendering.d.ts +6 -1
  58. package/dist/rendering.js +23 -5
  59. package/dist/rules.d.ts +51 -0
  60. package/dist/rules.js +115 -0
  61. package/dist/sequence.js +6 -1
  62. package/dist/sources.d.ts +6 -4
  63. package/dist/sources.js +22 -13
  64. package/dist/style-wire.js +1 -1
  65. package/dist/style.js +1 -1
  66. package/dist/theme.d.ts +4 -0
  67. package/dist/theme.js +25 -5
  68. package/dist/thenable.d.ts +15 -0
  69. package/dist/thenable.js +29 -0
  70. package/dist/translators.d.ts +69 -0
  71. package/dist/translators.js +253 -0
  72. package/dist/types.d.ts +11 -1
  73. package/dist/validation.d.ts +44 -3
  74. package/dist/validation.js +156 -57
  75. package/dist/view.d.ts +48 -14
  76. package/dist/view.js +155 -77
  77. package/package.json +1 -1
@@ -1,10 +1,11 @@
1
- import type { AfterAction, AttachmentConstraint, AfterArgument, AfterCommand, AfterResult, Command, CommandMethod, CommandNodeHandle, CommandState, ResultMethod } from './command.js';
1
+ import type { AfterAction, AttachmentConstraint, AfterArgument, AfterCommand, AfterResult, ChildOwner, Command, CommandMethod, CommandNodeHandle, CommandState, ResultMethod } from './command.js';
2
2
  import type { ApplicationEnvironment, applicationEnvironment } from './environment.js';
3
3
  import type { ExtensionValue } from './extension.js';
4
4
  import type { GlobalsState } from './globals.js';
5
5
  import type { CommandGraph } from './inspect.js';
6
6
  import type { BuiltPlugin, Plugin } from './plugin.js';
7
7
  import type { RenderingPolicy } from './rendering.js';
8
+ import type { Translation, TranslatorRegistry } from './translators.js';
8
9
  import type { Action, ArgumentConfig, ArgumentValue, declaredTypes, DeclaredTypes, DefaultConstraint, GlobalNameConstraint, GlobalOmissionConstraint, PerValueConstraint, NameConstraint, ExitCode, OptionConfig, OptionValue, ResultViews, ResultViewsOf, RowViews, RunOptions, ValidateOmittedConstraint } from './types.js';
9
10
  import type { ViewContributions, ViewOverride } from './view.js';
10
11
  /**
@@ -14,9 +15,20 @@ import type { ViewContributions, ViewOverride } from './view.js';
14
15
  * to no bare token, so it has no name to alias.
15
16
  */
16
17
  export type ApplicationMethod = Exclude<CommandMethod, 'alias'> | 'globalOption';
18
+ /**
19
+ * The build fact file, `loom.packet.json`, that the entry imports and hands to the Application.
20
+ * `build` is typed `string`, because a JSON module types its members that way, and the Application
21
+ * constructor accepts `development` or `distributed` alone. Core ignores every other member.
22
+ */
23
+ export interface Packet {
24
+ readonly build: string;
25
+ }
17
26
  export interface ApplicationOptions<Plugins extends readonly Plugin[] = readonly Plugin[]> {
18
27
  rendering?: RenderingPolicy;
28
+ /** The packet that says whether this is a development build. With none, it is distributed. */
29
+ packet?: Packet;
19
30
  views?: readonly ViewOverride[];
31
+ translators?: readonly Translation[];
20
32
  plugins?: Plugins;
21
33
  extensions?: readonly ExtensionValue<'command'>[];
22
34
  description?: string;
@@ -29,13 +41,17 @@ export interface ApplicationOptions<Plugins extends readonly Plugin[] = readonly
29
41
  interface ApplicationConfig {
30
42
  /** Whether the application's own `command()` or `action()` has run, which closes `globalOption()`. */
31
43
  composed: boolean;
44
+ /** Whether the packet reads `development`, read once at construction. */
45
+ development: boolean;
32
46
  /** Each installed plugin's view contributions, in installation order. */
33
47
  contributors: readonly ViewContributions[];
34
48
  facts: ApplicationFacts;
35
49
  /** The parent that claimed each node the graph holds, so one value attaches at one point. */
36
- owners: ReadonlyMap<CommandNodeHandle, string | null>;
50
+ owners: ReadonlyMap<CommandNodeHandle, ChildOwner>;
37
51
  plugins: readonly BuiltPlugin[];
38
52
  rendering: RenderingPolicy;
53
+ /** The application's translations, then each installed plugin's, in resolution order. */
54
+ translators: TranslatorRegistry;
39
55
  /** The application's own view overrides. */
40
56
  views: ViewContributions;
41
57
  }
@@ -1,18 +1,26 @@
1
1
  import { runInvocation } from './chain.js';
2
- import { attachToRoot, childNode, buildGraph, checkDeclaredOptions, collectInputs, declareAction, declareExtensions, declareArgument, declareOption, declareResult, declareResultViews, freshState, isPortableName, layerOf, portableNameCorrection, } from './command.js';
3
- import { DeclarationError, InternalError, reasonOf, toFailure } from './errors.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';
4
6
  import { storeCommandLayers } from './extension.js';
5
- import { checkDescription, checkNoListingFacts, checkVersion, isPlainObject } from './facts.js';
6
- import { declareGlobalOption, emptyGlobals, globalTable } from './globals.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';
7
10
  import { captureHost } from './host.js';
11
+ import { globalOptionAfterCommand } from './input-rules.js';
8
12
  import { inspectGraph } from './inspect.js';
9
13
  import { coreViews } from './lanes.js';
10
14
  import { Output, reportPlainly } from './output.js';
11
- import { 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';
12
18
  import { renderingPolicy } from './rendering.js';
19
+ import { brokenOutputView, runOptions, viewCorrection } from './rules.js';
13
20
  import { bracketRun, cancellationCode, isCancellationEcho } from './signals.js';
14
- import { captureConfig, checkDeclarations, prepareInputs } from './validation.js';
15
- 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';
16
24
  /** The registry a failure is reported through when the application's own could not be built. */
17
25
  const noViews = [];
18
26
  /**
@@ -27,6 +35,16 @@ function silenced(thrown, signal, cancelled) {
27
35
  * `undefined` is itself throwable, so the absence is spelled here rather than borrowed from it.
28
36
  */
29
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
+ }
30
48
  /**
31
49
  * Whether the primary outcome carries one recorded cause already: the value itself, or a failure
32
50
  * that wraps it at any depth, which an action that caught a source failure and rethrew its own
@@ -54,10 +72,31 @@ function checkSignal(signal) {
54
72
  return undefined;
55
73
  }
56
74
  if (!(signal instanceof AbortSignal)) {
57
- 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
+ });
58
80
  }
59
81
  return signal;
60
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
+ }
61
100
  /**
62
101
  * The Application holds the unnamed root's declaration state and applies the same transitions a
63
102
  * Command does, so each declaration call has one typed implementation and no builder to recover.
@@ -78,7 +117,7 @@ class ApplicationBuilder {
78
117
  }
79
118
  argument(name, config) {
80
119
  const input = {
81
- config: captureConfig(config),
120
+ config,
82
121
  kind: 'argument',
83
122
  name,
84
123
  };
@@ -86,7 +125,7 @@ class ApplicationBuilder {
86
125
  }
87
126
  option(name, config) {
88
127
  const input = {
89
- config: captureConfig(config),
128
+ config,
90
129
  kind: 'option',
91
130
  name,
92
131
  };
@@ -95,18 +134,24 @@ class ApplicationBuilder {
95
134
  globalOption(name, config) {
96
135
  // The plugins' Commands attach at construction, so only the application's own calls close it.
97
136
  if (this.#config.composed) {
98
- throw new DeclarationError(`The Application declares global option "${name}" after command() or action(). Declare global options before attaching Commands or registering an action.`);
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
+ });
99
144
  }
100
145
  const input = {
101
- config: captureConfig(config),
146
+ config,
102
147
  kind: 'option',
103
148
  name,
104
149
  };
105
150
  const descriptors = new Map(this.#root.descriptors);
106
- const globals = declareGlobalOption(this.#globals, input, descriptors);
151
+ const { input: captured, state: globals } = declareGlobalOption(this.#globals, input, descriptors);
107
152
  const root = { ...this.#root, descriptors };
108
153
  checkDeclaredOptions(root, globalTable(globals.inputs, this.#config.plugins));
109
- checkDeclarations([input]);
154
+ checkDeclarations([{ input: captured, site: globalSite(captured) }]);
110
155
  return new ApplicationBuilder(this.#name, { config: this.#config, globals, root });
111
156
  }
112
157
  /** Registering the action closes input authoring; extension configuration remains available. */
@@ -120,7 +165,8 @@ class ApplicationBuilder {
120
165
  owners: new Map(this.#config.owners),
121
166
  table: this.table(),
122
167
  };
123
- const root = attachToRoot(this.#root, childNode(null, child), scope);
168
+ const node = childNode(null, child);
169
+ const root = attachToRoot(this.#root, { node, placement: commandPlacement([], node.name) }, scope);
124
170
  return this.derive(root, { composed: true, owners: scope.owners });
125
171
  }
126
172
  /**
@@ -180,7 +226,10 @@ class ApplicationBuilder {
180
226
  rendering: () => undefined,
181
227
  views: () => undefined,
182
228
  });
183
- return inspectGraph(this.#name, built.graph, built.facts);
229
+ return inspectGraph(this.#name, built.graph, {
230
+ ...built.facts,
231
+ development: this.#config.development,
232
+ });
184
233
  }
185
234
  async run(options) {
186
235
  let stderr = process.stderr;
@@ -193,6 +242,27 @@ class ApplicationBuilder {
193
242
  const faults = [];
194
243
  // The failure this run reports as its primary outcome, so nothing reports it a second time.
195
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;
196
266
  // One private controller per run, subscribed to the caller's signal at run entry.
197
267
  const controller = new AbortController();
198
268
  /**
@@ -207,6 +277,20 @@ class ApplicationBuilder {
207
277
  const reason = graphBuilt ? signals?.reason() : undefined;
208
278
  return reason ? cancellationCode(reason) : undefined;
209
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
+ };
210
294
  /**
211
295
  * Every exit path of the run leaves through the removal below, the one place it is written,
212
296
  * so no listener this run installed outlives it however the run ends.
@@ -216,6 +300,7 @@ class ApplicationBuilder {
216
300
  const overrides = options?.host;
217
301
  stderr = overrides?.stderr ?? stderr;
218
302
  const host = captureHost(overrides, stderr);
303
+ reportHost = host;
219
304
  const invocationOutput = new Output(host, controller.signal);
220
305
  output = invocationOutput;
221
306
  // The constructor validated the declared policy, which the build hands over after the overrides.
@@ -226,7 +311,8 @@ class ApplicationBuilder {
226
311
  invocationOutput.configure(policy, plugins.find((entry) => entry.theme !== undefined)?.theme ?? new Map());
227
312
  },
228
313
  rendering: (declared) => {
229
- policy = { ...declared, ...renderingPolicy(options?.rendering) };
314
+ const rendering = options?.rendering;
315
+ policy = { ...declared, ...renderingPolicy(rendering, runRendering(rendering)) };
230
316
  invocationOutput.configure(policy, new Map());
231
317
  },
232
318
  views: (value) => {
@@ -235,8 +321,17 @@ class ApplicationBuilder {
235
321
  },
236
322
  });
237
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
+ }
238
333
  const inputs = { globals: graph.globals.inputs, locals: collectInputs(graph.root) };
239
- const defaults = await prepareInputs(inputs, host);
334
+ const defaults = await prepareInputs(inputs, host, inputPlaces(graph));
240
335
  graphBuilt = true;
241
336
  if (!controller.signal.aborted) {
242
337
  /**
@@ -247,14 +342,15 @@ class ApplicationBuilder {
247
342
  await runInvocation({
248
343
  channel: (binding) => invocationOutput.channel(binding),
249
344
  defaults,
250
- facts: built.facts,
251
345
  graph,
252
346
  host,
253
- name: this.#name,
347
+ inspected,
348
+ offer,
254
349
  out: output.out,
255
350
  plugins: built.plugins,
256
351
  report: (fault) => faults.push(fault),
257
352
  route: (path) => {
353
+ walked = path;
258
354
  invocationOutput.useRoute(path);
259
355
  },
260
356
  signal: controller.signal,
@@ -268,80 +364,96 @@ class ApplicationBuilder {
268
364
  const fault = output.fault;
269
365
  if (fault) {
270
366
  // The action returned, so the view failure is this invocation's own failure.
271
- 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
+ });
272
372
  }
273
373
  }
274
374
  catch (error) {
275
375
  primary = error;
276
376
  try {
277
377
  const failure = toFailure(error);
278
- code = failure.exitCode;
378
+ code = exitCodeOf(failure);
279
379
  output ??= new Output(captureHost(undefined, stderr), controller.signal);
280
- const writes = await output.settle();
380
+ const writes = answeredWrite(await output.settle(), translatedFrom.get(failure));
281
381
  if (writes.kind === 'ok' && !silenced(error, controller.signal, cancellation())) {
282
- const report = describeFailure(registry ?? noViews, failure, output.context('stderr'));
283
- if (report.kind === 'rendered') {
284
- // The view owns the trailing newline; output resolves its marked text.
285
- await output.report(report.text);
286
- }
287
- 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())) {
288
385
  code = 1;
289
- // `report.text` is core's default text, which already ends in `\n`.
290
- await reportPlainly(stderr, `${report.text}Internal error: Rendering the failure failed: ${report.reason}\n`);
291
386
  }
292
387
  }
293
388
  }
294
- catch {
389
+ catch (reportError) {
295
390
  code = 1;
296
391
  reportingFailed = true;
392
+ reportingCause = reportError;
297
393
  }
298
394
  }
299
395
  /**
300
396
  * A sequence that stopped on its own source reports the same way: the call the action never
301
397
  * awaited observed nothing, and a failure the action let propagate is the primary outcome
302
- * 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.
303
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();
304
406
  for (const cause of output?.stopped ?? []) {
305
- if (!carried(primary, cause)) {
306
- 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));
307
414
  }
308
415
  }
309
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.
310
418
  // The primary outcome keeps its code, the way a view failure leaves it alone.
311
419
  // It is reported the way the primary failure is, so an override answers its class.
312
420
  for (const fault of faults) {
313
421
  if (!silenced(fault, controller.signal, cancellation())) {
314
- code = code === 0 ? 1 : code;
422
+ const own = deferred.has(fault) ? exitCodeOf(fault) : 1;
423
+ code = code === 0 ? own : code;
315
424
  try {
316
- const report = describeFailure(registry ?? noViews, fault, output?.context('stderr'));
317
- if (report.kind === 'rendered') {
318
- await output?.report(report.text);
319
- }
320
- else {
425
+ const sink = output && { build, output, registry: registry ?? noViews, stderr };
426
+ if (sink && (await reportFailure(sink, fault, scene()))) {
321
427
  code = 1;
322
- await reportPlainly(stderr, `${report.text}Internal error: Rendering the failure failed: ${report.reason}\n`);
323
428
  }
324
429
  }
325
- catch {
430
+ catch (reportError) {
326
431
  reportingFailed = true;
432
+ reportingCause ??= reportError;
327
433
  }
328
434
  }
329
435
  }
330
436
  if (output) {
331
- const writes = await output.settle();
437
+ const writes = answeredWrite(await output.settle(), translatedFrom.get(primary));
332
438
  if (writes.kind === 'failed') {
333
439
  code = 1;
334
440
  reportingFailed = true;
441
+ reportingCause ??= writes.error;
335
442
  }
336
443
  output.dispose();
337
444
  }
338
445
  if (reportingFailed) {
339
- 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
+ }
340
451
  }
341
452
  /**
342
453
  * One rule orders every code: a cancelled run resolves its signal's code, and a broken
343
- * failure view or destination in that run is reported as text without changing it. The
344
- * 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.
345
457
  */
346
458
  code = cancellation() ?? code;
347
459
  process.exitCode = code;
@@ -352,48 +464,116 @@ class ApplicationBuilder {
352
464
  }
353
465
  }
354
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
+ }
355
484
  /** Reject obsolete wiring before silently losing options that invocations depend on. */
356
- function checkOptions(options) {
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
+ };
357
491
  if (options === undefined) {
358
- return { description: undefined, version: checkVersion(undefined) };
492
+ return { description: undefined, version: checkVersion(site, undefined) };
359
493
  }
360
494
  if (!isPlainObject(options)) {
361
- throw new DeclarationError('The Application options must be an object. Supply an Application options object.');
362
- }
363
- if ('globals' in options) {
364
- throw new DeclarationError('The Application options contain globals. Declare them with globalOption(name, config).');
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
+ });
365
500
  }
366
- if ('failures' in options) {
367
- throw new DeclarationError('The Application options contain failures. Declare view overrides under views with override(key, view).');
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
+ });
509
+ }
368
510
  }
369
511
  // The root is every page's entry point, so it carries neither listing fact.
370
512
  // A key that may not be there is a fault of the slot, so it answers with the slot's shape.
371
- checkNoListingFacts('The Application', options);
513
+ checkNoListingFacts(site, options);
372
514
  return {
373
- description: checkDescription('The Application', options.description),
374
- version: checkVersion(options.version),
515
+ description: checkDescription(site, options.description),
516
+ version: checkVersion(site, options.version),
375
517
  };
376
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
+ }
377
549
  /**
378
550
  * Every rule `new Application(name, options)` applies, in the order it reads the slot: the
379
- * application's own view overrides, the rendering policy, the options slot and its facts, the
380
- * installed list and every rule between two plugins, the root's extension values, and then each
381
- * plugin's Commands, which attach to the root first, in installation order and list order.
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.
382
555
  */
383
- function declareApplication(options) {
556
+ function declareApplication(name, options) {
384
557
  const slot = isPlainObject(options) ? options : undefined;
385
558
  const identities = viewIdentities(coreViews);
386
- const views = buildViews({ declares: false, sentence: 'The Application' }, slot?.views, identities);
387
- const rendering = renderingPolicy(slot?.rendering);
388
- const facts = checkOptions(options);
389
- const installed = installPlugins(slot?.plugins ?? []);
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 ?? []);
390
569
  const { plugins } = installed;
391
- const contributors = plugins.map((entry) => buildViews({ declares: true, sentence: pluginSentence(entry.identity) }, entry.views, identities));
570
+ const contributors = plugins.map((entry) => buildViews(pluginViews(entry.identity), entry.views, identities));
392
571
  const table = globalTable([], plugins);
393
572
  const descriptors = installed.descriptors;
394
573
  const extensions = storeCommandLayers({
395
574
  descriptors,
396
575
  layers: [slot?.extensions],
576
+ site: optionSite(name, 'extensions', slot?.extensions),
397
577
  subject: layerOf(null),
398
578
  });
399
579
  // The Application checks its own facts, so the root carries none.
@@ -406,14 +586,24 @@ function declareApplication(options) {
406
586
  });
407
587
  const owners = new Map();
408
588
  for (const command of plugins.flatMap((entry) => entry.commands)) {
409
- root = attachToRoot(root, command.node, {
589
+ root = attachToRoot(root, command, {
410
590
  descriptors: new Map(root.descriptors),
411
591
  owners,
412
592
  table,
413
593
  });
414
594
  }
415
595
  return {
416
- config: { composed: false, contributors, facts, owners, plugins, rendering, views },
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
+ },
417
607
  globals: emptyGlobals(),
418
608
  root,
419
609
  };
@@ -421,7 +611,11 @@ function declareApplication(options) {
421
611
  /** The application name is typed as a command at the prompt, so it answers to the portable rule. */
422
612
  function checkApplicationName(name) {
423
613
  if (!isPortableName(name)) {
424
- throw new DeclarationError(`Application name "${String(name)}" is invalid. ${portableNameCorrection}`);
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
+ });
425
619
  }
426
620
  return name;
427
621
  }
@@ -429,7 +623,8 @@ function checkApplicationName(name) {
429
623
  class ApplicationDeclaration extends ApplicationBuilder {
430
624
  constructor(name, options) {
431
625
  // The arguments evaluate in order, so the name is checked before any option is read.
432
- super(checkApplicationName(name), declareApplication(options));
626
+ const checked = checkApplicationName(name);
627
+ super(checked, declareApplication(checked, options));
433
628
  }
434
629
  }
435
630
  export const Application = ApplicationDeclaration;
@@ -1,3 +1,4 @@
1
+ import type { FactSite } from './facts.js';
1
2
  /** The two keys the binding rules read, whichever scope declared the option. */
2
3
  interface BindingConfig {
3
4
  readonly env?: unknown;
@@ -6,21 +7,25 @@ interface BindingConfig {
6
7
  /**
7
8
  * The environment binding one option declares, or `undefined` when it declares none. A multiple
8
9
  * option takes its list from the configuration source, so it cannot bind, and a bound name must be
9
- * a variable name. `sentence` names the declaration at the start of the diagnostic.
10
+ * a variable name. The site names the declaration and marks its `env`.
10
11
  */
11
- export declare function checkEnvBinding(sentence: string, config: BindingConfig): string | undefined;
12
+ export declare function checkEnvBinding(site: FactSite, config: BindingConfig): string | undefined;
12
13
  /** An argument is identified by its place among bare tokens, so no variable can stand in for it. */
13
- export declare function checkNoArgumentBinding(sentence: string, config: object): void;
14
- /** One option bound to a variable, with the phrase a duplicate-variable diagnostic names it by. */
14
+ export declare function checkNoArgumentBinding(site: FactSite, config: object): void;
15
+ /**
16
+ * One option bound to a variable: the phrase a duplicate-variable diagnostic names it by, and the
17
+ * site whose `env` its finding marks.
18
+ */
15
19
  export interface BoundOption {
16
- readonly site: string;
20
+ readonly phrase: string;
21
+ readonly site: FactSite;
17
22
  readonly variable: string;
18
23
  }
19
24
  /**
20
- * The variables one scope binds, each to the phrase of the option that binds it. Within one
21
- * invocation's scope a variable binds one option, so a second binder is a declaration error that
22
- * names the first binder, in scope order, and then the second. `held` is what an enclosing scope
23
- * already binds, such as the globals table under a Command's own options.
25
+ * The variables one scope binds, each to the option that binds it. Within one invocation's scope a
26
+ * variable binds one option, so a second binder is a declaration error that names the first
27
+ * binder, in scope order, and then the second. `held` is what an enclosing scope already binds,
28
+ * such as the globals table under a Command's own options.
24
29
  */
25
- export declare function claimVariables(bound: readonly BoundOption[], held?: ReadonlyMap<string, string>): ReadonlyMap<string, string>;
30
+ export declare function claimVariables(bound: readonly BoundOption[], held?: ReadonlyMap<string, BoundOption>): ReadonlyMap<string, BoundOption>;
26
31
  export {};