@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
@@ -1,4 +1,8 @@
1
1
  import type { StandardSchemaV1 } from '@standard-schema/spec';
2
+ import type { CaptureFaults } from './capture.js';
3
+ import type { Finding } from './diagnostic-text.js';
4
+ import { DeclarationError } from './errors.js';
5
+ import type { InputSite } from './facts.js';
2
6
  import type { OptionValues } from './options.js';
3
7
  import type { ArgumentConfig, ArgumentValue, Host, OptionConfig, OptionValue } from './types.js';
4
8
  /**
@@ -73,12 +77,62 @@ declare class ValidatedInputs {
73
77
  read(input: InputDeclaration): unknown;
74
78
  }
75
79
  export type { ValidatedInputs };
80
+ /** The faults a config's capture raises: those of every capture, and a default nested too deep. */
81
+ export interface ConfigFaults extends CaptureFaults {
82
+ /** The default holds a path deeper than `defaultLevels`; the finding prints it elided. */
83
+ readonly tooDeep: () => DeclarationError;
84
+ }
85
+ /**
86
+ * Authoring's one read of a config, through `captureDeclaration`: the prototype verdict, the copy of
87
+ * every own string key, the copy of its `extensions` list, and the snapshot of its default, inside
88
+ * one try. Every later check, the registry entry, and every sentence read the copy and never the
89
+ * author's object again, so a getter runs once and the caller's later changes reach nothing. The
90
+ * default is copied and frozen to `defaultLevels` levels, cycles included, and that copy is the
91
+ * value the graph publishes and a run validates; every other property is captured as declared,
92
+ * because core clones no library object. A read that throws, from a getter or a proxy trap, is the
93
+ * unreadable fault, named by the key it threw in, a default nested deeper is the too-deep fault,
94
+ * and a value that is not a plain object is the not-an-object fault.
95
+ */
96
+ export declare function captureConfig<Config extends ArgumentConfig | OptionConfig>(config: Config, faults: ConfigFaults): Config;
97
+ /** The fault of a default nested deeper than `defaultLevels`, declared by `subject`. */
98
+ export declare function defaultDepthFault(subject: string, findings: readonly Finding[]): DeclarationError;
76
99
  /**
77
- * Authoring's snapshot of one config. An array default is the one declared value core hands to an
78
- * action as its own value, so the declaration keeps a copy and the caller keeps its array. Every
79
- * other property is captured as declared, because core clones no library object.
100
+ * The config every `argument()`, `option()`, and `globalOption()` call, and every input a lifecycle
101
+ * hook declares, reads through `captureConfig`. A call in JavaScript that supplies none, or a value
102
+ * of another kind, reports a declaration fault and not a TypeError, and so does a config whose read
103
+ * throws. Each fault marks the config on the call at `place`.
80
104
  */
81
- export declare function captureConfig<Config extends ArgumentConfig | OptionConfig>(config: Config): Config;
105
+ export declare function captureInputConfig<Config extends ArgumentConfig | OptionConfig>(declared: {
106
+ readonly config: Config;
107
+ readonly kind: InputDeclaration['kind'];
108
+ readonly name: string;
109
+ }, place: InputPlace): Config;
110
+ /**
111
+ * A declaration error names the declaration, because the author reads the declaration to fix it.
112
+ * An argument declares and reads under one name, so the two namings differ for options alone.
113
+ */
114
+ export declare function declarationSubject(input: InputDeclaration): string;
115
+ /** Where the call that declared one input sits: the call's name and the Command it is on. */
116
+ export type InputPlace = Pick<Finding, 'call' | 'path'>;
117
+ /**
118
+ * Where one input was declared: a global option at its `globalOption()` call, and every other input
119
+ * at its own `argument()` or `option()` call on the Command at `path`.
120
+ */
121
+ export declare function inputPlace(input: InputDeclaration, scope: {
122
+ readonly global: boolean;
123
+ readonly path: readonly string[];
124
+ }): InputPlace;
125
+ /**
126
+ * The site of the call that declared one input at `place`, rebuilt as `call(name, config)`. Every
127
+ * finding for an input's own call starts from it, so each marks the call the same way. `subject`
128
+ * is the sentence's name for the input.
129
+ */
130
+ export declare function declaringSite(input: InputDeclaration, place: InputPlace, subject?: string): InputSite;
131
+ /**
132
+ * One input's site with its config printed elided, for a fault judged before the config is read,
133
+ * such as an invalid name, so the fault reads none of the config.
134
+ */
135
+ export declare function configUnread(site: InputSite): InputSite;
82
136
  /**
83
137
  * The declaration flag that sends an omitted value to its own validator. Every declaration reads it
84
138
  * here, and the declaration rules below reject it wherever another rule already decides absence.
@@ -90,6 +144,11 @@ export declare function validatesOmission(input: InputDeclaration): boolean;
90
144
  * helper, so a rejected item reads alike wherever its diagnostic is written.
91
145
  */
