@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,6 +1,12 @@
1
1
  import { schemaOptions } from './context.js';
2
- import { DeclarationError, InputError } from './errors.js';
2
+ import { escapeControlCharacters } from './controls.js';
3
+ import { asSentence, DeclarationError, InputError, quoted, reasonOf } from './errors.js';
4
+ import { callSite, factFault, flagFault, partOf, siteFinding } from './facts.js';
5
+ import { booleanOptionValueRule, defaultShape, invalidDefault, notAValidator, omissionAlreadyDecided, omissionWithoutValidator, requiredWithDefault, } from './input-rules.js';
3
6
  import { booleanValue } from './options.js';
7
+ import { isPlainObject } from './plain.js';
8
+ import { notAnObject } from './plugin-rules.js';
9
+ import { validatorFailed } from './rules.js';
4
10
  /** Every declaration in validation order: the globals first, then the reading Command's own. */
5
11
  function scoped(inputs) {
6
12
  return [
@@ -46,6 +52,27 @@ class ValidatedInputs {
46
52
  return { [input.name]: this.#values.get(input) };
47
53
  }
48
54
  }
55
+ /**
56
+ * The one check every `argument()`, `option()`, and `globalOption()` call makes before it reads its
57
+ * config, so a call in JavaScript that supplies none, or a value of another kind, reports a
58
+ * declaration fault and not a TypeError. `place` is where the call sits.
59
+ */
60
+ export function checkInputConfig(declared, place) {
61
+ const { config, kind, name } = declared;
62
+ if (isPlainObject(config)) {
63
+ return;
64
+ }
65
+ const subject = kind === 'argument' ? 'Argument' : 'Option';
66
+ const site = callSite(`${subject} ${quoted(name)}`, {
67
+ arguments: [name, config],
68
+ ...place,
69
+ });
70
+ throw new DeclarationError(notAnObject, {
71
+ correction: `Supply ${kind === 'argument' ? 'an argument' : 'an option'} config object, such as ${kind === 'argument' ? '{}' : "{ type: 'string' }"}.`,
72
+ findings: [siteFinding(site, '1')],
73
+ sentence: `${site.subject} declares a config that is not an object.`,
74
+ });
75
+ }
49
76
  /**
50
77
  * Authoring's snapshot of one config. An array default is the one declared value core hands to an
51
78
  * action as its own value, so the declaration keeps a copy and the caller keeps its array. Every
@@ -59,8 +86,25 @@ export function captureConfig(config) {
59
86
  * A declaration error names the declaration, because the author reads the declaration to fix it.
60
87
  * An argument declares and reads under one name, so the two namings differ for options alone.
61
88
  */
62
- function declaredName(input) {
63
- return input.kind === 'argument' ? `Argument "${input.name}"` : `Option "${input.name}"`;
89
+ export function declarationSubject(input) {
90
+ return input.kind === 'argument'
91
+ ? `Argument ${quoted(input.name)}`
92
+ : `Option ${quoted(input.name)}`;
93
+ }
94
+ /**
95
+ * Where one input was declared: a global option at its `globalOption()` call, and every other input
96
+ * at its own `argument()` or `option()` call on the Command at `path`.
97
+ */
98
+ export function inputPlace(input, scope) {
99
+ return scope.global ? { call: 'globalOption', path: [] } : { call: input.kind, path: scope.path };
100
+ }
101
+ /**
102
+ * The site of the call that declared one input at `place`, rebuilt as `call(name, config)`. Every
103
+ * finding for an input's own call starts from it, so each marks the call the same way. `subject`
104
+ * is the sentence's name for the input.
105
+ */
106
+ export function declaringSite(input, place, subject = declarationSubject(input)) {
107
+ return callSite(subject, { arguments: [input.name, input.config], ...place });
64
108
  }
65
109
  /**
66
110
  * The token an operator would type for one declaration: `--file` for an option, `-F` when the
@@ -143,61 +187,88 @@ function holdsRawDefault(input) {
143
187
  * rule that already decides absence rejects it, and the flag needs a validator to receive the
144
188
  * omission.
145
189
  */
146
- function checkOmissionValidation(input, subject) {
190
+ function checkOmissionValidation(input, site, subject) {
147
191
  const { config } = input;
192
+ const decided = (sentence, correction) => factFault(omissionAlreadyDecided, site, { correction, fact: 'validateOmitted', sentence });
148
193
  if (config.required) {
149
- throw new DeclarationError(`${subject} is required and declares validateOmitted. Remove validateOmitted or make the input optional.`);
194
+ throw decided(`${subject} is required and declares validateOmitted.`, 'Remove validateOmitted or make the input optional.');
150
195
  }
151
196
  if (hasDefault(input)) {
152
- throw new DeclarationError(`${subject} declares a default and validateOmitted. Remove one; the default already fills an omitted value.`);
197
+ throw decided(`${subject} declares a default and validateOmitted.`, 'Remove one; the default already fills an omitted value.');
153
198
  }
154
199
  if (collects(input)) {
155
- throw new DeclarationError(`${subject} takes several values and declares validateOmitted. Remove validateOmitted; with no values the action receives an empty array and no validator runs.`);
200
+ throw decided(`${subject} takes several values and declares validateOmitted.`, 'Remove validateOmitted; with no values the action receives an empty array and no validator runs.');
156
201
  }
157
202
  if (config.validate === undefined) {
158
- throw new DeclarationError(`${subject} declares validateOmitted without a validator. Add validate or remove validateOmitted.`);
203
+ throw factFault(omissionWithoutValidator, site, {
204
+ correction: 'Add validate or remove validateOmitted.',
205
+ fact: 'validateOmitted',
206
+ sentence: `${subject} declares validateOmitted without a validator.`,
207
+ });
159
208
  }
160
209
  }
161
- function checkDeclaration(input, subject) {
210
+ /** The value rules a Boolean option may not declare, in the order its diagnostic names them. */
211
+ const valueRules = ['validate', 'default', 'required', 'validateOmitted'];
212
+ /** Whether a `validate` value is a Standard Schema v1 object that core can call. */
213
+ function isStandardSchema(validator) {
214
+ if (validator === null || (typeof validator !== 'object' && typeof validator !== 'function')) {
215
+ return false;
216
+ }
217
+ const props = Reflect.get(validator, '~standard');
218
+ return (typeof props === 'object' &&
219
+ props !== null &&
220
+ Reflect.get(props, 'version') === 1 &&
221
+ typeof Reflect.get(props, 'vendor') === 'string' &&
222
+ typeof Reflect.get(props, 'validate') === 'function');
223
+ }
224
+ function checkDeclaration(input, site, subject) {
162
225
  const { config } = input;
163
226
  if (input.kind === 'option' && input.config.type === 'boolean') {
164
- if ('validate' in config ||
165
- 'default' in config ||
166
- 'required' in config ||
167
- 'validateOmitted' in config) {
168
- throw new DeclarationError(`${subject} is Boolean. Remove validate, default, required, and validateOmitted; use polarity to control its absent value.`);
227
+ const declared = valueRules.find((key) => key in config);
228
+ if (declared !== undefined) {
229
+ throw factFault(booleanOptionValueRule, site, {
230
+ correction: `Remove ${declared}; use polarity to control its absent value.`,
231
+ fact: declared,
232
+ sentence: `${subject} is Boolean and declares ${declared}.`,
233
+ });
169
234
  }
170
235
  return;
171
236
  }
172
237
  if (config.required !== undefined && typeof config.required !== 'boolean') {
173
- throw new DeclarationError(`${subject} required must be Boolean. Use true or false.`);
238
+ throw flagFault(site, 'required');
174
239
  }
175
240
  if (input.kind === 'argument' &&
176
241
  input.config.variadic !== undefined &&
177
242
  typeof input.config.variadic !== 'boolean') {
178
- throw new DeclarationError(`${subject} variadic must be Boolean. Use true or false.`);
243
+ throw flagFault(site, 'variadic');
179
244
  }
180
245
  // The test reads presence, not truth, so a declared `undefined` is a declaration to reject.
181
246
  if ('validateOmitted' in config && typeof config.validateOmitted !== 'boolean') {
182
- throw new DeclarationError(`${subject} validateOmitted must be Boolean. Use true or false.`);
247
+ throw flagFault(site, 'validateOmitted');
183
248
  }
184
249
  if (config.required && Object.hasOwn(config, 'default')) {
185
- throw new DeclarationError(`${subject} is required and declares a default. Remove the default or make the input optional.`);
250
+ throw factFault(requiredWithDefault, site, {
251
+ correction: 'Remove the default or make the input optional.',
252
+ fact: 'default',
253
+ sentence: `${subject} is required and declares a default.`,
254
+ });
186
255
  }
187
256
  if (validatesOmission(input)) {
188
- checkOmissionValidation(input, subject);
257
+ checkOmissionValidation(input, site, subject);
189
258
  }
190
- const validator = config.validate;
191
- if (validator !== undefined &&
192
- (validator === null ||
193
- (typeof validator !== 'object' && typeof validator !== 'function') ||
194
- !validator['~standard'] ||
195
- validator['~standard'].version !== 1 ||
196
- typeof validator['~standard'].vendor !== 'string' ||
197
- typeof validator['~standard'].validate !== 'function')) {
198
- throw new DeclarationError(`${subject} validate must be a Standard Schema v1 object. Supply a compatible validator.`);
259
+ if (config.validate !== undefined && !isStandardSchema(config.validate)) {
260
+ throw factFault(notAValidator, site, {
261
+ correction: 'Supply a compatible validator.',
262
+ fact: 'validate',
263
+ sentence: `${subject} validate must be a Standard Schema v1 object.`,
264
+ });
199
265
  }
200
266
  }
267
+ /**
268
+ * One issue a validator returned, checked where it enters. Core keeps every own enumerable field
269
+ * the issue carries and rewrites only `path`, reducing each segment to its key, so a field core
270
+ * never reads, such as a validator's issue code, reaches a failure view unchanged.
271
+ */
201
272
  function readIssue(issue) {
202
273
  if (issue === null || typeof issue !== 'object' || !('message' in issue)) {
203
274
  throw new Error('The validator returned an invalid Standard Schema issue.');
@@ -208,7 +279,7 @@ function readIssue(issue) {
208
279
  }
209
280
  const suppliedPath = 'path' in issue ? issue.path : undefined;
210
281
  if (suppliedPath === undefined) {
211
- return { message };
282
+ return { ...issue, message };
212
283
  }
213
284
  if (!Array.isArray(suppliedPath)) {
214
285
  throw new Error('The validator returned an invalid Standard Schema issue path.');
@@ -220,13 +291,15 @@ function readIssue(issue) {
220
291
  }
221
292
  return key;
222
293
  });
223
- return { message, path };
294
+ return { ...issue, message, path };
224
295
  }
225
296
  /**
226
297
  * A broken validator is a fault in the declaration, whichever value reached it, so its diagnostic
227
- * names the declaration. Returned issues belong to the value, so the caller names those.
298
+ * names the declaration and its finding marks the validator on the call at `place`. Returned issues
299
+ * belong to the value, so the caller names those.
228
300
  */
229
- async function validate(input, raw, context) {
301
+ async function validate(input, raw, call) {
302
+ const { context, place } = call;
230
303
  const validator = input.config.validate;
231
304
  if (validator === undefined) {
232
305
  return { value: raw };
@@ -246,8 +319,13 @@ async function validate(input, raw, context) {
246
319
  return { issues: Array.from(issues, readIssue) };
247
320
  }
248
321
  catch (error) {
249
- const reason = error instanceof Error ? error.message : 'Unknown validator failure.';
250
- throw new DeclarationError(`${declaredName(input)} validator failed unexpectedly: ${reason} Fix the validator.`);
322
+ // The reason is the author's detail: a distributed build shows the generic defect message.
323
+ const site = place === undefined ? undefined : declaringSite(input, place);
324
+ throw new DeclarationError(validatorFailed, {
325
+ correction: 'Fix the validator.',
326
+ findings: site === undefined ? [] : [siteFinding(site, partOf(site, 'validate'))],
327
+ sentence: `${declarationSubject(input)} validator failed unexpectedly: ${asSentence(reasonOf(error))}`,
328
+ }, { cause: error });
251
329
  }
252
330
  }
253
331
  /**
@@ -258,13 +336,13 @@ async function validate(input, raw, context) {
258
336
  * next call. The host is the one captured object that every call and the action share.
259
337
  */
260
338
  async function validateDeclared(input, raw, call) {
261
- const { context, signal } = call;
339
+ const { context, place, signal } = call;
262
340
  if (!collects(input) || input.config.validate === undefined) {
263
- return validate(input, raw, context());
341
+ return validate(input, raw, { context: context(), place });
264
342
  }
265
343
  if (!Array.isArray(raw)) {
266
344
  // The parser, the input sources, and the declaration rules only ever supply an array here.
267
- throw new TypeError(`${declaredName(input)} reached validation without an array of values.`);
345
+ throw new TypeError(`${declarationSubject(input)} reached validation without an array of values.`);
268
346
  }
269
347
  const outputs = [];
270
348
  const issues = [];
@@ -273,15 +351,15 @@ async function validateDeclared(input, raw, call) {
273
351
  // A cancelled run starts no further call; the run resolves its cancellation code instead.
274
352
  break;
275
353
  }
276
- const result = await validate(input, value, context());
354
+ const result = await validate(input, value, { context: context(), place });
277
355
  if (result.issues === undefined) {
278
356
  outputs.push(result.value);
279
357
  }
280
358
  else {
281
- issues.push(...reported(result.issues).map((issue) => ({
282
- message: issue.message,
283
- path: [position, ...(issue.path ?? [])],
284
- })));
359
+ // The issue keeps its own fields, and only its path gains the value's position.
360
+ for (const issue of reported(result.issues)) {
361
+ issues.push({ ...issue, path: [position, ...(issue.path ?? [])] });
362
+ }
285
363
  }
286
364
  }
287
365
  return issues.length === 0 ? { value: outputs } : { issues };
@@ -304,23 +382,33 @@ export function issuePath(issue) {
304
382
  */
305
383
  function reported(issues) {
306
384
  return issues.length === 0
307
- ? [{ message: 'The validator rejected this value without an explanation.' }]
385
+ ? [
386
+ {
387
+ message: 'The validator rejected this value without an explanation. Supply a different value.',
388
+ },
389
+ ]
308
390
  : issues;
309
391
  }
392
+ /**
393
+ * One line per issue under the input's subject. A path segment can hold text the operator typed,
394
+ * such as a key of a record, so the line escapes the path; `issuePath` keeps it raw for a view.
395
+ */
310
396
  function messages(subject, issues) {
311
397
  return issues.map((issue) => {
312
398
  const path = issuePath(issue);
313
- return `${subject}${path === undefined ? '' : ` at ${path}`}: ${issue.message}`;
399
+ const position = path === undefined ? '' : ` at ${escapeControlCharacters(path)}`;
400
+ return `${subject}${position}: ${issue.message}`;
314
401
  });
315
402
  }
316
403
  /** The fault a default of the wrong raw shape reports, by the shape its declaration expects. */
317
- function defaultShapeFault(input, subject) {
404
+ function defaultShapeFault(input, site, subject) {
405
+ const fault = (sentence, correction) => factFault(defaultShape, site, { correction, fact: 'default', sentence });
318
406
  if (!collects(input)) {
319
- return `${subject} default must be a string without a validator. Supply a string default.`;
407
+ return fault(`${subject} default must be a string without a validator.`, 'Supply a string default.');
320
408
  }
321
409
  return input.config.validate === undefined
322
- ? `${subject} default must be an array of strings without a validator. Supply a string array default.`
323
- : `${subject} default must be an array. Supply an array of values.`;
410
+ ? fault(`${subject} default must be an array of strings without a validator.`, 'Supply a string array default.')
411
+ : fault(`${subject} default must be an array.`, 'Supply an array of values.');
324
412
  }
325
413
  /** A declared `default: undefined` is a default, so presence is the key, never the value. */
326
414
  function hasDefault(input) {
@@ -334,31 +422,41 @@ function hasDefault(input) {
334
422
  * diagnostics read with; every other caller is named by the declaration itself.
335
423
  */
336
424
  export function checkDeclarations(inputs, named) {
337
- for (const input of inputs) {
338
- checkDeclaration(input, named ?? declaredName(input));
425
+ for (const { input, site } of inputs) {
426
+ checkDeclaration(input, site, named ?? declarationSubject(input));
339
427
  }
340
- for (const input of inputs.filter((entry) => hasDefault(entry))) {
428
+ for (const { input, site } of inputs.filter((entry) => hasDefault(entry.input))) {
341
429
  if (!holdsRawDefault(input)) {
342
- const subject = named ?? declaredName(input);
343
- throw new DeclarationError(defaultShapeFault(input, subject));
430
+ throw defaultShapeFault(input, site, named ?? declarationSubject(input));
344
431
  }
345
432
  }
346
433
  }
347
434
  /**
348
435
  * Every declared default, validated before any token is read. The host is captured by then, so a
349
- * default's validator reads the same Host its action will, under the `default` phase.
436
+ * default's validator reads the same Host its action will, under the `default` phase. `places`
437
+ * says where each declaration sits, which a rejected default's finding rebuilds.
350
438
  */
351
- export async function prepareInputs(inputs, host) {
439
+ export async function prepareInputs(inputs, host, places) {
352
440
  const declarations = scoped(inputs);
353
441
  const defaults = new Map();
354
442
  for (const entry of declarations.filter(({ input }) => hasDefault(input))) {
355
443
  const { input } = entry;
356
- const subject = declaredName(input);
444
+ const subject = declarationSubject(input);
445
+ const place = places.get(input);
357
446
  const result = await validateDeclared(input, input.config.default, {
358
447
  context: () => ({ host, input: identityOf(entry), phase: 'default' }),
448
+ place,
359
449
  });
360
450
  if (result.issues !== undefined) {
361
- throw new DeclarationError(`${subject} has an invalid default. Fix the default or its validator.\n${messages(subject, reported(result.issues)).join('\n')}`);
451
+ const site = place === undefined ? undefined : declaringSite(input, place);
452
+ throw new DeclarationError(invalidDefault, {
453
+ correction: 'Fix the default or its validator.',
454
+ findings: site === undefined ? [] : [siteFinding(site, partOf(site, 'default'))],
455
+ sentence: [
456
+ `${subject} has an invalid default.`,
457
+ ...messages(subject, reported(result.issues)),
458
+ ].join('\n'),
459
+ });
362
460
  }
363
461
  defaults.set(input, result.value);
364
462
  }
@@ -461,6 +559,7 @@ export async function validateValues(invocation) {
461
559
  const accept = async (entry, raw, spelling) => {
462
560
  const result = await validateDeclared(entry.input, raw, {
463
561
  context: () => contextOf(entry),
562
+ place: inputPlace(entry.input, { global: entry.global, path: invocation.command }),
464
563
  signal: invocation.signal,
465
564
  });
466
565
  if (result.issues === undefined) {
package/dist/view.d.ts CHANGED
@@ -39,11 +39,32 @@ type AnyDeclaredView = (View<never> | RowView<never>) & DeclaredViewBrand & {
39
39
  /** A failure class as an override key, so `UsageError` and an application's subclass both fit. */
40
40
  type FailureClass<Failure extends LoomError> = abstract new (...args: never[]) => Failure;
41
41
  /**
42
- * One stored view function, with the data type erased. A registry holds one entry per key and
43
- * cannot carry a type parameter per entry, so `override(key, replacement)` is where the
44
- * replacement is typed against the key it answers.
42
+ * What every failure view reads: the stderr view context, where the run was, and the hints the
43
+ * installed plugins' `onFailure` hooks returned. `run()` fills it where it catches the failure, so
44
+ * no failure class carries these facts. `path` holds the canonical names routing walked, `[]`
45
+ * before routing, and `hints` is `[]` when no hook contributed.
45
46
  */
46
- type ViewFunction = (data: never, context: ViewContext) => unknown;
47
+ interface FailureViewContext extends ViewContext {
48
+ readonly application: string;
49
+ readonly path: readonly string[];
50
+ readonly hints: readonly string[];
51
+ }
52
+ /**
53
+ * A failure class's view. A `View<Failure>` written against `ViewContext` is assignable to it,
54
+ * because its function reads less of the context.
55
+ */
56
+ interface FailureView<Failure extends LoomError> {
57
+ render: (failure: Readonly<Failure>, context: FailureViewContext) => string;
58
+ /** A failure view has one shape, as a view does. */
59
+ row?: never;
60
+ }
61
+ /**
62
+ * One stored view function, with the data type and the context erased. A registry holds one entry
63
+ * per key and cannot carry a type parameter per entry, so `override(key, replacement)` is where the
64
+ * replacement is typed against the key it answers: a failure class's view reads the failure view
65
+ * context, and every other view reads the view context.
66
+ */
67
+ type ViewFunction = (data: never, context: never) => unknown;
47
68
  /** One stored row function, with the row type erased for the same reason. */
48
69
  type RowFunction = (row: never, index: number, context: ViewContext) => unknown;
49
70
  /** One stored function that opens a sequence and reads the context alone. */
@@ -114,7 +135,7 @@ declare function override<Data>(key: DeclaredView<Data>, replacement: NoInfer<Vi
114
135
  declare function override<Row>(key: DeclaredRowView<Row>, replacement: NoInfer<RowView<Row>>): ViewOverride;
115
136
  declare function override<Failure extends LoomError>(key: FailureClass<Failure> & {
116
137
  readonly [declaredView]?: never;
117
- }, replacement: NoInfer<View<Failure>>): ViewOverride;
138
+ }, replacement: NoInfer<FailureView<Failure>>): ViewOverride;
118
139
  /** One contributor's overrides, read once per build and consulted in contributor order. */
119
140
  interface ViewContributions {
120
141
  failures: Map<unknown, StoredView>;
@@ -127,13 +148,20 @@ interface ViewContributions {
127
148
  type ViewRegistry = readonly ViewContributions[];
128
149
  /** The identities one build has met, so a second object under one identity is visible. */
129
150
  type ViewIdentities = Map<string, AnyDeclaredView>;
130
- /** How one contributor's diagnostics name it, and whether its list may declare a view. */
151
+ /**
152
+ * How one contributor's diagnostics name it, whether its list may declare a view, and the call
153
+ * that declared the list, `plugin(identity, …)` or `new Application(name, …)`, which a fault marks.
154
+ */
131
155
  interface ViewSubject {
132
156
  /** A plugin declares views beside its overrides; an application overrides alone. */
133
157
  declares: boolean;
134
158
  sentence: string;
159
+ owner: {
160
+ call: string;
161
+ named: unknown;
162
+ };
135
163
  }
136
- /** The identity register one build starts from, holding the views core itself declares. */
164
+ /** The identity one build starts from, holding the views core itself declares. */
137
165
  declare function viewIdentities(declared: readonly AnyDeclaredView[]): ViewIdentities;
138
166
  /**
139
167
  * One contributor's own overrides, with the identity of every declared view it lists or names as a
@@ -158,23 +186,29 @@ interface ResolvedRowView<Row> {
158
186
  */
159
187
  declare function resolveRowView<Row>(registry: ViewRegistry, value: RowView<Row>): ResolvedRowView<Row>;
160
188
  /**
161
- * The report of one failure: the text core writes, and whether a view produced it. An unrendered
162
- * report carries core's own text, which the plain fallback path writes beside the diagnostic
163
- * naming the view that could not answer.
189
+ * The report of one failure: the text core writes, and whether a view produced it. A rendered
190
+ * report says whether core's own default text answered, which for a defect is the generic defect
191
+ * message. An unrendered report carries core's own text, which the plain fallback path writes
192
+ * beside the diagnostic naming the view that could not answer, and what the view threw.
164
193
  */
165
194
  type FailureReport = {
166
195
  kind: 'rendered';
167
196
  text: string;
197
+ core: boolean;
168
198
  } | {
169
199
  kind: 'unrendered';
170
200
  text: string;
171
201
  reason: string;
202
+ cause: unknown;
172
203
  };
173
204
  /**
174
205
  * The text core writes for one failure. Resolution walks the registry as `resolveFailure` defines
175
- * it and falls to core's own default text, which escapes the raw facts it interpolates. A
176
- * `FatalError` keeps the authored marked message it was given.
206
+ * it and falls to core's own default text, which opens a usage failure with the context's
207
+ * application name and escapes the raw facts it interpolates. A `FatalError` keeps the authored
208
+ * marked message it was given. The default text writes each hint on its own line under the
209
+ * sentence, as marked text it does not escape; an override decides for itself. An unrendered report
210
+ * carries the same default text without hints, which the plain fallback path writes.
177
211
  */
178
- declare function describeFailure(registry: ViewRegistry, failure: LoomError, context?: ViewContext): FailureReport;
179
- export type { AnyDeclaredView, DeclaredRowView, DeclaredView, DeclaredViewBrand, FailureClass, FailureReport, ResolvedRowView, ViewContribution, ViewContributions, ViewIdentities, ViewOverride, ViewRegistry, ViewShape, };
212
+ declare function describeFailure(registry: ViewRegistry, failure: LoomError, context: FailureViewContext): FailureReport;
213
+ export type { AnyDeclaredView, DeclaredRowView, DeclaredView, DeclaredViewBrand, FailureClass, FailureReport, FailureView, FailureViewContext, ResolvedRowView, ViewContribution, ViewContributions, ViewSubject, ViewIdentities, ViewOverride, ViewRegistry, ViewShape, };
180
214
  export { buildViews, describeFailure, override, resolveRowView, resolveView, shapeOf, view, viewIdentities, };