@loomcli/core 0.5.0 → 0.7.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 (90) hide show
  1. package/NOTICE +34 -0
  2. package/dist/application.d.ts +18 -2
  3. package/dist/application.js +302 -75
  4. package/dist/bindings.d.ts +15 -10
  5. package/dist/bindings.js +34 -15
  6. package/dist/capture.d.ts +65 -0
  7. package/dist/capture.js +99 -0
  8. package/dist/chain.d.ts +15 -6
  9. package/dist/chain.js +40 -20
  10. package/dist/command-rules.d.ts +55 -0
  11. package/dist/command-rules.js +142 -0
  12. package/dist/command.d.ts +65 -23
  13. package/dist/command.js +734 -236
  14. package/dist/controls.d.ts +8 -0
  15. package/dist/controls.js +23 -0
  16. package/dist/defect.d.ts +18 -0
  17. package/dist/defect.js +272 -0
  18. package/dist/developer.d.ts +24 -0
  19. package/dist/developer.js +52 -0
  20. package/dist/diagnostic-text.d.ts +81 -0
  21. package/dist/diagnostic-text.js +283 -0
  22. package/dist/diagnostic.d.ts +11 -0
  23. package/dist/diagnostic.js +70 -0
  24. package/dist/errors.d.ts +108 -24
  25. package/dist/errors.js +324 -53
  26. package/dist/exit-codes.d.ts +45 -0
  27. package/dist/exit-codes.js +46 -0
  28. package/dist/extension.d.ts +41 -8
  29. package/dist/extension.js +142 -57
  30. package/dist/facts.d.ts +71 -11
  31. package/dist/facts.js +106 -23
  32. package/dist/globals.d.ts +29 -21
  33. package/dist/globals.js +117 -46
  34. package/dist/glyphs.generated.js +1 -1
  35. package/dist/hints.d.ts +79 -0
  36. package/dist/hints.js +247 -0
  37. package/dist/host.d.ts +13 -0
  38. package/dist/host.js +43 -1
  39. package/dist/identity.d.ts +19 -0
  40. package/dist/identity.js +72 -0
  41. package/dist/index.d.ts +12 -2
  42. package/dist/index.js +6 -0
  43. package/dist/input-rules.d.ts +68 -0
  44. package/dist/input-rules.js +152 -0
  45. package/dist/inspect.d.ts +12 -12
  46. package/dist/inspect.js +92 -48
  47. package/dist/lanes.js +1 -1
  48. package/dist/locate.js +4 -4
  49. package/dist/options.d.ts +30 -2
  50. package/dist/options.js +143 -46
  51. package/dist/output.d.ts +9 -2
  52. package/dist/output.js +18 -2
  53. package/dist/plain.d.ts +56 -0
  54. package/dist/plain.js +238 -0
  55. package/dist/plugin-rules.d.ts +68 -0
  56. package/dist/plugin-rules.js +164 -0
  57. package/dist/plugin-settings.d.ts +18 -0
  58. package/dist/plugin-settings.js +38 -0
  59. package/dist/plugin.d.ts +27 -15
  60. package/dist/plugin.js +429 -144
  61. package/dist/prototypes.d.ts +7 -0
  62. package/dist/prototypes.js +29 -0
  63. package/dist/rendering.d.ts +6 -1
  64. package/dist/rendering.js +23 -5
  65. package/dist/rules.d.ts +51 -0
  66. package/dist/rules.js +115 -0
  67. package/dist/sequence.js +6 -1
  68. package/dist/sources.d.ts +6 -4
  69. package/dist/sources.js +25 -16
  70. package/dist/style-layout.js +2 -2
  71. package/dist/style-width.d.ts +13 -0
  72. package/dist/style-width.js +170 -0
  73. package/dist/style-wire.js +1 -1
  74. package/dist/style.js +1 -1
  75. package/dist/theme.d.ts +4 -0
  76. package/dist/theme.js +25 -5
  77. package/dist/thenable.d.ts +15 -0
  78. package/dist/thenable.js +29 -0
  79. package/dist/translators.d.ts +69 -0
  80. package/dist/translators.js +253 -0
  81. package/dist/types.d.ts +13 -3
  82. package/dist/unicode.generated.d.ts +27 -0
  83. package/dist/unicode.generated.js +1036 -0
  84. package/dist/validation.d.ts +69 -7
  85. package/dist/validation.js +211 -67
  86. package/dist/view.d.ts +61 -22
  87. package/dist/view.js +168 -81
  88. package/licenses/unicode-LICENSE.txt +41 -0
  89. package/licenses/uucode-LICENSE.md +35 -0
  90. package/package.json +9 -5
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?: undefined;
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. */
@@ -104,17 +125,22 @@ declare class OverrideDeclaration {
104
125
  type ViewOverride = Pick<OverrideDeclaration, typeof viewOverride>;
105
126
  /** What a plugin's own list holds: the views it declares and the overrides it makes. */
106
127
  type ViewContribution = AnyDeclaredView | ViewOverride;
128
+ /** Every value an override can key on: a declared view of either shape or a failure class. */
129
+ type AnyOverrideKey = AnyDeclaredView | FailureClass<LoomError>;
130
+ /**
131
+ * The replacement view one key takes, derived from the key alone. The declared-view branches come
132
+ * first, so a declared view is never read as a failure class. Each check is wrapped in a tuple so a
133
+ * union key is not split into a union of replacements that each answer only one of its members.
134
+ */
135
+ type ReplacementView<Key> = [Key] extends [DeclaredView<infer Data>] ? View<Data> : [Key] extends [DeclaredRowView<infer Row>] ? RowView<Row> : [Key] extends [FailureClass<infer Failure>] ? FailureView<Failure> : never;
107
136
  /**
108
- * One override pairing a key with a replacement view. Under a declared view the replacement is
109
- * typed from the view's data; under a failure class it is typed from the class's instances, which
110
- * is the typed path for a class-keyed list, because an array literal cannot carry a different type
111
- * parameter per element.
137
+ * One override pairing a key with a replacement view. The replacement's type is derived from the
138
+ * key: under a declared view it is typed from the view's data, and under a failure class from the
139
+ * class's instances, which is the typed path for a class-keyed list, because an array literal cannot
140
+ * carry a different type parameter per element. One signature serves every key, so a mismatch
141
+ * names the replacement's type against the one the key expects.
112
142
  */
113
- declare function override<Data>(key: DeclaredView<Data>, replacement: NoInfer<View<Data>>): ViewOverride;
114
- declare function override<Row>(key: DeclaredRowView<Row>, replacement: NoInfer<RowView<Row>>): ViewOverride;
115
- declare function override<Failure extends LoomError>(key: FailureClass<Failure> & {
116
- readonly [declaredView]?: never;
117
- }, replacement: NoInfer<View<Failure>>): ViewOverride;
143
+ declare function override<Key extends AnyOverrideKey>(key: Key, replacement: NoInfer<ReplacementView<Key>>): ViewOverride;
118
144
  /** One contributor's overrides, read once per build and consulted in contributor order. */
119
145
  interface ViewContributions {
120
146
  failures: Map<unknown, StoredView>;
@@ -127,13 +153,20 @@ interface ViewContributions {
127
153
  type ViewRegistry = readonly ViewContributions[];
128
154
  /** The identities one build has met, so a second object under one identity is visible. */
129
155
  type ViewIdentities = Map<string, AnyDeclaredView>;
130
- /** How one contributor's diagnostics name it, and whether its list may declare a view. */
156
+ /**
157
+ * How one contributor's diagnostics name it, whether its list may declare a view, and the call
158
+ * that declared the list, `plugin(identity, …)` or `new Application(name, …)`, which a fault marks.
159
+ */
131
160
  interface ViewSubject {
132
161
  /** A plugin declares views beside its overrides; an application overrides alone. */
133
162
  declares: boolean;
134
163
  sentence: string;
164
+ owner: {
165
+ call: string;
166
+ named: unknown;
167
+ };
135
168
  }
136
- /** The identity register one build starts from, holding the views core itself declares. */
169
+ /** The identity one build starts from, holding the views core itself declares. */
137
170
  declare function viewIdentities(declared: readonly AnyDeclaredView[]): ViewIdentities;
138
171
  /**
139
172
  * One contributor's own overrides, with the identity of every declared view it lists or names as a
@@ -158,23 +191,29 @@ interface ResolvedRowView<Row> {
158
191
  */
159
192
  declare function resolveRowView<Row>(registry: ViewRegistry, value: RowView<Row>): ResolvedRowView<Row>;
160
193
  /**
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.
194
+ * The report of one failure: the text core writes, and whether a view produced it. A rendered
195
+ * report says whether core's own default text answered, which for a defect is the generic defect
196
+ * message. An unrendered report carries core's own text, which the plain fallback path writes
197
+ * beside the diagnostic naming the view that could not answer, and what the view threw.
164
198
  */
165
199
  type FailureReport = {
166
200
  kind: 'rendered';
167
201
  text: string;
202
+ core: boolean;
168
203
  } | {
169
204
  kind: 'unrendered';
170
205
  text: string;
171
206
  reason: string;
207
+ cause: unknown;
172
208
  };
173
209
  /**
174
210
  * 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.
211
+ * it and falls to core's own default text, which opens a usage failure with the context's
212
+ * application name and escapes the raw facts it interpolates. A `FatalError` keeps the authored
213
+ * marked message it was given. The default text writes each hint on its own line under the
214
+ * sentence, as marked text it does not escape; an override decides for itself. An unrendered report
215
+ * carries the same default text without hints, which the plain fallback path writes.
177
216
  */
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, };
217
+ declare function describeFailure(registry: ViewRegistry, failure: LoomError, context: FailureViewContext): FailureReport;
218
+ export type { AnyDeclaredView, AnyOverrideKey, DeclaredRowView, DeclaredView, DeclaredViewBrand, FailureClass, FailureReport, FailureView, FailureViewContext, ReplacementView, ResolvedRowView, ViewContribution, ViewContributions, ViewSubject, ViewIdentities, ViewOverride, ViewRegistry, ViewShape, };
180
219
  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)
@@ -68,12 +84,21 @@ class OverrideDeclaration {
68
84
  Object.freeze(this);
69
85
  }
70
86
  }
