@loomcli/core 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/LICENSE +21 -0
  2. package/dist/application.d.ts +60 -31
  3. package/dist/application.js +356 -149
  4. package/dist/bindings.d.ts +31 -0
  5. package/dist/bindings.js +64 -0
  6. package/dist/chain.d.ts +26 -12
  7. package/dist/chain.js +59 -92
  8. package/dist/command-rules.d.ts +55 -0
  9. package/dist/command-rules.js +142 -0
  10. package/dist/command.d.ts +212 -93
  11. package/dist/command.js +1224 -454
  12. package/dist/controls.d.ts +8 -0
  13. package/dist/controls.js +23 -0
  14. package/dist/defect.d.ts +18 -0
  15. package/dist/defect.js +272 -0
  16. package/dist/developer.d.ts +24 -0
  17. package/dist/developer.js +52 -0
  18. package/dist/diagnostic-text.d.ts +81 -0
  19. package/dist/diagnostic-text.js +283 -0
  20. package/dist/diagnostic.d.ts +11 -0
  21. package/dist/diagnostic.js +70 -0
  22. package/dist/errors.d.ts +115 -26
  23. package/dist/errors.js +333 -54
  24. package/dist/exit-codes.d.ts +45 -0
  25. package/dist/exit-codes.js +46 -0
  26. package/dist/extension.d.ts +44 -9
  27. package/dist/extension.js +147 -65
  28. package/dist/facts.d.ts +71 -11
  29. package/dist/facts.js +108 -25
  30. package/dist/globals.d.ts +63 -22
  31. package/dist/globals.js +164 -39
  32. package/dist/hints.d.ts +79 -0
  33. package/dist/hints.js +247 -0
  34. package/dist/host.d.ts +13 -0
  35. package/dist/host.js +43 -1
  36. package/dist/identity.d.ts +19 -0
  37. package/dist/identity.js +72 -0
  38. package/dist/index.d.ts +15 -4
  39. package/dist/index.js +6 -0
  40. package/dist/input-rules.d.ts +64 -0
  41. package/dist/input-rules.js +145 -0
  42. package/dist/inspect.d.ts +36 -5
  43. package/dist/inspect.js +125 -27
  44. package/dist/lanes.js +1 -1
  45. package/dist/locate.d.ts +41 -0
  46. package/dist/locate.js +121 -0
  47. package/dist/options.d.ts +96 -2
  48. package/dist/options.js +259 -71
  49. package/dist/output.d.ts +11 -2
  50. package/dist/output.js +23 -3
  51. package/dist/plain.d.ts +6 -0
  52. package/dist/plain.js +12 -0
  53. package/dist/plugin-rules.d.ts +62 -0
  54. package/dist/plugin-rules.js +155 -0
  55. package/dist/plugin.d.ts +121 -55
  56. package/dist/plugin.js +496 -126
  57. package/dist/prototypes.d.ts +7 -0
  58. package/dist/prototypes.js +29 -0
  59. package/dist/rendering.d.ts +6 -1
  60. package/dist/rendering.js +23 -5
  61. package/dist/rules.d.ts +51 -0
  62. package/dist/rules.js +115 -0
  63. package/dist/sequence.js +6 -1
  64. package/dist/sources.d.ts +58 -0
  65. package/dist/sources.js +258 -0
  66. package/dist/style-wire.js +1 -1
  67. package/dist/style.js +1 -1
  68. package/dist/theme.d.ts +4 -0
  69. package/dist/theme.js +25 -5
  70. package/dist/thenable.d.ts +15 -0
  71. package/dist/thenable.js +29 -0
  72. package/dist/translators.d.ts +69 -0
  73. package/dist/translators.js +253 -0
  74. package/dist/types.d.ts +76 -21
  75. package/dist/validation.d.ts +65 -10
  76. package/dist/validation.js +314 -108
  77. package/dist/view.d.ts +49 -15
  78. package/dist/view.js +157 -79
  79. package/package.json +3 -2