92
146
  export declare function issuePath(issue: StandardSchemaV1.Issue): string | undefined;
147
+ /** One declaration and where it was declared, which its faults' findings rebuild. */
148
+ export interface SitedInput {
149
+ readonly input: InputDeclaration;
150
+ readonly site: InputSite;
151
+ }
93
152
  /**
94
153
  * Every declaration rule that reads the declaration alone. It is synchronous, so the call that
95
154
  * declares an input applies it, and build applies it to an input a lifecycle hook declared; only
@@ -97,10 +156,13 @@ export declare function issuePath(issue: StandardSchemaV1.Issue): string | undef
97
156
  * contributor that declares under its own name, such as a plugin, supplies the subject its
98
157
  * diagnostics read with; every other caller is named by the declaration itself.
99
158
  */
100
- export declare function checkDeclarations(inputs: readonly InputDeclaration[], named?: string): void;
159
+ export declare function checkDeclarations(inputs: readonly SitedInput[], named?: string): void;
160
+ /** The call that declared one input and the Command it sits on, which a default's fault rebuilds. */
161
+ export type InputPlaces = ReadonlyMap<InputDeclaration, InputPlace>;
101
162
  /**
102
163
  * Every declared default, validated before any token is read. The host is captured by then, so a
103
- * default's validator reads the same Host its action will, under the `default` phase.
164
+ * default's validator reads the same Host its action will, under the `default` phase. `places`
165
+ * says where each declaration sits, which a rejected default's finding rebuilds.
104
166
  */
105
- export declare function prepareInputs(inputs: ScopedInputs, host: Host): Promise<DefaultValues>;
167
+ export declare function prepareInputs(inputs: ScopedInputs, host: Host, places: InputPlaces): Promise<DefaultValues>;
106
168
  export declare function validateValues(invocation: Invocation): Promise<ValidatedInputs>;
@@ -1,6 +1,14 @@
1
+ import { captureDeclaration, elidedRead, unreadableFault } from './capture.js';
1
2
  import { schemaOptions } from './context.js';
2
- import { DeclarationError, InputError } from './errors.js';
3
+ import { escapeControlCharacters } from './controls.js';
4
+ import { elided, spelled } from './diagnostic-text.js';
5
+ import { asSentence, DeclarationError, InputError, quoted, reasonOf } from './errors.js';
6
+ import { callSite, factFault, flagFault, partOf, siteFinding } from './facts.js';
7
+ import { booleanOptionValueRule, defaultDepth, defaultLevels, defaultShape, invalidDefault, notAValidator, omissionAlreadyDecided, omissionWithoutValidator, requiredWithDefault, } from './input-rules.js';
3
8
  import { booleanValue } from './options.js';
9
+ import { boundedSnapshot, NestedTooDeepError, shallowList } from './plain.js';
10
+ import { notAnObject } from './plugin-rules.js';
11
+ import { validatorFailed } from './rules.js';
4
12
  /** Every declaration in validation order: the globals first, then the reading Command's own. */