87
+ /**
88
+ * One override pairing a key with a replacement view. The replacement's type is derived from the
89
+ * key: under a declared view it is typed from the view's data, and under a failure class from the
90
+ * class's instances, which is the typed path for a class-keyed list, because an array literal cannot
91
+ * carry a different type parameter per element. One signature serves every key, so a mismatch
92
+ * names the replacement's type against the one the key expects.
93
+ */
71
94
  function override(key, replacement) {
95
+ // Every replacement a key derives is a stored view, so it widens here with no check.
96
+ const supplied = replacement;
72
97
  const stored = {
73
- head: replacement.head,
74
- render: replacement.render,
75
- row: replacement.row,
76
- tail: replacement.tail,
98
+ head: supplied.head,
99
+ render: supplied.render,
100
+ row: supplied.row,
101
+ tail: supplied.tail,
77
102
  };
78
103
  const declared = declarations.get(key);
79
104
  if (declared) {
@@ -97,7 +122,7 @@ function failureKey(key) {
97
122
  const name = 'name' in key && typeof key.name === 'string' && key.name !== '' ? key.name : 'a failure class';
98
123
  return { kind: 'failure', name, prototype };
99
124
  }
100
- /** The identity register one build starts from, holding the views core itself declares. */
125
+ /** The identity one build starts from, holding the views core itself declares. */
101
126
  function viewIdentities(declared) {
102
127
  const identities = new Map();
103
128
  for (const value of declared) {
@@ -105,22 +130,63 @@ function viewIdentities(declared) {
105
130
  }
106
131
  return identities;
107
132
  }
108
- /** One identity means one declared view, wherever on the graph that view appears. */
109
- function registerIdentity(identities, declared) {
133
+ /**
134
+ * One identity means one declared view, wherever on the graph that view appears. `place` marks the
135
+ * list entry that names the view, when one does.
136
+ */
137
+ function registerIdentity(identities, declared, place) {
110
138
  const known = identities.get(declared.identity);
111
139
  if (known === undefined) {
112
140
  identities.set(declared.identity, declared);
113
141
  return;
114
142
  }
115
143
  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.`);
144
+ throw new DeclarationError(twoPackageCopies, {
145
+ correction: 'Install one copy of the package that declares it.',
146
+ findings: place === undefined ? [] : [{ ...place, note: `another ${quoted(declared.identity)}` }],
147
+ sentence: `View ${quoted(declared.identity)} is declared by two distinct objects.`,
148
+ });
149
+ }
150
+ }
151
+ /** A name JavaScript source can spell as an identifier, which a finding prints a class key as. */
152
+ const identifier = /^[A-Za-z_$][\w$]*$/u;
153
+ /** How a finding prints an override's key: a failure class by its name, anything else elided. */
154
+ function keyCode(key) {
155
+ return key.kind === 'failure' && identifier.test(key.name) ? key.name : elided;
156
+ }
157
+ /**
158
+ * One list entry as a finding prints it: a declared view as the `view()` call that made it, an
159
+ * override as its `override()` call, and any other value as it is.
160
+ */
161
+ function entryCode(entry) {
162
+ if (typeof entry !== 'object' || entry === null) {
163
+ return entry;
117
164
  }
165
+ const listed = declarations.get(entry);
166
+ if (listed) {
167
+ return spelled(`view(${quoteString(listed.identity)}, ${elided})`);
168
+ }
169
+ const record = overrides.get(entry);
170
+ return record ? spelled(`override(${keyCode(record.key)}, ${elided})`) : entry;
171
+ }
172
+ /** Where one contributor's `views` list sits, with each entry printed as the call that made it. */
173
+ function listSite(subject, declared) {
174
+ const views = Array.isArray(declared) ? Array.from(declared, entryCode) : declared;
175
+ return slotSite({ ...subject.owner, subject: subject.sentence }, 'views', views);
118
176
  }
119
- /** The sentence one contributor's list reports for a value it cannot read. */
120
- function entryFault(subject) {
177
+ /** The fault of one list entry that is not a value this contributor's list may hold. */
178
+ function entryFault(subject, place) {
121
179
  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).`;
180
+ ? new DeclarationError(foreignValue, {
181
+ correction: 'Supply the value returned by view(identity, definition) or override(key, view).',
182
+ findings: [place],
183
+ sentence: `${subject.sentence} holds a value that is not a view.`,
184
+ })
185
+ : new DeclarationError(foreignValue, {
186
+ correction: 'Supply the value returned by override(key, view).',
187
+ findings: [place],
188
+ sentence: `${subject.sentence} holds a value that is not a view override.`,
189
+ });
124
190
  }
125
191
  /** A `views` slot holds a list, so anything else is the same declaration fault. */
126
192
  function readContributions(subject, declared) {
@@ -128,50 +194,80 @@ function readContributions(subject, declared) {
128
194
  return [];
129
195
  }
130
196
  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));
197
+ throw new DeclarationError(notAList, {
198
+ correction: subject.declares
199
+ ? 'Supply a list of declared views and override values.'
200
+ : 'Supply a list of override values.',
201
+ findings: [partFinding(listSite(subject, declared), [])],
202
+ sentence: `${subject.sentence} declares views that are not an array.`,
203
+ });
134
204
  }
135
205
  return declared;
136
206
  }
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));
207
+ /**
208
+ * The entry a key's first override sits at, recording this one's when it is the first. One key
209
+ * answers to one override inside one contributor, so a second override marks both entries.
210
+ */
211
+ function claimKey(build, key, index) {
212
+ const slot = key.kind === 'failure' ? key.prototype : key.view;
213
+ const first = build.positions.get(slot);
214
+ if (first === undefined) {
215
+ build.positions.set(slot, index);
216
+ return;
142
217
  }
143
- return record;
218
+ const clause = key.kind === 'failure'
219
+ ? `the view for ${quoted(key.name)}`
220
+ : `view ${quoted(key.view.identity)}`;
221
+ throw new DeclarationError(overrideTwice, {
222
+ correction: 'Remove one override.',
223
+ findings: [
224
+ partFinding(build.site, [first], 'the first override'),
225
+ partFinding(build.site, [index], 'the second override'),
226
+ ],
227
+ sentence: `${build.subject.sentence} overrides ${clause} twice.`,
228
+ });
144
229
  }