package/dist/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. */
@@ -78,7 +99,7 @@ declare function view<Data>(identity: string, definition: View<Data>): DeclaredV
78
99
  declare function view<Row>(identity: string, definition: RowView<Row>): DeclaredRowView<Row>;
79
100
  /**
80
101
  * What one override replaces: a declared view, a failure class read as its prototype, or a value
81
- * that is neither, which build reports as the entry fault of the list that holds it.
102
+ * that is neither, which the list that holds it reports as its entry fault.
82
103
  */
83
104
  type OverrideKey = {
84
105
  kind: 'view';
@@ -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, };
package/dist/view.js CHANGED
@@ -1,4 +1,10 @@
1
- import { DeclarationError, defaultText, FatalError, notTextReason, reasonOf } from './errors.js';
1
+ import { viewShape } from './command-rules.js';
2
+ import { elided, quoteString, spelled } from './diagnostic-text.js';
3
+ import { DeclarationError, defaultText, FatalError, notTextReason, quoted, reasonOf, } from './errors.js';
4
+ import { partFinding, slotSite } from './facts.js';
5
+ import { checkIdentity } from './identity.js';
6
+ import { foreignValue, notAList, overrideKey, overrideTwice, twoPackageCopies, } from './plugin-rules.js';
7
+ import { prototypeChain } from './prototypes.js';
2
8
  import { escapeText } from './style.js';
3
9
  /** Authored declarations register here, so a hand-built object with an identity is a bare view. */
4
10
  const declarations = new WeakMap();
@@ -48,12 +54,22 @@ function shapeOf(value) {
48
54
  return render ? 'render' : 'neither';
49
55
  }
