@loomcli/core 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/dist/application.d.ts +18 -2
  2. package/dist/application.js +266 -71
  3. package/dist/bindings.d.ts +15 -10
  4. package/dist/bindings.js +34 -15
  5. package/dist/chain.d.ts +15 -6
  6. package/dist/chain.js +40 -20
  7. package/dist/command-rules.d.ts +55 -0
  8. package/dist/command-rules.js +142 -0
  9. package/dist/command.d.ts +65 -23
  10. package/dist/command.js +695 -226
  11. package/dist/controls.d.ts +8 -0
  12. package/dist/controls.js +23 -0
  13. package/dist/defect.d.ts +18 -0
  14. package/dist/defect.js +272 -0
  15. package/dist/developer.d.ts +24 -0
  16. package/dist/developer.js +52 -0
  17. package/dist/diagnostic-text.d.ts +81 -0
  18. package/dist/diagnostic-text.js +283 -0
  19. package/dist/diagnostic.d.ts +11 -0
  20. package/dist/diagnostic.js +70 -0
  21. package/dist/errors.d.ts +108 -24
  22. package/dist/errors.js +324 -53
  23. package/dist/exit-codes.d.ts +45 -0
  24. package/dist/exit-codes.js +46 -0
  25. package/dist/extension.d.ts +41 -8
  26. package/dist/extension.js +142 -57
  27. package/dist/facts.d.ts +66 -11
  28. package/dist/facts.js +98 -22
  29. package/dist/globals.d.ts +29 -21
  30. package/dist/globals.js +115 -46
  31. package/dist/hints.d.ts +79 -0
  32. package/dist/hints.js +247 -0
  33. package/dist/host.d.ts +13 -0
  34. package/dist/host.js +43 -1
  35. package/dist/identity.d.ts +19 -0
  36. package/dist/identity.js +72 -0
  37. package/dist/index.d.ts +11 -2
  38. package/dist/index.js +5 -0
  39. package/dist/input-rules.d.ts +64 -0
  40. package/dist/input-rules.js +145 -0
  41. package/dist/inspect.d.ts +12 -4
  42. package/dist/inspect.js +88 -28
  43. package/dist/lanes.js +1 -1
  44. package/dist/locate.js +4 -4
  45. package/dist/options.d.ts +23 -2
  46. package/dist/options.js +135 -44
  47. package/dist/output.d.ts +9 -2
  48. package/dist/output.js +18 -2
  49. package/dist/plain.d.ts +6 -0
  50. package/dist/plain.js +12 -0
  51. package/dist/plugin-rules.d.ts +62 -0
  52. package/dist/plugin-rules.js +155 -0
  53. package/dist/plugin.d.ts +22 -10
  54. package/dist/plugin.js +348 -116
  55. package/dist/prototypes.d.ts +7 -0
  56. package/dist/prototypes.js +29 -0
  57. package/dist/rendering.d.ts +6 -1
  58. package/dist/rendering.js +23 -5
  59. package/dist/rules.d.ts +51 -0
  60. package/dist/rules.js +115 -0
  61. package/dist/sequence.js +6 -1
  62. package/dist/sources.d.ts +6 -4
  63. package/dist/sources.js +22 -13
  64. package/dist/style-wire.js +1 -1
  65. package/dist/style.js +1 -1
  66. package/dist/theme.d.ts +4 -0
  67. package/dist/theme.js +25 -5
  68. package/dist/thenable.d.ts +15 -0
  69. package/dist/thenable.js +29 -0
  70. package/dist/translators.d.ts +69 -0
  71. package/dist/translators.js +253 -0
  72. package/dist/types.d.ts +11 -1
  73. package/dist/validation.d.ts +44 -3
  74. package/dist/validation.js +156 -57
  75. package/dist/view.d.ts +48 -14
  76. package/dist/view.js +155 -77
  77. package/package.json +1 -1
@@ -1,4 +1,6 @@
1
1
  import type { StandardSchemaV1 } from '@standard-schema/spec';
2
+ import type { Finding } from './diagnostic-text.js';
3
+ import type { FactSite } from './facts.js';
2
4
  import type { ArgumentNode, CommandNode, OptionNode } from './inspect.js';