145
230
  /**
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.
231
+ * One override recorded under the key it answers. The same key overridden by two contributors
232
+ * resolves first-in-wins. A key that is neither a declared view nor a failure class is a fault of
233
+ * the list that holds it, reported here rather than at the `override()` call.
150
234
  */
151
- function recordOverride(build, { key, replacement }) {
235
+ function recordOverride(build, { key, replacement }, index) {
152
236
  if (key.kind === 'invalid') {
153
- throw new DeclarationError(entryFault(build.subject));
237
+ throw new DeclarationError(overrideKey, {
238
+ correction: 'Key the override on a value view(identity, definition) returned, or on a failure class.',
239
+ findings: [partFinding(build.site, [index])],
240
+ sentence: `${build.subject.sentence} overrides a key that is neither a declared view nor a failure class.`,
241
+ });
154
242
  }
243
+ claimKey(build, key, index);
155
244
  if (key.kind === 'failure') {
156
- recordFailureOverride(build, key, replacement);
245
+ build.contributions.failures.set(key.prototype, replacement);
157
246
  return;
158
247
  }
159
- recordViewOverride(build, key.view, replacement);
248
+ // Naming a declared view as a key registers its identity, as listing the declaration does.
249
+ registerIdentity(build.identities, key.view, partFinding(build.site, [index]));
250
+ build.contributions.views.set(key.view, replacement);
160
251
  }
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.`);
252
+ /**
253
+ * One entry of a contributor's list: a declared view, which only a plugin may list, or an
254
+ * override. Any other value is the list's entry fault.
255
+ */
256
+ function recordEntry(build, entry, index) {
257
+ const { identities, site, subject } = build;
258
+ const place = partFinding(site, [index]);
259
+ const object = typeof entry === 'object' && entry !== null ? entry : undefined;
260
+ const listed = object === undefined ? undefined : declarations.get(object);
261
+ const record = object === undefined ? undefined : overrides.get(object);
262
+ if (listed && subject.declares) {
263
+ registerIdentity(identities, listed, place);
165
264
  }
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.`);
265
+ else if (record) {
266
+ recordOverride(build, record, index);
267
+ }
268
+ else {
269
+ throw entryFault(subject, place);
173
270
  }