5
13
  function scoped(inputs) {
6
14
  return [
@@ -47,20 +55,98 @@ class ValidatedInputs {
47
55
  }
48
56
  }
49
57
  /**
50
- * Authoring's snapshot of one config. An array default is the one declared value core hands to an
51
- * action as its own value, so the declaration keeps a copy and the caller keeps its array. Every
52
- * other property is captured as declared, because core clones no library object.
58
+ * Authoring's one read of a config, through `captureDeclaration`: the prototype verdict, the copy of
59
+ * every own string key, the copy of its `extensions` list, and the snapshot of its default, inside
60
+ * one try. Every later check, the registry entry, and every sentence read the copy and never the
61
+ * author's object again, so a getter runs once and the caller's later changes reach nothing. The
62
+ * default is copied and frozen to `defaultLevels` levels, cycles included, and that copy is the
63
+ * value the graph publishes and a run validates; every other property is captured as declared,
64
+ * because core clones no library object. A read that throws, from a getter or a proxy trap, is the
65
+ * unreadable fault, named by the key it threw in, a default nested deeper is the too-deep fault,
66
+ * and a value that is not a plain object is the not-an-object fault.
53
67
  */
54
- export function captureConfig(config) {
55
- const value = config.default;
56
- return Array.isArray(value) ? { ...config, default: [...value] } : { ...config };
68
+ export function captureConfig(config, faults) {
69
+ const { notAnObject: notAnObjectFault, tooDeep, unreadable } = faults;
70
+ return captureDeclaration(config, (copy, read) => {
71
+ // Presence is the key, so a declared `default: undefined` stays a default.
72
+ read.nested(copy, 'default', (value) => boundedSnapshot(value, defaultLevels));
73
+ read.nested(copy, 'extensions', shallowList);
74
+ }, {
75
+ notAnObject: notAnObjectFault,
76
+ unreadable: (thrown, slot) => thrown instanceof NestedTooDeepError ? tooDeep() : unreadable(thrown, slot),
77
+ });
78
+ }
79
+ /** The fault of a default nested deeper than `defaultLevels`, declared by `subject`. */
80
+ export function defaultDepthFault(subject, findings) {
81
+ return new DeclarationError(defaultDepth, {
82
+ correction: `Nest a default at most ${String(defaultLevels)} levels deep.`,
83
+ findings,
84
+ sentence: `${subject} default nests deeper than ${String(defaultLevels)} levels.`,
85
+ });
86
+ }
87
+ /**
88
+ * The config every `argument()`, `option()`, and `globalOption()` call, and every input a lifecycle
89
+ * hook declares, reads through `captureConfig`. A call in JavaScript that supplies none, or a value
90
+ * of another kind, reports a declaration fault and not a TypeError, and so does a config whose read
91
+ * throws. Each fault marks the config on the call at `place`.
92
+ */
93
+ export function captureInputConfig(declared, place) {
94
+ const { config, kind, name } = declared;
95
+ const subject = `${kind === 'argument' ? 'Argument' : 'Option'} ${quoted(name)}`;
96
+ // The finding marks the config, or one key inside it, which `shown` stands for when the call prints.
97
+ const findings = (shown, keys = []) => [
98
+ siteFinding(callSite(subject, { arguments: [name, shown], ...place }), ['1', ...keys].join('.')),
99
+ ];
100
+ return captureConfig(config, {
101
+ notAnObject: () => new DeclarationError(notAnObject, {
102
+ correction: `Supply ${kind === 'argument' ? 'an argument' : 'an option'} config object, such as ${kind === 'argument' ? '{}' : "{ type: 'string' }"}.`,
103
+ findings: findings(config),
104
+ sentence: `${subject} declares a config that is not an object.`,
105
+ }),
106
+ // The default's walk stopped at the limit, so the finding prints it elided.
107
+ tooDeep: () => {
108
+ const { keys, shown } = elidedRead('default');
109
+ return defaultDepthFault(subject, findings(shown, keys));
110
+ },
111
+ // A part whose read threw is never read again, so the finding prints it elided.
112
+ unreadable: (thrown, slot) => {
113
+ const { keys, shown } = elidedRead(slot);
114
+ return unreadableFault({ declared: 'config', findings: findings(shown, keys), subject }, thrown);
115
+ },
116
+ });
57
117
  }
58
118
  /**
59
119
  * A declaration error names the declaration, because the author reads the declaration to fix it.
60
120
  * An argument declares and reads under one name, so the two namings differ for options alone.
61
121
  */
62
- function declaredName(input) {
63
- return input.kind === 'argument' ? `Argument "${input.name}"` : `Option "${input.name}"`;
122
+ export function declarationSubject(input) {
123
+ return input.kind === 'argument'
124
+ ? `Argument ${quoted(input.name)}`
125
+ : `Option ${quoted(input.name)}`;
126
+ }
127
+ /**
128
+ * Where one input was declared: a global option at its `globalOption()` call, and every other input
129
+ * at its own `argument()` or `option()` call on the Command at `path`.
130
+ */
131
+ export function inputPlace(input, scope) {
132
+ return scope.global ? { call: 'globalOption', path: [] } : { call: input.kind, path: scope.path };
133
+ }
134
+ /**
135
+ * The site of the call that declared one input at `place`, rebuilt as `call(name, config)`. Every
136
+ * finding for an input's own call starts from it, so each marks the call the same way. `subject`
137
+ * is the sentence's name for the input.
138
+ */
139
+ export function declaringSite(input, place, subject = declarationSubject(input)) {
140
+ return callSite(subject, { arguments: [input.name, input.config], ...place });
141
+ }
142
+ /**
143
+ * One input's site with its config printed elided, for a fault judged before the config is read,
144
+ * such as an invalid name, so the fault reads none of the config.
145
+ */
146
+ export function configUnread(site) {
147
+ const [name, config] = site.declaration.arguments;
148
+ const shown = config === undefined ? [name] : [name, spelled(elided)];
149
+ return { ...site, declaration: { ...site.declaration, arguments: shown } };
64
150
  }
65
151
  /**
66
152
  * The token an operator would type for one declaration: `--file` for an option, `-F` when the
@@ -143,61 +229,88 @@ function holdsRawDefault(input) {
143
229
  * rule that already decides absence rejects it, and the flag needs a validator to receive the
144
230
  * omission.
145
231
  */
146
- function checkOmissionValidation(input, subject) {
232
+ function checkOmissionValidation(input, site, subject) {
147
233
  const { config } = input;
234
+ const decided = (sentence, correction) => factFault(omissionAlreadyDecided, site, { correction, fact: 'validateOmitted', sentence });
148
235
  if (config.required) {
149
- throw new DeclarationError(`${subject} is required and declares validateOmitted. Remove validateOmitted or make the input optional.`);
236
+ throw decided(`${subject} is required and declares validateOmitted.`, 'Remove validateOmitted or make the input optional.');
150
237
  }
151
238
  if (hasDefault(input)) {
152
- throw new DeclarationError(`${subject} declares a default and validateOmitted. Remove one; the default already fills an omitted value.`);
239
+ throw decided(`${subject} declares a default and validateOmitted.`, 'Remove one; the default already fills an omitted value.');
153
240
  }
154
241
  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.`);
242
+ 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
243
  }
157
244
  if (config.validate === undefined) {
158
- throw new DeclarationError(`${subject} declares validateOmitted without a validator. Add validate or remove validateOmitted.`);
245
+ throw factFault(omissionWithoutValidator, site, {
246
+ correction: 'Add validate or remove validateOmitted.',
247
+ fact: 'validateOmitted',
248
+ sentence: `${subject} declares validateOmitted without a validator.`,
249
+ });
159
250
  }
160
251
  }
161
- function checkDeclaration(input, subject) {
252
+ /** The value rules a Boolean option may not declare, in the order its diagnostic names them. */
253
+ const valueRules = ['validate', 'default', 'required', 'validateOmitted'];
254
+ /** Whether a `validate` value is a Standard Schema v1 object that core can call. */
255
+ function isStandardSchema(validator) {
256
+ if (validator === null || (typeof validator !== 'object' && typeof validator !== 'function')) {
257
+ return false;
258
+ }
259
+ const props = Reflect.get(validator, '~standard');
260
+ return (typeof props === 'object' &&
261
+ props !== null &&
262
+ Reflect.get(props, 'version') === 1 &&
263
+ typeof Reflect.get(props, 'vendor') === 'string' &&
264
+ typeof Reflect.get(props, 'validate') === 'function');
265
+ }
266
+ function checkDeclaration(input, site, subject) {
162
267
  const { config } = input;
163
268
  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.`);
269
+ const declared = valueRules.find((key) => key in config);
270
+ if (declared !== undefined) {
271
+ throw factFault(booleanOptionValueRule, site, {
272
+ correction: `Remove ${declared}; use polarity to control its absent value.`,
273
+ fact: declared,
274
+ sentence: `${subject} is Boolean and declares ${declared}.`,
275
+ });
169
276
  }
170
277
  return;
171
278
  }
172
279
  if (config.required !== undefined && typeof config.required !== 'boolean') {
173
- throw new DeclarationError(`${subject} required must be Boolean. Use true or false.`);
280
+ throw flagFault(site, 'required');
174
281
  }
175
282
  if (input.kind === 'argument' &&
176
283
  input.config.variadic !== undefined &&
177
284
  typeof input.config.variadic !== 'boolean') {
178
- throw new DeclarationError(`${subject} variadic must be Boolean. Use true or false.`);
285
+ throw flagFault(site, 'variadic');
179
286
  }
180
287
  // The test reads presence, not truth, so a declared `undefined` is a declaration to reject.
181
288
  if ('validateOmitted' in config && typeof config.validateOmitted !== 'boolean') {
182
- throw new DeclarationError(`${subject} validateOmitted must be Boolean. Use true or false.`);
289
+ throw flagFault(site, 'validateOmitted');
183
290
  }
184
291
  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.`);
292
+ throw factFault(requiredWithDefault, site, {
293
+ correction: 'Remove the default or make the input optional.',
294
+ fact: 'default',
295
+ sentence: `${subject} is required and declares a default.`,
296
+ });
186
297
  }
187
298
  if (validatesOmission(input)) {
188
- checkOmissionValidation(input, subject);
299
+ checkOmissionValidation(input, site, subject);
189
300
  }
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.`);
301
+ if (config.validate !== undefined && !isStandardSchema(config.validate)) {
302
+ throw factFault(notAValidator, site, {
303
+ correction: 'Supply a compatible validator.',
304
+ fact: 'validate',
305
+ sentence: `${subject} validate must be a Standard Schema v1 object.`,
306
+ });
199
307
  }
200
308
  }
309
+ /**
310
+ * One issue a validator returned, checked where it enters. Core keeps every own enumerable field
311
+ * the issue carries and rewrites only `path`, reducing each segment to its key, so a field core
312
+ * never reads, such as a validator's issue code, reaches a failure view unchanged.
313
+ */
201
314
  function readIssue(issue) {
202
315
  if (issue === null || typeof issue !== 'object' || !('message' in issue)) {
203
316
  throw new Error('The validator returned an invalid Standard Schema issue.');
@@ -208,7 +321,7 @@ function readIssue(issue) {
208
321
  }
209
322
  const suppliedPath = 'path' in issue ? issue.path : undefined;
210
323
  if (suppliedPath === undefined) {
211
- return { message };
324
+ return { ...issue, message };
212
325
  }
213
326
  if (!Array.isArray(suppliedPath)) {
214
327
  throw new Error('The validator returned an invalid Standard Schema issue path.');
@@ -220,13 +333,15 @@ function readIssue(issue) {
220
333
  }
221
334
  return key;
222
335
  });
223
- return { message, path };
336
+ return { ...issue, message, path };
224
337
  }
225
338
  /**
226
339
  * 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.
340
+ * names the declaration and its finding marks the validator on the call at `place`. Returned issues
341
+ * belong to the value, so the caller names those.
228
342
  */
229
- async function validate(input, raw, context) {
343
+ async function validate(input, raw, call) {
344
+ const { context, place } = call;
230
345
  const validator = input.config.validate;
231
346
  if (validator === undefined) {
232
347
  return { value: raw };
@@ -246,8 +361,13 @@ async function validate(input, raw, context) {
246
361
  return { issues: Array.from(issues, readIssue) };
247
362
  }
248
363
  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.`);
364
+ // The reason is the author's detail: a distributed build shows the generic defect message.
365
+ const site = place === undefined ? undefined : declaringSite(input, place);
366
+ throw new DeclarationError(validatorFailed, {
367
+ correction: 'Fix the validator.',
368
+ findings: site === undefined ? [] : [siteFinding(site, partOf(site, 'validate'))],
369
+ sentence: `${declarationSubject(input)} validator failed unexpectedly: ${asSentence(reasonOf(error))}`,
370
+ }, { cause: error });
251
371
  }
252
372
  }
253
373
  /**
@@ -258,13 +378,13 @@ async function validate(input, raw, context) {
258
378
  * next call. The host is the one captured object that every call and the action share.
259
379
  */
260
380
  async function validateDeclared(input, raw, call) {
261
- const { context, signal } = call;
381
+ const { context, place, signal } = call;
262
382
  if (!collects(input) || input.config.validate === undefined) {
263
- return validate(input, raw, context());
383
+ return validate(input, raw, { context: context(), place });
264
384
  }
265
385
  if (!Array.isArray(raw)) {
266
386
  // 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.`);
387
+ throw new TypeError(`${declarationSubject(input)} reached validation without an array of values.`);
268
388
  }
269
389
  const outputs = [];
270
390
  const issues = [];
@@ -273,15 +393,15 @@ async function validateDeclared(input, raw, call) {
273
393
  // A cancelled run starts no further call; the run resolves its cancellation code instead.
274
394
  break;
275
395
  }
276
- const result = await validate(input, value, context());
396
+ const result = await validate(input, value, { context: context(), place });
277
397
  if (result.issues === undefined) {
278
398
  outputs.push(result.value);
279
399
  }
280
400
  else {
281
- issues.push(...reported(result.issues).map((issue) => ({
282
- message: issue.message,
283
- path: [position, ...(issue.path ?? [])],
284
- })));
401
+ // The issue keeps its own fields, and only its path gains the value's position.
402
+ for (const issue of reported(result.issues)) {
403
+ issues.push({ ...issue, path: [position, ...(issue.path ?? [])] });
404
+ }
285
405
  }
286
406
  }
287
407
  return issues.length === 0 ? { value: outputs } : { issues };
@@ -304,23 +424,33 @@ export function issuePath(issue) {
304
424
  */
305
425
  function reported(issues) {
306
426
  return issues.length === 0
307
- ? [{ message: 'The validator rejected this value without an explanation.' }]
427
+ ? [
428
+ {
429
+ message: 'The validator rejected this value without an explanation. Supply a different value.',
430
+ },
431
+ ]
308
432
  : issues;
309
433
  }
434
+ /**
435
+ * One line per issue under the input's subject. A path segment can hold text the operator typed,
436
+ * such as a key of a record, so the line escapes the path; `issuePath` keeps it raw for a view.
437
+ */
310
438
  function messages(subject, issues) {
311
439
  return issues.map((issue) => {
312
440
  const path = issuePath(issue);
313
- return `${subject}${path === undefined ? '' : ` at ${path}`}: ${issue.message}`;
441
+ const position = path === undefined ? '' : ` at ${escapeControlCharacters(path)}`;
442
+ return `${subject}${position}: ${issue.message}`;
314
443
  });
315
444
  }
316
445
  /** The fault a default of the wrong raw shape reports, by the shape its declaration expects. */
317
- function defaultShapeFault(input, subject) {
446
+ function defaultShapeFault(input, site, subject) {
447
+ const fault = (sentence, correction) => factFault(defaultShape, site, { correction, fact: 'default', sentence });
318
448
  if (!collects(input)) {
319
- return `${subject} default must be a string without a validator. Supply a string default.`;
449
+ return fault(`${subject} default must be a string without a validator.`, 'Supply a string default.');
320
450
  }
321
451
  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.`;
452
+ ? fault(`${subject} default must be an array of strings without a validator.`, 'Supply a string array default.')
453
+ : fault(`${subject} default must be an array.`, 'Supply an array of values.');
324
454
  }
325
455
  /** A declared `default: undefined` is a default, so presence is the key, never the value. */
326
456
  function hasDefault(input) {
@@ -334,44 +464,57 @@ function hasDefault(input) {
334
464
  * diagnostics read with; every other caller is named by the declaration itself.
335
465
  */
336
466
  export function checkDeclarations(inputs, named) {
337
- for (const input of inputs) {
338
- checkDeclaration(input, named ?? declaredName(input));
467
+ for (const { input, site } of inputs) {
468
+ checkDeclaration(input, site, named ?? declarationSubject(input));
339
469
  }
340
- for (const input of inputs.filter((entry) => hasDefault(entry))) {
470
+ for (const { input, site } of inputs.filter((entry) => hasDefault(entry.input))) {
341
471
  if (!holdsRawDefault(input)) {
342
- const subject = named ?? declaredName(input);
343
- throw new DeclarationError(defaultShapeFault(input, subject));
472
+ throw defaultShapeFault(input, site, named ?? declarationSubject(input));
344
473
  }
345
474
  }
346
475
  }
347
476
  /**
348
477
  * 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.
478
+ * default's validator reads the same Host its action will, under the `default` phase. `places`
479
+ * says where each declaration sits, which a rejected default's finding rebuilds.
350
480
  */
351
- export async function prepareInputs(inputs, host) {
481
+ export async function prepareInputs(inputs, host, places) {
352
482
  const declarations = scoped(inputs);
353
483
  const defaults = new Map();
354
484
  for (const entry of declarations.filter(({ input }) => hasDefault(input))) {
355
485
  const { input } = entry;
356
- const subject = declaredName(input);
486
+ const subject = declarationSubject(input);
487
+ const place = places.get(input);
357
488
  const result = await validateDeclared(input, input.config.default, {
358
489
  context: () => ({ host, input: identityOf(entry), phase: 'default' }),
490
+ place,
359
491
  });
360
492
  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')}`);
493
+ const site = place === undefined ? undefined : declaringSite(input, place);
494
+ throw new DeclarationError(invalidDefault, {
495
+ correction: 'Fix the default or its validator.',
496
+ findings: site === undefined ? [] : [siteFinding(site, partOf(site, 'default'))],
497
+ sentence: [
498
+ `${subject} has an invalid default.`,
499
+ ...messages(subject, reported(result.issues)),
500
+ ].join('\n'),
501
+ });
362
502
  }
363
503
  defaults.set(input, result.value);
364
504
  }
365
505
  return defaults;
366
506
  }