3
5
  import type { AttachedCommand } from './types.js';
4
6
  /** The three declaration kinds an extension can name, each with its own node in the graph. */
@@ -15,8 +17,14 @@ interface AnyExtension {
15
17
  readonly target: ExtensionTarget;
16
18
  readonly collect: boolean;
17
19
  }
18
- /** What a value must show to be taken for a descriptor before its `collect` flag is checked. */
19
- type DescriptorShape = Pick<AnyExtension, 'identity' | 'target'>;
20
+ /**
21
+ * What a value must show to be taken for a descriptor before admission checks its identity and its
22
+ * `collect` flag. The identity stays unread here, so admission reads it once.
23
+ */
24
+ interface DescriptorShape {
25
+ readonly identity: unknown;
26
+ readonly target: ExtensionTarget;
27
+ }
20
28
  /** One carried value: the input its author supplied and the descriptor that produced it. */
21
29
  interface CarriedValue {
22
30
  descriptor: AnyExtension;
@@ -92,8 +100,15 @@ interface ExtensionSubject {
92
100
  /** The subject at the start of a sentence, such as `Command "get"`. */
93
101
  sentence: string;
94
102
  }
103
+ /**
104
+ * A descriptor admission accepted: its shape and a Boolean `collect` flag. Its identity is the key
105
+ * a registry holds it under, so nothing reads it from the descriptor again.
106
+ */
107
+ type AdmittedDescriptor = DescriptorShape & {
108
+ readonly collect: boolean;
109
+ };
95
110
  /** Every descriptor one build has met, so a second descriptor under one identity is visible. */
96
- type DescriptorRegistry = Map<string, AnyExtension>;
111
+ type DescriptorRegistry = Map<string, AdmittedDescriptor>;
97
112
  /**
98
113
  * The record each declaration published during one build, keyed by the declaration itself. The
99
114
  * records live here rather than on the declaration, because one build's outputs belong to that
@@ -102,15 +117,33 @@ type DescriptorRegistry = Map<string, AnyExtension>;
102
117
  type ExtensionRecords = Map<object, Readonly<Record<string, unknown>>>;
103
118
  /**
104
119
  * Whether a value has a descriptor's shape: a callable object carrying an identity and a declared
105
- * target. Its `collect` flag is checked where a build registers it.
120
+ * target. Admission checks its identity and its `collect` flag where a build registers it.
106
121
  */
107
122
  declare function isDescriptor(value: unknown): value is DescriptorShape;
108
- /** Admits one descriptor and records it, so a later descriptor of its identity is compared to it. */
109
- declare function registerDescriptor(descriptors: DescriptorRegistry, descriptor: DescriptorShape): void;
110
- /** Everything one `extensions` slot needs to answer: whose it is, and what it may carry. */
123
+ /**
124
+ * How one descriptor is admitted: the declaration that carries it, the plugin that holds it in a
125
+ * sentence, and its identity when a registry already read it once.
126
+ */
127
+ interface Admission {
128
+ place?: Finding | undefined;
129
+ holder?: string | undefined;
130
+ identity?: string | undefined;
131
+ }
132
+ /**
133
+ * Admits one descriptor and records it under the identity admission read, so a later descriptor of
134
+ * that identity is compared to it.
135
+ */
136
+ declare function registerDescriptor(descriptors: DescriptorRegistry, descriptor: DescriptorShape, admission?: Admission): void;
137
+ /**
138
+ * Where one `extensions` list sits: the call that declared it, and the dotted path among that
139
+ * call's arguments to the list, which is empty for `extend()`, whose arguments are the list.
140
+ */
141
+ type ExtensionSite = Pick<FactSite, 'at' | 'declaration'>;
142
+ /** Everything one `extensions` slot needs to answer: whose it is, where, and what it may carry. */
111
143
  interface ExtensionSlot {
112
144
  declared: unknown;
113
145
  descriptors: DescriptorRegistry;
146
+ site: ExtensionSite;
114
147
  subject: ExtensionSubject;
115
148
  target: ExtensionTarget;
116
149
  }
@@ -153,5 +186,5 @@ declare function buildExtensions(slot: ExtensionSlot): Readonly<Record<string, u
153
186
  declare function storeCommandLayers(slot: Omit<ExtensionSlot, 'declared' | 'target'> & {
154
187
  layers: readonly unknown[];
155
188
  }): ExtensionStore;
156
- export type { AnyExtension, DeepReadonly, DescriptorRegistry, Extension, ExtensionRecords, ExtensionStore, ExtensionSubject, ExtensionTarget, ExtensionValue, };
189
+ export type { AdmittedDescriptor, AnyExtension, DeepReadonly, DescriptorRegistry, Extension, ExtensionRecords, ExtensionSite, ExtensionStore, ExtensionSubject, ExtensionTarget, ExtensionValue, };
157
190
  export { appliesTo, buildExtensions, extendStore, extension, isDescriptor, publishStore, readExtension, registerDescriptor, storeCommandLayers, validateLayer, };
package/dist/extension.js CHANGED
@@ -1,5 +1,13 @@
1
- import { asSentence, DeclarationError, reasonOf } from './errors.js';
2
- import { isPlainObject } from './facts.js';
1
+ import { escapeControlCharacters } from './controls.js';
2
+ import { asSentence, DeclarationError, quoted, reasonOf } from './errors.js';
3
+ import { partOf } from './facts.js';
4
+ import { checkIdentity, identityFault, isIdentity } from './identity.js';
5
+ import { flagNotBoolean } from './input-rules.js';
6
+ import { isPlainObject } from './plain.js';
7
+ import { asyncExtensionSchema, extensionOutput, extensionTarget as extensionTargetRule, extensionValueTwice, extensionWithoutSchema, foreignValue, invalidExtensionValue, notAList, twoPackageCopies, } from './plugin-rules.js';
8
+ import { isThenable } from './thenable.js';
9
+ /** The fix every fault from two copies of one package shares. */
10
+ const copiesCorrection = 'Install one copy of the package that defines it.';
3
11
  /** Authored values register here, so the public brand publishes no state to reach or replace. */
4
12
  const values = new WeakMap();
5
13
  /**
@@ -20,6 +28,7 @@ class ExtensionCarrier {
20
28
  }
21
29
  }
22
30
  function extension(identity, config) {
31
+ checkIdentity('extension', identity);
23
32
  // `Object.assign` returns the same function object, so the value carries the descriptor itself.
24
33
  function create(input) {
25
34
  return new ExtensionCarrier({ descriptor, input });
@@ -44,14 +53,19 @@ const noValues = Object.freeze([]);
44
53
  * collection order, and an empty list where the node carries none.
45
54
  */
46
55
  function readExtension(node, descriptor) {
56
+ const { identity } = descriptor;
47
57
  const record = node.extensions;
48
- const owner = owners.get(record)?.get(descriptor.identity);
58
+ const owner = owners.get(record)?.get(identity);
49
59
  if (owner !== undefined && owner !== descriptor) {
50
- throw new DeclarationError(`Extension "${descriptor.identity}" was read through a descriptor that did not define the stored value. Install one copy of the package that defines it.`);
60
+ // A read happens inside a plugin's own code, so no declaration call stands for it.
61
+ throw new DeclarationError(twoPackageCopies, {
62
+ correction: copiesCorrection,
63
+ sentence: `Extension ${quoted(identity)} was read through a descriptor that did not define the stored value.`,
64
+ });
51
65
  }
52
66
  // A collecting extension a declaration carries no value of reads as an empty list.
53
67
  const absent = descriptor.collect ? noValues : undefined;
54
- const stored = owner === undefined ? absent : record[descriptor.identity];
68
+ const stored = owner === undefined ? absent : record[identity];
55
69
  // Last resort: no typed path exists.
56
70
  // The graph stores every output under a string identity, so the record reads back as `unknown`.
57
71
  // No key relates one entry to a descriptor's schema or to its runtime `collect` flag.
@@ -182,13 +196,12 @@ function plainData(value) {
182
196
  }
183
197
  /**
184
198
  * Whether a value has a descriptor's shape: a callable object carrying an identity and a declared
185
- * target. Its `collect` flag is checked where a build registers it.
199
+ * target. Admission checks its identity and its `collect` flag where a build registers it.
186
200
  */
187
201
  function isDescriptor(value) {
188
202
  return (value !== null &&
189
203
  (typeof value === 'object' || typeof value === 'function') &&
190
204
  'identity' in value &&
191
- typeof value.identity === 'string' &&
192
205
  'target' in value &&
193
206
  (value.target === 'argument' || value.target === 'command' || value.target === 'option'));
194
207
  }
@@ -200,29 +213,53 @@ function hasCollectFlag(descriptor) {
200
213
  function collects(descriptor) {
201
214
  return descriptor.collect;
202
215
  }
216
+ /** The findings for the declaration one fault marks, when a declaration stands for it. */
217
+ function marking(place, note) {
218
+ return place === undefined ? [] : [{ ...place, note }];
219
+ }
203
220
  /**
204
- * The descriptor a registry holds for one identity once this one is admitted. One identity means
205
- * one descriptor, wherever on the graph that descriptor appears, and a descriptor is checked when
206
- * a build first meets it: its `collect` flag is `true` or `false`, which the factory always
207
- * publishes and a hand-built descriptor may not. It reads the registry and changes nothing.
221
+ * The descriptor a registry holds for one identity once this one is admitted, and that identity.
222
+ * One identity means one descriptor, wherever on the graph that descriptor appears, and a
223
+ * descriptor is checked when a build first meets it: its identity follows the identity grammar and
224
+ * its `collect` flag is `true` or `false`, which the factory always ensures and a hand-built
225
+ * descriptor may not. The identity is read once, so a getter cannot answer the check with one value
226
+ * and the registry with another. It reads the registry and changes nothing.
208
227
  */
209
- function admitDescriptor(descriptors, descriptor) {
210
- const known = descriptors.get(descriptor.identity);
228
+ function admitDescriptor(descriptors, descriptor, { holder, place, ...admission }) {
229
+ const identity = admission.identity ?? descriptor.identity;
230
+ if (!isIdentity(identity)) {
231
+ const subject = holder === undefined
232
+ ? 'An extension declares the identity'
233
+ : `${holder} holds the extension identity`;
234
+ throw identityFault(subject, identity, place === undefined ? [] : [place]);
235
+ }
236
+ const known = descriptors.get(identity);
211
237
  if (known === undefined) {
212
238
  if (!hasCollectFlag(descriptor)) {
213
- throw new DeclarationError(`Extension "${descriptor.identity}" declares collect that is not a Boolean. Supply true or false, or build the descriptor with extension(identity, config).`);
239
+ throw new DeclarationError(flagNotBoolean, {
240
+ correction: 'Use true or false.',
241
+ findings: marking(place, `extension ${quoted(identity)}`),
242
+ sentence: `Extension ${quoted(identity)} declares collect that is not a Boolean.`,
243
+ });
214
244
  }
215
- return descriptor;
245
+ return { descriptor, identity };
216
246
  }
217
247
  if (known !== descriptor) {
218
- throw new DeclarationError(`Extension "${descriptor.identity}" is defined twice. Install one copy of the package that defines it.`);
248
+ throw new DeclarationError(twoPackageCopies, {
249
+ correction: copiesCorrection,
250
+ findings: marking(place, `another ${quoted(identity)}`),
251
+ sentence: `Extension ${quoted(identity)} is defined twice.`,
252
+ });
219
253
  }
220
- return known;
254
+ return { descriptor: known, identity };
221
255
  }
222
- /** Admits one descriptor and records it, so a later descriptor of its identity is compared to it. */
223
- function registerDescriptor(descriptors, descriptor) {
224
- const admitted = admitDescriptor(descriptors, descriptor);
225
- descriptors.set(admitted.identity, admitted);
256
+ /**
257
+ * Admits one descriptor and records it under the identity admission read, so a later descriptor of
258
+ * that identity is compared to it.
259
+ */
260
+ function registerDescriptor(descriptors, descriptor, admission = {}) {
261
+ const admitted = admitDescriptor(descriptors, descriptor, admission);
262
+ descriptors.set(admitted.identity, admitted.descriptor);
226
263
  }
227
264
  /** Whether a value answers the Standard Schema v1 contract this build calls synchronously. */
228
265
  function isSchema(value) {
@@ -235,101 +272,149 @@ function isSchema(value) {
235
272
  'validate' in standard &&
236
273
  typeof standard.validate === 'function');
237
274
  }
238
- /** The message one rejected value reports, with the placeholder a silent schema earns. */
275
+ /** The message one rejected value reports, escaped, with the placeholder a silent schema earns. */
239
276
  function issueText(issues) {
240
277
  const first = Array.isArray(issues) ? issues[0] : undefined;
241
278
  if (first !== null && typeof first === 'object' && 'message' in first) {
242
279
  const { message } = first;
243
280
  if (typeof message === 'string') {
244
- return message;
281
+ return escapeControlCharacters(message);
245
282
  }
246
283
  }
247
284
  // The sentence the caller composes ends the diagnostic, so this text carries no full stop.
248
285
  return 'The schema rejected this value without an explanation';
249
286
  }
250
- /**
251
- * Whether one schema answered with a promise. A thenable object and a promise from another realm
252
- * are as unwaitable here as a native one, so the test is the contract and not the class.
253
- */
254
- function isThenable(value) {
255
- return 'then' in value && typeof value.then === 'function';
287
+ /** One entry of a list a fault marks: the site's call, with the mark on the entry. */
288
+ function entryFinding(site, index, note) {
289
+ const finding = { ...site.declaration, mark: partOf(site, index) };
290
+ return note === undefined ? finding : { ...finding, note };
256
291
  }
257
292
  /** The schema one descriptor answers with, which a JavaScript author can leave out. */
258
- function schemaOf(subject, descriptor) {
293
+ function schemaOf(subject, { carried, place }) {
294
+ const { descriptor } = carried;
259
295
  const schema = 'schema' in descriptor ? descriptor.schema : undefined;
260
296
  if (!isSchema(schema)) {
261
- throw new DeclarationError(`${subject.sentence} holds extension "${descriptor.identity}", which declares no schema. Supply a Standard Schema v1 object that answers synchronously.`);
297
+ throw new DeclarationError(extensionWithoutSchema, {
298
+ correction: 'Supply a Standard Schema v1 object that answers synchronously.',
299
+ findings: [place],
300
+ sentence: `${subject.sentence} holds extension ${quoted(descriptor.identity)}, which declares no schema.`,
301
+ });
262
302
  }
263
303
  return schema;
264
304
  }
305
+ /** The fix every rejected extension value shares. */
306
+ const correctValue = 'Correct the value.';
265
307
  /** The result one schema answered with, or the rejection its own throw is. */
266
- function validated(subject, carried, schema) {
308
+ function validated(subject, entry, schema) {
309
+ const { carried, place } = entry;
267
310
  try {
268
311
  return schema['~standard'].validate(carried.input);
269
312
  }
270
313
  catch (error) {
271
314
  // A schema that throws rejected the value the only way it could, so it reads as a rejection.
272
- throw new DeclarationError(`${subject.sentence} holds an invalid "${carried.descriptor.identity}" value: ${asSentence(reasonOf(error))} Correct the value.`);
315
+ throw new DeclarationError(invalidExtensionValue, {
316
+ correction: correctValue,
317
+ findings: [place],
318
+ sentence: `${subject.sentence} holds an invalid ${quoted(carried.descriptor.identity)} value: ${asSentence(reasonOf(error))}`,
319
+ }, { cause: error });
273
320
  }
274
321
  }
275
322
  /** Validates one carried value and answers the plain-data output the node stores under it. */
276
- function validateValue(subject, carried) {
323
+ function validateValue(subject, entry) {
324
+ const { carried, place } = entry;
277
325
  const { identity } = carried.descriptor;
278
- const result = validated(subject, carried, schemaOf(subject, carried.descriptor));
326
+ const result = validated(subject, entry, schemaOf(subject, entry));
279
327
  if (result === null || typeof result !== 'object' || isThenable(result)) {
280
- throw new DeclarationError(`Extension "${identity}" validates asynchronously. Supply a schema that answers synchronously.`);
328
+ throw new DeclarationError(asyncExtensionSchema, {
329
+ correction: 'Supply a schema that answers synchronously.',
330
+ findings: [place],
331
+ sentence: `Extension ${quoted(identity)} validates asynchronously.`,
332
+ });
281
333
  }
282
334
  const issues = 'issues' in result ? result.issues : undefined;
283
335
  if (issues !== undefined) {
284
- throw new DeclarationError(`${subject.sentence} holds an invalid "${identity}" value: ${asSentence(issueText(issues))} Correct the value.`);
336
+ throw new DeclarationError(invalidExtensionValue, {
337
+ correction: correctValue,
338
+ findings: [place],
339
+ sentence: `${subject.sentence} holds an invalid ${quoted(identity)} value: ${asSentence(issueText(issues))}`,
340
+ });
285
341
  }
286
342
  const output = plainData('value' in result ? result.value : undefined);
287
343
  if (!output) {
288
- throw new DeclarationError(`Extension "${identity}" produced a value that is not plain data ${subject.phrase}. Return strings, numbers, booleans, null, arrays, and plain objects.`);
344
+ throw new DeclarationError(extensionOutput, {
345
+ correction: 'Return strings, numbers, booleans, null, arrays, and plain objects.',
346
+ findings: [place],
347
+ sentence: `Extension ${quoted(identity)} produced a value that is not plain data ${subject.phrase}.`,
348
+ });
289
349
  }
290
350
  return output.data;
291
351
  }
292
352
  /** An `extensions` slot holds a list of values, so anything else is the same declaration fault. */
293
- function readList(subject, declared) {
353
+ function readList(slot) {
354
+ const { declared, site, subject } = slot;
294
355
  if (declared === undefined) {
295
356
  return [];
296
357
  }
297
358
  if (!Array.isArray(declared)) {
298
- throw new DeclarationError(`${subject.sentence} holds a value that is not an extension value. Supply the value returned by calling an extension.`);
359
+ throw new DeclarationError(notAList, {
360
+ correction: 'Supply a list of values returned by calling an extension.',
361
+ findings: [{ ...site.declaration, mark: site.at }],
362
+ sentence: `${subject.sentence} declares extensions that are not an array.`,
363
+ });
299
364
  }
300
365
  return declared;
301
366
  }
302
367
  /** The value one entry carries, under the rules its own slot's target sets. */
303
- function carriedValue(subject, target, entry) {
368
+ function carriedValue(slot, entry, index) {
369
+ const { site, subject, target } = slot;
304
370
  const carried = typeof entry === 'object' && entry !== null ? values.get(entry) : undefined;
305
371
  if (!carried) {
306
- throw new DeclarationError(`${subject.sentence} holds a value that is not an extension value. Supply the value returned by calling an extension.`);
307
- }
308
- if (carried.descriptor.target !== target) {
309
- throw new DeclarationError(`${subject.sentence} holds extension "${carried.descriptor.identity}", which applies to ${applies[carried.descriptor.target]}. Supply an extension that applies to ${applies[target]}.`);
310
- }
311
- return carried;
372
+ throw new DeclarationError(foreignValue, {
373
+ correction: 'Supply the value returned by calling an extension.',
374
+ findings: [entryFinding(site, index)],
375
+ sentence: `${subject.sentence} holds a value that is not an extension value.`,
376
+ });
377
+ }
378
+ const { descriptor } = carried;
379
+ const place = entryFinding(site, index, `extension ${quoted(descriptor.identity)}`);
380
+ if (descriptor.target !== target) {
381
+ throw new DeclarationError(extensionTargetRule, {
382
+ correction: `Supply an extension that applies to ${applies[target]}.`,
383
+ findings: [place],
384
+ sentence: `${subject.sentence} holds extension ${quoted(descriptor.identity)}, which applies to ${applies[descriptor.target]}.`,
385
+ });
386
+ }
387
+ return { carried, place };
312
388
  }
313
389
  /**
314
390
  * One layer's values, each validated once, synchronously, in authoring order. A layer holds at
315
391
  * most one value of an extension, collecting or not, so a second one is a declaration fault.
316
392
  */
317
393
  function validateLayer(slot) {
318
- const { subject } = slot;
394
+ const { site, subject } = slot;
319
395
  const layer = [];
320
- const seen = new Set();
396
+ // Each identity's position in the list, so a repeat marks both values.
397
+ const seen = new Map();
321
398
  // The layer's descriptors register together once every value is valid.
322
399
  // A rejected layer, such as a hook's `extend()` call the hook catches, leaves the registry as it was.
323
400
  const staged = new Map(slot.descriptors);
324
- for (const entry of readList(subject, slot.declared)) {
325
- const carried = carriedValue(subject, slot.target, entry);
326
- const { descriptor } = carried;
327
- registerDescriptor(staged, descriptor);
328
- if (seen.has(descriptor.identity)) {
329
- throw new DeclarationError(`${subject.sentence} holds extension "${descriptor.identity}" twice. Supply one value.`);
401
+ for (const [index, value] of readList(slot).entries()) {
402
+ const entry = carriedValue(slot, value, index);
403
+ const { descriptor } = entry.carried;
404
+ registerDescriptor(staged, descriptor, { place: entryFinding(site, index) });
405
+ const first = seen.get(descriptor.identity);
406
+ if (first !== undefined) {
407
+ throw new DeclarationError(extensionValueTwice, {
408
+ correction: 'Supply one value.',
409
+ findings: [
410
+ entryFinding(site, first, 'the first value'),
411
+ entryFinding(site, index, 'the second value'),
412
+ ],
413
+ sentence: `${subject.sentence} holds extension ${quoted(descriptor.identity)} twice.`,
414
+ });
330
415
  }
331
- seen.add(descriptor.identity);
332
- layer.push({ descriptor, output: validateValue(subject, carried) });
416
+ seen.set(descriptor.identity, index);
417
+ layer.push({ descriptor, output: validateValue(subject, entry) });
333
418
  }
334
419
  for (const [identity, descriptor] of staged) {
335
420
  slot.descriptors.set(identity, descriptor);
package/dist/facts.d.ts CHANGED
@@ -1,3 +1,64 @@
1
+ import type { DiagnosticRule, Finding } from './diagnostic-text.js';
2
+ import { DeclarationError } from './errors.js';
3
+ /**
4
+ * Where one fact was declared: the subject its sentence names, the call that declared it, and the
5
+ * dotted path among that call's arguments to the object that holds the fact, which a finding marks.
6
+ */
7
+ export interface FactSite {
8
+ readonly subject: string;
9
+ readonly declaration: Omit<Finding, 'mark' | 'note'>;
10
+ readonly at: string;
11
+ }
12
+ /** The finding for the call one site holds, marking one part of it, with a note when given. */
13
+ export declare function siteFinding(site: FactSite, mark: string, note?: string): Finding;
14
+ /**
15
+ * Where one key of a declaration's options object sits, rebuilt with that key alone, as
16
+ * `plugin(identity, { middleware })` or `new Application(name, { plugins })`, at `1.<key>`. A fault
17
+ * about one slot shows the slot, not every other one the author declared beside it.
18
+ */
19
+ export declare function slotSite(declaration: {
20
+ call: string;
21
+ named: unknown;
22
+ subject: string;
23
+ }, key: string, value: unknown): FactSite;
24
+ /**
25
+ * The dotted path to one part inside the value a site holds, such as `1.middleware.activate` for
26
+ * `activate` under a site at `1.middleware`. A site at the call's own arguments has an empty path.
27
+ */
28
+ export declare function partOf(site: Pick<FactSite, 'at'>, ...keys: readonly (number | string)[]): string;
29
+ /** The finding that marks one part inside the value a site holds, with a note when given. */
30
+ export declare function partFinding(site: FactSite, keys: readonly (number | string)[], note?: string): Finding;
31
+ /**
32
+ * One fact's fault, which marks the fact inside the call that declared it. Every rule about one key
33
+ * of a declaration's config object reports through it, so each marks the key the same way.
34
+ */
35
+ export declare function factFault(rule: DiagnosticRule, site: FactSite, parts: {
36
+ fact: string;
37
+ sentence: string;
38
+ correction: string;
39
+ }): DeclarationError;
40
+ /**
41
+ * One yes-or-no declaration key that holds a value other than a Boolean, such as `required` or
42
+ * `hidden`. Every such key reports under one rule, in one sentence and with one correction.
43
+ */
44
+ export declare function flagFault(site: FactSite, flag: string): DeclarationError;
45
+ /**
46
+ * Where one input was declared: the site of its config object, and the dotted path to its declared
47
+ * name, which a fault about the name or about the whole input marks.
48
+ */
49
+ export interface InputSite extends FactSite {
50
+ readonly named: string;
51
+ }
52
+ /** The site of an input a call declared as `call(name, config)`, such as `option()`. */
53
+ export declare function callSite(subject: string, declaration: Omit<Finding, 'mark' | 'note'>): InputSite;
54
+ /**
55
+ * The site of one option a plugin declared, rebuilt as `plugin(identity, { options })` from the
56
+ * options the plugin holds. The option's entry in the record stands for its name.
57
+ */
58
+ export declare function pluginOptionSite(plugin: {
59
+ identity: string;
60
+ options: unknown;
61
+ }, name: string, subject: string): InputSite;
1
62
  /**
2
63
  * One line of prose: a string that holds a character other than whitespace and no line terminator.
3
64
  * Every one-line fact reads this rule, and so does the label a configuration source answers with.
@@ -8,37 +69,31 @@ export declare function isProseLine(value: unknown): value is string;
8
69
  * string fails the same way a blank one does, because the author reads one rule for one fact.
9
70
  * An omitted description is absent, not a fault, so it passes through as `undefined`.
10
71
  */
11
- export declare function checkDescription(subject: string, value: unknown): string | undefined;
72
+ export declare function checkDescription(site: FactSite, value: unknown): string | undefined;
12
73
  /**
13
74
  * The `hidden` core fact: whether a listing omits this member. It is a Boolean, because a listing
14
75
  * asks one question of it, and an omitted declaration reads `false`. Routing selects and parsing
15
76
  * binds without reading it, so a hidden member behaves as any other.
16
77
  */
17
- export declare function checkHidden(subject: string, value: unknown): boolean;
78
+ export declare function checkHidden(site: FactSite, value: unknown): boolean;
18
79
  /**
19
80
  * The `deprecated` core fact: the one-line migration message a listing shows beside the member.
20
81
  * It answers the rule a description answers, because both are one line of prose a projection
21
82
  * prints. A bare `true` is rejected with every other value that is not prose: a deprecation with
22
83
  * no migration path leaves an operator or an agent with nothing to do.
23
84
  */
24
- export declare function checkDeprecated(subject: string, value: unknown): string | undefined;
85
+ export declare function checkDeprecated(site: FactSite, value: unknown): string | undefined;
25
86
  /**
26
87
  * The declarations that carry neither listing fact: an argument, which cannot leave the grammar it
27
88
  * sits in, and the root, which is every page's entry point. The types remove both keys there, and
28
89
  * a JavaScript author, or a TypeScript author whose argument config is inferred from a value,
29
90
  * reaches this rule instead.
30
91
  */
31
- export declare function checkNoListingFacts(subject: string, declared: object): void;
92
+ export declare function checkNoListingFacts(site: FactSite, declared: object): void;
32
93
  /**
33
94
  * The `version` core fact, which the Application alone carries. Core reads it as an opaque string,
34
95
  * because the convention is the package manifest's own field and no scheme is imposed on it. An
35
96
  * omitted version is `0.0.0`, which means unversioned, and core keeps no record of which one the
36
97
  * author wrote.
37
98
  */
38
- export declare function checkVersion(value: unknown): string;
39
- /**
40
- * A structural value core reads as plain data: an object literal, and never a declaration that
41
- * carries state of its own. The options slots read it to reject a value that is not an options
42
- * object, and inspection reads it to copy a declared value faithfully.
43
- */
44
- export declare function isPlainObject(value: unknown): value is Record<string, unknown>;
99
+ export declare function checkVersion(site: FactSite, value: unknown): string;