174
- contributions.views.set(key, replacement);
175
271
  }
176
272
  /**
177
273
  * One contributor's own overrides, with the identity of every declared view it lists or names as a
@@ -180,17 +276,10 @@ function recordViewOverride({ contributions, identities, subject }, key, replace
180
276
  */
181
277
  function buildViews(subject, declared, identities) {
182
278
  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
- }
279
+ const site = listSite(subject, declared);
280
+ const build = { contributions, identities, positions: new Map(), site, subject };
281
+ for (const [index, entry] of readContributions(subject, declared).entries()) {
282
+ recordEntry(build, entry, index);
194
283
  }
195
284
  return contributions;
196
285
  }
@@ -198,9 +287,10 @@ function buildViews(subject, declared, identities) {
198
287
  function readStored(stored) {
199
288
  // Last resort: no typed path exists.
200
289
  // 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.
290
+ // A stored view value therefore reads back with its data type and its context erased.
202
291
  // It holds because `override(key, replacement)` typed the replacement against its key's data.
203
292
  // Resolution reaches a stored value through that key alone.
293
+ // A failure key is resolved by `describeFailure` alone, which passes the failure view context.
204
294
  // oxlint-disable-next-line typescript/no-unsafe-type-assertion
205
295
  return stored;
206
296
  }
@@ -217,16 +307,6 @@ function missing(name, key) {
217
307
  function callView(stored, key) {
218
308
  return readStored(stored).render ?? missing('render', key);
219
309
  }
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
310
  /**
231
311
  * The view function one declared view resolves to: the first contributor that overrides it, then
232
312
  * its own default. A bare view is never overridden, so it resolves to its own function.
@@ -267,7 +347,8 @@ function resolveRowView(registry, value) {
267
347
  * for a base class beats a plugin's override for a subclass.
268
348
  */
269
349
  function resolveFailure(registry, failure) {
270
- const chain = chainOf(failure);
350
+ // A failure core reports has a readable chain, so the empty fallback only keeps the walk total.
351
+ const chain = prototypeChain(failure) ?? [];
271
352
  for (const contributor of registry) {
272
353
  for (const prototype of chain) {
273
354
  const replacement = contributor.failures.get(prototype);
@@ -278,30 +359,36 @@ function resolveFailure(registry, failure) {
278
359
  }
279
360
  return undefined;
280
361
  }
362
+ /**
363
+ * Core's own default text for one failure, escaped unless it is the authored marked message of a
364
+ * `FatalError`, with each hint on its own line under it as marked text it does not escape.
365
+ */
366
+ function coreText(failure, text, hints) {
367
+ const sentence = failure instanceof FatalError ? text : escapeText(text);
368
+ return `${sentence}${hints.map((hint) => `${hint}\n`).join('')}`;
369
+ }
281
370
  /**
282
371
  * 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.
372
+ * it and falls to core's own default text, which opens a usage failure with the context's
373
+ * application name and escapes the raw facts it interpolates. A `FatalError` keeps the authored
374
+ * marked message it was given. The default text writes each hint on its own line under the
375
+ * sentence, as marked text it does not escape; an override decides for itself. An unrendered report
376
+ * carries the same default text without hints, which the plain fallback path writes.
285
377
  */
286
378
  function describeFailure(registry, failure, context) {
379
+ const text = defaultText(failure, context.application);
287
380
  const replacement = resolveFailure(registry, failure);
288
381
  if (!replacement) {
289
- return {
290
- kind: 'rendered',
291
- text: failure instanceof FatalError ? defaultText(failure) : escapeText(defaultText(failure)),
292
- };
382
+ return { core: true, kind: 'rendered', text: coreText(failure, text, context.hints) };
293
383
  }
294
384
  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) };
385
+ const rendered = callView(replacement, failure.name)(failure, context);
386
+ return typeof rendered === 'string'
387
+ ? { core: false, kind: 'rendered', text: rendered }
388
+ : { cause: undefined, kind: 'unrendered', reason: notTextReason(rendered), text };
302
389
  }
303
390
  catch (error) {
304
- return { kind: 'unrendered', reason: reasonOf(error), text: defaultText(failure) };
391
+ return { cause: error, kind: 'unrendered', reason: reasonOf(error), text };
305
392
  }
306
393
  }
307
394
  export { buildViews, describeFailure, override, resolveRowView, resolveView, shapeOf, view, viewIdentities, };
@@ -0,0 +1,41 @@
1
+ https://www.unicode.org/license.txt
2
+
3
+ UNICODE LICENSE V3
4
+
5
+ COPYRIGHT AND PERMISSION NOTICE
6
+
7
+ Copyright © 1991-2025 Unicode, Inc.
8
+
9
+ NOTICE TO USER: Carefully read the following legal agreement. BY
10
+ DOWNLOADING, INSTALLING, COPYING OR OTHERWISE USING DATA FILES, AND/OR
11
+ SOFTWARE, YOU UNEQUIVOCALLY ACCEPT, AND AGREE TO BE BOUND BY, ALL OF THE
12
+ TERMS AND CONDITIONS OF THIS AGREEMENT. IF YOU DO NOT AGREE, DO NOT
13
+ DOWNLOAD, INSTALL, COPY, DISTRIBUTE OR USE THE DATA FILES OR SOFTWARE.
14
+
15
+ Permission is hereby granted, free of charge, to any person obtaining a
16
+ copy of data files and any associated documentation (the "Data Files") or
17
+ software and any associated documentation (the "Software") to deal in the
18
+ Data Files or Software without restriction, including without limitation
19
+ the rights to use, copy, modify, merge, publish, distribute, and/or sell
20
+ copies of the Data Files or Software, and to permit persons to whom the
21
+ Data Files or Software are furnished to do so, provided that either (a)
22
+ this copyright and permission notice appear with all copies of the Data
23
+ Files or Software, or (b) this copyright and permission notice appear in
24
+ associated Documentation.
25
+
26
+ THE DATA FILES AND SOFTWARE ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
27
+ KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
28
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF
29
+ THIRD PARTY RIGHTS.
30
+
31
+ IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS NOTICE
32
+ BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL DAMAGES,
33
+ OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,
34
+ WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION,
35
+ ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THE DATA
36
+ FILES OR SOFTWARE.
37
+
38
+ Except as contained in this notice, the name of a copyright holder shall
39
+ not be used in advertising or otherwise to promote the sale, use or other
40
+ dealings in these Data Files or Software without prior written
41
+ authorization of the copyright holder.
@@ -0,0 +1,35 @@
1
+ # uucode license
2
+
3
+ MIT License
4
+
5
+ Copyright (c) 2026 Tim Culverhouse
6
+
7
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
8
+ this software and associated documentation files (the "Software"), to deal in
9
+ the Software without restriction, including without limitation the rights to
10
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
11
+ of the Software, and to permit persons to whom the Software is furnished to do
12
+ so, subject to the following conditions:
13
+
14
+ The above copyright notice and this permission notice shall be included in all
15
+ copies or substantial portions of the Software.
16
+
17
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23
+ SOFTWARE.
24
+
25
+ ## Attribution
26
+
27
+ This TypeScript implementation is based on the design and table strategy of
28
+ Jacob Sandlund's `uucode`:
29
+
30
+ - https://github.com/jacobsandlund/uucode
31
+
32
+ The bundled Unicode Character Database files are provided by Unicode, Inc. and
33
+ are subject to the Unicode terms of use:
34
+
35
+ - https://www.unicode.org/terms_of_use.html
package/package.json CHANGED
@@ -1,15 +1,17 @@
1
1
  {
2
2
  "name": "@loomcli/core",
3
- "version": "0.5.0",
3
+ "version": "0.7.0",
4
4
  "description": "The Loom CLI core package. Provides the scaffolding for creating new Loom CLI applications.",
5
- "license": "MIT",
5
+ "license": "MIT AND Unicode-3.0",
6
6
  "repository": {
7
7
  "type": "git",
8
8
  "url": "git+https://github.com/dbtlr/loomcli.git"
9
9
  },
10
10
  "files": [
11
11
  "dist",
12
- "LICENSE"
12
+ "LICENSE",
13
+ "licenses",
14
+ "NOTICE"
13
15
  ],
14
16
  "type": "module",
15
17
  "exports": {
@@ -19,9 +21,11 @@
19
21
  }
20
22
  },
21
23
  "dependencies": {
22
- "@rockorager/uucode": "2.2.1",
23
24
  "@standard-schema/spec": "^1.1.0",
24
- "@types/node": "22.20.1"
25
+ "@types/node": "22.20.5"
26
+ },
27
+ "devDependencies": {
28
+ "@rockorager/uucode": "2.2.1"
25
29
  },
26
30
  "engines": {
27
31
  "bun": ">=1.4.0",