367
507
  /**
368
- * An array default reaches the action as its own copy, so an action that mutates its array
508
+ * An array default reaches the action as its own mutable copy, so an action that mutates its array
369
509
  * rewrites neither the declaration nor the next invocation. One prepared default serves every
370
- * invocation of a run, and an unvalidated default is the declared array itself, so each read
371
- * copies it. Every other output passes through unchanged.
510
+ * invocation of a run, and an unvalidated default is the frozen snapshot the declaring call took,
511
+ * so each read copies it. The copy keeps a hole where the default has one, as the graph's does.
512
+ * Every other output passes through unchanged.
372
513
  */
373
514
  function freshDefault(value) {
374
- return Array.isArray(value) ? [...value] : value;
515
+ // A spread reads a hole as `undefined`, and `slice` keeps it.
516
+ // oxlint-disable-next-line unicorn/prefer-spread
517
+ return Array.isArray(value) ? value.slice() : value;
375
518
  }
376
519
  /**
377
520
  * The raw tokens of one invocation, keyed by declared name. Every declared input of the routed
@@ -461,6 +604,7 @@ export async function validateValues(invocation) {
461
604
  const accept = async (entry, raw, spelling) => {
462
605
  const result = await validateDeclared(entry.input, raw, {
463
606
  context: () => contextOf(entry),
607
+ place: inputPlace(entry.input, { global: entry.global, path: invocation.command }),
464
608
  signal: invocation.signal,
465
609
  });
466
610
  if (result.issues === undefined) {