50
56
  function view(identity, definition) {
57
+ checkIdentity('view', identity);
51
58
  const shape = shapeOf(definition);
59
+ const findings = [{ arguments: [identity, definition], call: 'view', mark: '1' }];
52
60
  if (shape === 'both') {
53
- throw new DeclarationError(`View "${identity}" carries render and row. Supply one of the two.`);
61
+ throw new DeclarationError(viewShape, {
62
+ correction: 'Supply one of the two.',
63
+ findings,
64
+ sentence: `View ${quoted(identity)} carries render and row.`,
65
+ });
54
66
  }
55
67
  if (shape === 'neither') {
56
- throw new DeclarationError(`View "${identity}" carries neither render nor row. Supply a view with render or a row view with row.`);
68
+ throw new DeclarationError(viewShape, {
69
+ correction: 'Supply a view with render or a row view with row.',
70
+ findings,
71
+ sentence: `View ${quoted(identity)} carries neither render nor row.`,
72
+ });
57
73
  }
58
74
  return typeof definition.row === 'function'
59
75
  ? new RowViewDeclaration(identity, definition)
@@ -83,8 +99,8 @@ function override(key, replacement) {
83
99
  }
84
100
  /**
85
101
  * The key one failure-class override answers. A class is a function whose `prototype` is the
86
- * object a thrown failure's chain holds, so anything else is no key at all and build reports it as
87
- * the entry fault of the list that holds it, rather than colliding with every other such value.
102
+ * object a thrown failure's chain holds, so anything else is no key at all and the list that holds
103
+ * it reports it as its entry fault, rather than colliding with every other such value.
88
104
  */
89
105
  function failureKey(key) {
90
106
  if (typeof key !== 'function' || !('prototype' in key)) {
@@ -97,7 +113,7 @@ function failureKey(key) {
97
113
  const name = 'name' in key && typeof key.name === 'string' && key.name !== '' ? key.name : 'a failure class';
98
114
  return { kind: 'failure', name, prototype };
99
115
  }
100
- /** The identity register one build starts from, holding the views core itself declares. */
116
+ /** The identity one build starts from, holding the views core itself declares. */
101
117
  function viewIdentities(declared) {
102
118
  const identities = new Map();
103
119
  for (const value of declared) {
@@ -105,22 +121,63 @@ function viewIdentities(declared) {
105
121
  }
106
122
  return identities;
107
123
  }
108
- /** One identity means one declared view, wherever on the graph that view appears. */
109
- function registerIdentity(identities, declared) {
124
+ /**
125
+ * One identity means one declared view, wherever on the graph that view appears. `place` marks the
126
+ * list entry that names the view, when one does.
127
+ */
128
+ function registerIdentity(identities, declared, place) {
110
129
  const known = identities.get(declared.identity);
111
130
  if (known === undefined) {
112
131
  identities.set(declared.identity, declared);
113
132
  return;
114
133
  }
115
134
  if (known !== declared) {
116
- throw new DeclarationError(`View "${declared.identity}" is declared by two distinct objects. Install one copy of the package that declares it.`);
135
+ throw new DeclarationError(twoPackageCopies, {
136
+ correction: 'Install one copy of the package that declares it.',
137
+ findings: place === undefined ? [] : [{ ...place, note: `another ${quoted(declared.identity)}` }],
138
+ sentence: `View ${quoted(declared.identity)} is declared by two distinct objects.`,
139
+ });
117
140
  }
118
141
  }
119
- /** The sentence one contributor's list reports for a value it cannot read. */
120
- function entryFault(subject) {
142
+ /** A name JavaScript source can spell as an identifier, which a finding prints a class key as. */
143
+ const identifier = /^[A-Za-z_$][\w$]*$/u;
144
+ /** How a finding prints an override's key: a failure class by its name, anything else elided. */
145
+ function keyCode(key) {
146
+ return key.kind === 'failure' && identifier.test(key.name) ? key.name : elided;
147
+ }
148
+ /**
149
+ * One list entry as a finding prints it: a declared view as the `view()` call that made it, an
150
+ * override as its `override()` call, and any other value as it is.
151
+ */
152
+ function entryCode(entry) {
153
+ if (typeof entry !== 'object' || entry === null) {
154
+ return entry;
155
+ }
156
+ const listed = declarations.get(entry);
157
+ if (listed) {
158
+ return spelled(`view(${quoteString(listed.identity)}, ${elided})`);
159
+ }
160
+ const record = overrides.get(entry);
161
+ return record ? spelled(`override(${keyCode(record.key)}, ${elided})`) : entry;
162
+ }
163
+ /** Where one contributor's `views` list sits, with each entry printed as the call that made it. */
164
+ function listSite(subject, declared) {
165
+ const views = Array.isArray(declared) ? Array.from(declared, entryCode) : declared;
166
+ return slotSite({ ...subject.owner, subject: subject.sentence }, 'views', views);
167
+ }
168
+ /** The fault of one list entry that is not a value this contributor's list may hold. */
169
+ function entryFault(subject, place) {
121
170
  return subject.declares
122
- ? `${subject.sentence} holds a value that is not a view. Supply the value returned by view(identity, definition) or override(key, view).`
123
- : `${subject.sentence} holds a value that is not a view override. Supply the value returned by override(key, view).`;
171
+ ? new DeclarationError(foreignValue, {
172
+ correction: 'Supply the value returned by view(identity, definition) or override(key, view).',
173
+ findings: [place],
174
+ sentence: `${subject.sentence} holds a value that is not a view.`,
175
+ })
176
+ : new DeclarationError(foreignValue, {
177
+ correction: 'Supply the value returned by override(key, view).',
178
+ findings: [place],
179
+ sentence: `${subject.sentence} holds a value that is not a view override.`,
180
+ });
124
181
  }
125
182
  /** A `views` slot holds a list, so anything else is the same declaration fault. */
126
183
  function readContributions(subject, declared) {
@@ -128,50 +185,80 @@ function readContributions(subject, declared) {
128
185
  return [];
129
186
  }
130
187
  if (!Array.isArray(declared)) {
131
- throw new DeclarationError(subject.declares
132
- ? `${subject.sentence} declares views that are not an array. Supply a list of declared views and override values.`
133
- : entryFault(subject));
188
+ throw new DeclarationError(notAList, {
189
+ correction: subject.declares
190
+ ? 'Supply a list of declared views and override values.'
191
+ : 'Supply a list of override values.',
192
+ findings: [partFinding(listSite(subject, declared), [])],
193
+ sentence: `${subject.sentence} declares views that are not an array.`,
194
+ });
134
195
  }
135
196
  return declared;
136
197
  }
137
- /** The override one entry carries; anything else is a declaration fault of the slot. */
138
- function overrideOf(subject, entry) {
139
- const record = typeof entry === 'object' && entry !== null ? overrides.get(entry) : undefined;
140
- if (!record) {
141
- throw new DeclarationError(entryFault(subject));
198
+ /**
199
+ * The entry a key's first override sits at, recording this one's when it is the first. One key
200
+ * answers to one override inside one contributor, so a second override marks both entries.
201
+ */
202
+ function claimKey(build, key, index) {
203
+ const slot = key.kind === 'failure' ? key.prototype : key.view;
204
+ const first = build.positions.get(slot);
205
+ if (first === undefined) {
206
+ build.positions.set(slot, index);
207
+ return;
142
208
  }
143
- return record;
209
+ const clause = key.kind === 'failure'
210
+ ? `the view for ${quoted(key.name)}`
211
+ : `view ${quoted(key.view.identity)}`;
212
+ throw new DeclarationError(overrideTwice, {
213
+ correction: 'Remove one override.',
214
+ findings: [
215
+ partFinding(build.site, [first], 'the first override'),
216
+ partFinding(build.site, [index], 'the second override'),
217
+ ],
218
+ sentence: `${build.subject.sentence} overrides ${clause} twice.`,
219
+ });
144
220
  }
145
221
  /**
146
- * One override recorded under the key it answers. One key answers to one override inside one
147
- * contributor, so a second override for it is a declaration fault; the same key overridden by two
148
- * contributors resolves first-in-wins. A key that is neither a declared view nor a failure class
149
- * is the entry fault of the list that holds it, reported here rather than at the `override()` call.
222
+ * One override recorded under the key it answers. The same key overridden by two contributors
223
+ * resolves first-in-wins. A key that is neither a declared view nor a failure class is a fault of
224
+ * the list that holds it, reported here rather than at the `override()` call.
150
225
  */
151
- function recordOverride(build, { key, replacement }) {
226
+ function recordOverride(build, { key, replacement }, index) {
152
227
  if (key.kind === 'invalid') {
153
- throw new DeclarationError(entryFault(build.subject));
228
+ throw new DeclarationError(overrideKey, {
229
+ correction: 'Key the override on a value view(identity, definition) returned, or on a failure class.',
230
+ findings: [partFinding(build.site, [index])],
231
+ sentence: `${build.subject.sentence} overrides a key that is neither a declared view nor a failure class.`,
232
+ });
154
233
  }
234
+ claimKey(build, key, index);
155
235
  if (key.kind === 'failure') {
156
- recordFailureOverride(build, key, replacement);
236
+ build.contributions.failures.set(key.prototype, replacement);
157
237
  return;
158
238
  }
159
- recordViewOverride(build, key.view, replacement);
239
+ // Naming a declared view as a key registers its identity, as listing the declaration does.
240
+ registerIdentity(build.identities, key.view, partFinding(build.site, [index]));
241
+ build.contributions.views.set(key.view, replacement);
160
242
  }
161
- /** One failure class answers to one override inside one contributor, keyed by its prototype. */
162
- function recordFailureOverride({ contributions, subject }, key, replacement) {
163
- if (contributions.failures.has(key.prototype)) {
164
- throw new DeclarationError(`${subject.sentence} overrides the view for "${key.name}" twice. Remove one override.`);
243
+ /**
244
+ * One entry of a contributor's list: a declared view, which only a plugin may list, or an
245
+ * override. Any other value is the list's entry fault.
246
+ */
247
+ function recordEntry(build, entry, index) {
248
+ const { identities, site, subject } = build;
249
+ const place = partFinding(site, [index]);
250
+ const object = typeof entry === 'object' && entry !== null ? entry : undefined;
251
+ const listed = object === undefined ? undefined : declarations.get(object);
252
+ const record = object === undefined ? undefined : overrides.get(object);
253
+ if (listed && subject.declares) {
254
+ registerIdentity(identities, listed, place);
165
255
  }
166
- contributions.failures.set(key.prototype, replacement);
167
- }
168
- /** Naming a declared view as a key registers its identity, as listing the declaration does. */
169
- function recordViewOverride({ contributions, identities, subject }, key, replacement) {
170
- registerIdentity(identities, key);
171
- if (contributions.views.has(key)) {
172
- throw new DeclarationError(`${subject.sentence} overrides view "${key.identity}" twice. Remove one override.`);
256
+ else if (record) {
257
+ recordOverride(build, record, index);
258
+ }
259
+ else {
260
+ throw entryFault(subject, place);
173
261
  }
174
- contributions.views.set(key, replacement);
175
262
  }
176
263
  /**
177
264
  * One contributor's own overrides, with the identity of every declared view it lists or names as a
@@ -180,17 +267,10 @@ function recordViewOverride({ contributions, identities, subject }, key, replace
180
267
  */
181
268
  function buildViews(subject, declared, identities) {
182
269
  const contributions = { failures: new Map(), views: new Map() };
183
- for (const entry of readContributions(subject, declared)) {
184
- const listed = typeof entry === 'object' && entry !== null ? declarations.get(entry) : undefined;
185
- if (listed && !subject.declares) {
186
- throw new DeclarationError(entryFault(subject));
187
- }
188
- if (listed) {
189
- registerIdentity(identities, listed);
190
- }
191
- else {
192
- recordOverride({ contributions, identities, subject }, overrideOf(subject, entry));
193
- }
270
+ const site = listSite(subject, declared);
271
+ const build = { contributions, identities, positions: new Map(), site, subject };
272
+ for (const [index, entry] of readContributions(subject, declared).entries()) {
273
+ recordEntry(build, entry, index);
194
274
  }
195
275
  return contributions;
196
276
  }
@@ -198,9 +278,10 @@ function buildViews(subject, declared, identities) {
198
278
  function readStored(stored) {
199
279
  // Last resort: no typed path exists.
200
280
  // A registry holds one entry per key and cannot carry a type parameter per entry.
201
- // A stored view value therefore reads back with its data type erased.
281
+ // A stored view value therefore reads back with its data type and its context erased.
202
282
  // It holds because `override(key, replacement)` typed the replacement against its key's data.
203
283
  // Resolution reaches a stored value through that key alone.
284
+ // A failure key is resolved by `describeFailure` alone, which passes the failure view context.
204
285
  // oxlint-disable-next-line typescript/no-unsafe-type-assertion
205
286
  return stored;
206
287
  }
@@ -217,16 +298,6 @@ function missing(name, key) {
217
298
  function callView(stored, key) {
218
299
  return readStored(stored).render ?? missing('render', key);
219
300
  }
220
- /** Every prototype in a failure's chain, most derived first, so one walk reads one contributor. */
221
- function chainOf(failure) {
222
- const chain = [];
223
- let prototype = Object.getPrototypeOf(failure);
224
- while (prototype !== null) {
225
- chain.push(prototype);
226
- prototype = Object.getPrototypeOf(prototype);
227
- }
228
- return chain;
229
- }
230
301
  /**
231
302
  * The view function one declared view resolves to: the first contributor that overrides it, then
232
303
  * its own default. A bare view is never overridden, so it resolves to its own function.
@@ -267,7 +338,8 @@ function resolveRowView(registry, value) {
267
338
  * for a base class beats a plugin's override for a subclass.
268
339
  */
269
340
  function resolveFailure(registry, failure) {
270
- const chain = chainOf(failure);
341
+ // A failure core reports has a readable chain, so the empty fallback only keeps the walk total.
342
+ const chain = prototypeChain(failure) ?? [];
271
343
  for (const contributor of registry) {
272
344
  for (const prototype of chain) {
273
345
  const replacement = contributor.failures.get(prototype);
@@ -278,30 +350,36 @@ function resolveFailure(registry, failure) {
278
350
  }
279
351
  return undefined;
280
352
  }
353
+ /**
354
+ * Core's own default text for one failure, escaped unless it is the authored marked message of a
355
+ * `FatalError`, with each hint on its own line under it as marked text it does not escape.
356
+ */
357
+ function coreText(failure, text, hints) {
358
+ const sentence = failure instanceof FatalError ? text : escapeText(text);
359
+ return `${sentence}${hints.map((hint) => `${hint}\n`).join('')}`;
360
+ }
281
361
  /**
282
362
  * The text core writes for one failure. Resolution walks the registry as `resolveFailure` defines
283
- * it and falls to core's own default text, which escapes the raw facts it interpolates. A
284
- * `FatalError` keeps the authored marked message it was given.
363
+ * it and falls to core's own default text, which opens a usage failure with the context's
364
+ * application name and escapes the raw facts it interpolates. A `FatalError` keeps the authored
365
+ * marked message it was given. The default text writes each hint on its own line under the
366
+ * sentence, as marked text it does not escape; an override decides for itself. An unrendered report
367
+ * carries the same default text without hints, which the plain fallback path writes.
285
368
  */
286
369
  function describeFailure(registry, failure, context) {
370
+ const text = defaultText(failure, context.application);
287
371
  const replacement = resolveFailure(registry, failure);
288
372
  if (!replacement) {
289
- return {
290
- kind: 'rendered',
291
- text: failure instanceof FatalError ? defaultText(failure) : escapeText(defaultText(failure)),
292
- };
373
+ return { core: true, kind: 'rendered', text: coreText(failure, text, context.hints) };
293
374
  }
294
375
  try {
295
- if (context === undefined) {
296
- throw new Error('Missing rendering context.');
297
- }
298
- const text = callView(replacement, failure.name)(failure, context);
299
- return typeof text === 'string'
300
- ? { kind: 'rendered', text }
301
- : { kind: 'unrendered', reason: notTextReason(text), text: defaultText(failure) };
376
+ const rendered = callView(replacement, failure.name)(failure, context);
377
+ return typeof rendered === 'string'
378
+ ? { core: false, kind: 'rendered', text: rendered }
379
+ : { cause: undefined, kind: 'unrendered', reason: notTextReason(rendered), text };
302
380
  }
303
381
  catch (error) {
304
- return { kind: 'unrendered', reason: reasonOf(error), text: defaultText(failure) };
382
+ return { cause: error, kind: 'unrendered', reason: reasonOf(error), text };
305
383
  }
306
384
  }
307
385
  export { buildViews, describeFailure, override, resolveRowView, resolveView, shapeOf, view, viewIdentities, };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@loomcli/core",
3
- "version": "0.4.0",
3
+ "version": "0.6.0",
4
4
  "description": "The Loom CLI core package. Provides the scaffolding for creating new Loom CLI applications.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -8,7 +8,8 @@
8
8
  "url": "git+https://github.com/dbtlr/loomcli.git"
9
9
  },
10
10
  "files": [
11
- "dist"
11
+ "dist",
12
+ "LICENSE"
12
13
  ],
13
14
  "type": "module",
14
15
  "exports": {