@loomcli/core 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/LICENSE +21 -0
  2. package/dist/application.d.ts +60 -31
  3. package/dist/application.js +356 -149
  4. package/dist/bindings.d.ts +31 -0
  5. package/dist/bindings.js +64 -0
  6. package/dist/chain.d.ts +26 -12
  7. package/dist/chain.js +59 -92
  8. package/dist/command-rules.d.ts +55 -0
  9. package/dist/command-rules.js +142 -0
  10. package/dist/command.d.ts +212 -93
  11. package/dist/command.js +1224 -454
  12. package/dist/controls.d.ts +8 -0
  13. package/dist/controls.js +23 -0
  14. package/dist/defect.d.ts +18 -0
  15. package/dist/defect.js +272 -0
  16. package/dist/developer.d.ts +24 -0
  17. package/dist/developer.js +52 -0
  18. package/dist/diagnostic-text.d.ts +81 -0
  19. package/dist/diagnostic-text.js +283 -0
  20. package/dist/diagnostic.d.ts +11 -0
  21. package/dist/diagnostic.js +70 -0
  22. package/dist/errors.d.ts +115 -26
  23. package/dist/errors.js +333 -54
  24. package/dist/exit-codes.d.ts +45 -0
  25. package/dist/exit-codes.js +46 -0
  26. package/dist/extension.d.ts +44 -9
  27. package/dist/extension.js +147 -65
  28. package/dist/facts.d.ts +71 -11
  29. package/dist/facts.js +108 -25
  30. package/dist/globals.d.ts +63 -22
  31. package/dist/globals.js +164 -39
  32. package/dist/hints.d.ts +79 -0
  33. package/dist/hints.js +247 -0
  34. package/dist/host.d.ts +13 -0
  35. package/dist/host.js +43 -1
  36. package/dist/identity.d.ts +19 -0
  37. package/dist/identity.js +72 -0
  38. package/dist/index.d.ts +15 -4
  39. package/dist/index.js +6 -0
  40. package/dist/input-rules.d.ts +64 -0
  41. package/dist/input-rules.js +145 -0
  42. package/dist/inspect.d.ts +36 -5
  43. package/dist/inspect.js +125 -27
  44. package/dist/lanes.js +1 -1
  45. package/dist/locate.d.ts +41 -0
  46. package/dist/locate.js +121 -0
  47. package/dist/options.d.ts +96 -2
  48. package/dist/options.js +259 -71
  49. package/dist/output.d.ts +11 -2
  50. package/dist/output.js +23 -3
  51. package/dist/plain.d.ts +6 -0
  52. package/dist/plain.js +12 -0
  53. package/dist/plugin-rules.d.ts +62 -0
  54. package/dist/plugin-rules.js +155 -0
  55. package/dist/plugin.d.ts +121 -55
  56. package/dist/plugin.js +496 -126
  57. package/dist/prototypes.d.ts +7 -0
  58. package/dist/prototypes.js +29 -0
  59. package/dist/rendering.d.ts +6 -1
  60. package/dist/rendering.js +23 -5
  61. package/dist/rules.d.ts +51 -0
  62. package/dist/rules.js +115 -0
  63. package/dist/sequence.js +6 -1
  64. package/dist/sources.d.ts +58 -0
  65. package/dist/sources.js +258 -0
  66. package/dist/style-wire.js +1 -1
  67. package/dist/style.js +1 -1
  68. package/dist/theme.d.ts +4 -0
  69. package/dist/theme.js +25 -5
  70. package/dist/thenable.d.ts +15 -0
  71. package/dist/thenable.js +29 -0
  72. package/dist/translators.d.ts +69 -0
  73. package/dist/translators.js +253 -0
  74. package/dist/types.d.ts +76 -21
  75. package/dist/validation.d.ts +65 -10
  76. package/dist/validation.js +314 -108
  77. package/dist/view.d.ts +49 -15
  78. package/dist/view.js +157 -79
  79. package/package.json +3 -2
@@ -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;
@@ -83,6 +91,8 @@ type ExtensionRead<Schema extends StandardSchemaV1, Collect extends boolean> = C
83
91
  * collection order, and an empty list where the node carries none.
84
92
  */
85
93
  declare function readExtension<Target extends ExtensionTarget, Schema extends StandardSchemaV1, Collect extends boolean = false>(node: NodeFor<Target>, descriptor: Extension<Target, Schema, Collect>): ExtensionRead<Schema, Collect>;
94
+ /** How a diagnostic names the declarations one target covers, such as "Commands". */
95
+ declare function appliesTo(target: ExtensionTarget): string;
86
96
  /** The declaration one `extensions` slot belongs to, as its own diagnostics name it. */
87
97
  interface ExtensionSubject {
88
98
  /** The subject after a preposition, such as `on Command "get"`. */
@@ -90,8 +100,15 @@ interface ExtensionSubject {
90
100
  /** The subject at the start of a sentence, such as `Command "get"`. */
91
101
  sentence: string;
92
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
+ };
93
110
  /** Every descriptor one build has met, so a second descriptor under one identity is visible. */
94
- type DescriptorRegistry = Map<string, AnyExtension>;
111
+ type DescriptorRegistry = Map<string, AdmittedDescriptor>;
95
112
  /**
96
113
  * The record each declaration published during one build, keyed by the declaration itself. The
97
114
  * records live here rather than on the declaration, because one build's outputs belong to that
@@ -100,15 +117,33 @@ type DescriptorRegistry = Map<string, AnyExtension>;
100
117
  type ExtensionRecords = Map<object, Readonly<Record<string, unknown>>>;
101
118
  /**
102
119
  * Whether a value has a descriptor's shape: a callable object carrying an identity and a declared
103
- * 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.
104
121
  */
105
122
  declare function isDescriptor(value: unknown): value is DescriptorShape;
106
- /** Admits one descriptor and records it, so a later descriptor of its identity is compared to it. */
107
- declare function registerDescriptor(descriptors: DescriptorRegistry, descriptor: DescriptorShape): void;
108
- /** 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. */
109
143
  interface ExtensionSlot {
110
144
  declared: unknown;
111
145
  descriptors: DescriptorRegistry;
146
+ site: ExtensionSite;
112
147
  subject: ExtensionSubject;
113
148
  target: ExtensionTarget;
114
149
  }
@@ -151,5 +186,5 @@ declare function buildExtensions(slot: ExtensionSlot): Readonly<Record<string, u
151
186
  declare function storeCommandLayers(slot: Omit<ExtensionSlot, 'declared' | 'target'> & {
152
187
  layers: readonly unknown[];
153
188
  }): ExtensionStore;
154
- export type { AnyExtension, DeepReadonly, DescriptorRegistry, Extension, ExtensionRecords, ExtensionStore, ExtensionSubject, ExtensionTarget, ExtensionValue, };
155
- export { buildExtensions, extendStore, extension, isDescriptor, publishStore, readExtension, registerDescriptor, storeCommandLayers, validateLayer, };
189
+ export type { AdmittedDescriptor, AnyExtension, DeepReadonly, DescriptorRegistry, Extension, ExtensionRecords, ExtensionSite, ExtensionStore, ExtensionSubject, ExtensionTarget, ExtensionValue, };
190
+ export { appliesTo, buildExtensions, extendStore, extension, isDescriptor, publishStore, readExtension, registerDescriptor, storeCommandLayers, validateLayer, };
package/dist/extension.js CHANGED
@@ -1,5 +1,13 @@
1
- import { 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.
@@ -67,6 +81,10 @@ const applies = {
67
81
  command: 'Commands',
68
82
  option: 'options',
69
83
  };
84
+ /** How a diagnostic names the declarations one target covers, such as "Commands". */
85
+ function appliesTo(target) {
86
+ return applies[target];
87
+ }
70
88
  /**
71
89
  * The children one container contributes, or nothing when its own shape is not plain data. An
72
90
  * accessor, a symbol key, and a non-enumerable own property are not plain data, and an `undefined`
@@ -178,13 +196,12 @@ function plainData(value) {
178
196
  }
179
197
  /**
180
198
  * Whether a value has a descriptor's shape: a callable object carrying an identity and a declared
181
- * 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.
182
200
  */
183
201
  function isDescriptor(value) {
184
202
  return (value !== null &&
185
203
  (typeof value === 'object' || typeof value === 'function') &&
186
204
  'identity' in value &&
187
- typeof value.identity === 'string' &&
188
205
  'target' in value &&
189
206
  (value.target === 'argument' || value.target === 'command' || value.target === 'option'));
190
207
  }
@@ -196,29 +213,53 @@ function hasCollectFlag(descriptor) {
196
213
  function collects(descriptor) {
197
214
  return descriptor.collect;
198
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
+ }
199
220
  /**
200
- * The descriptor a registry holds for one identity once this one is admitted. One identity means
201
- * one descriptor, wherever on the graph that descriptor appears, and a descriptor is checked when
202
- * a build first meets it: its `collect` flag is `true` or `false`, which the factory always
203
- * 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.
204
227
  */
205
- function admitDescriptor(descriptors, descriptor) {
206
- 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);
207
237
  if (known === undefined) {
208
238
  if (!hasCollectFlag(descriptor)) {
209
- 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
+ });
210
244
  }
211
- return descriptor;
245
+ return { descriptor, identity };
212
246
  }
213
247
  if (known !== descriptor) {
214
- 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
+ });
215
253
  }
216
- return known;
254
+ return { descriptor: known, identity };
217
255
  }
218
- /** Admits one descriptor and records it, so a later descriptor of its identity is compared to it. */
219
- function registerDescriptor(descriptors, descriptor) {
220
- const admitted = admitDescriptor(descriptors, descriptor);
221
- 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);
222
263
  }
223
264
  /** Whether a value answers the Standard Schema v1 contract this build calls synchronously. */
224
265
  function isSchema(value) {
@@ -231,108 +272,149 @@ function isSchema(value) {
231
272
  'validate' in standard &&
232
273
  typeof standard.validate === 'function');
233
274
  }
234
- /**
235
- * One schema message as a sentence of its own. A schema author writes the message with or without a
236
- * full stop, so the diagnostic supplies one only where the message carries none.
237
- */
238
- function sentence(text) {
239
- return text.endsWith('.') ? text : `${text}.`;
240
- }
241
- /** 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. */
242
276
  function issueText(issues) {
243
277
  const first = Array.isArray(issues) ? issues[0] : undefined;
244
278
  if (first !== null && typeof first === 'object' && 'message' in first) {
245
279
  const { message } = first;
246
280
  if (typeof message === 'string') {
247
- return message;
281
+ return escapeControlCharacters(message);
248
282
  }
249
283
  }
250
284
  // The sentence the caller composes ends the diagnostic, so this text carries no full stop.
251
285
  return 'The schema rejected this value without an explanation';
252
286
  }
253
- /**
254
- * Whether one schema answered with a promise. A thenable object and a promise from another realm
255
- * are as unwaitable here as a native one, so the test is the contract and not the class.
256
- */
257
- function isThenable(value) {
258
- 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 };
259
291
  }
260
292
  /** The schema one descriptor answers with, which a JavaScript author can leave out. */
261
- function schemaOf(subject, descriptor) {
293
+ function schemaOf(subject, { carried, place }) {
294
+ const { descriptor } = carried;
262
295
  const schema = 'schema' in descriptor ? descriptor.schema : undefined;
263
296
  if (!isSchema(schema)) {
264
- 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
+ });
265
302
  }
266
303
  return schema;
267
304
  }
305
+ /** The fix every rejected extension value shares. */
306
+ const correctValue = 'Correct the value.';
268
307
  /** The result one schema answered with, or the rejection its own throw is. */
269
- function validated(subject, carried, schema) {
308
+ function validated(subject, entry, schema) {
309
+ const { carried, place } = entry;
270
310
  try {
271
311
  return schema['~standard'].validate(carried.input);
272
312
  }
273
313
  catch (error) {
274
314
  // A schema that throws rejected the value the only way it could, so it reads as a rejection.
275
- throw new DeclarationError(`${subject.sentence} holds an invalid "${carried.descriptor.identity}" value: ${sentence(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 });
276
320
  }
277
321
  }
278
322
  /** Validates one carried value and answers the plain-data output the node stores under it. */
279
- function validateValue(subject, carried) {
323
+ function validateValue(subject, entry) {
324
+ const { carried, place } = entry;
280
325
  const { identity } = carried.descriptor;
281
- const result = validated(subject, carried, schemaOf(subject, carried.descriptor));
326
+ const result = validated(subject, entry, schemaOf(subject, entry));
282
327
  if (result === null || typeof result !== 'object' || isThenable(result)) {
283
- 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
+ });
284
333
  }
285
334
  const issues = 'issues' in result ? result.issues : undefined;
286
335
  if (issues !== undefined) {
287
- throw new DeclarationError(`${subject.sentence} holds an invalid "${identity}" value: ${sentence(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
+ });
288
341
  }
289
342
  const output = plainData('value' in result ? result.value : undefined);
290
343
  if (!output) {
291
- 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
+ });
292
349
  }
293
350
  return output.data;
294
351
  }
295
352
  /** An `extensions` slot holds a list of values, so anything else is the same declaration fault. */
296
- function readList(subject, declared) {
353
+ function readList(slot) {
354
+ const { declared, site, subject } = slot;
297
355
  if (declared === undefined) {
298
356
  return [];
299
357
  }
300
358
  if (!Array.isArray(declared)) {
301
- 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
+ });
302
364
  }
303
365
  return declared;
304
366
  }
305
367
  /** The value one entry carries, under the rules its own slot's target sets. */
306
- function carriedValue(subject, target, entry) {
368
+ function carriedValue(slot, entry, index) {
369
+ const { site, subject, target } = slot;
307
370
  const carried = typeof entry === 'object' && entry !== null ? values.get(entry) : undefined;
308
371
  if (!carried) {
309
- throw new DeclarationError(`${subject.sentence} holds a value that is not an extension value. Supply the value returned by calling an extension.`);
310
- }
311
- if (carried.descriptor.target !== target) {
312
- 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]}.`);
313
- }
314
- 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 };
315
388
  }
316
389
  /**
317
390
  * One layer's values, each validated once, synchronously, in authoring order. A layer holds at
318
391
  * most one value of an extension, collecting or not, so a second one is a declaration fault.
319
392
  */
320
393
  function validateLayer(slot) {
321
- const { subject } = slot;
394
+ const { site, subject } = slot;
322
395
  const layer = [];
323
- const seen = new Set();
396
+ // Each identity's position in the list, so a repeat marks both values.
397
+ const seen = new Map();
324
398
  // The layer's descriptors register together once every value is valid.
325
399
  // A rejected layer, such as a hook's `extend()` call the hook catches, leaves the registry as it was.
326
400
  const staged = new Map(slot.descriptors);
327
- for (const entry of readList(subject, slot.declared)) {
328
- const carried = carriedValue(subject, slot.target, entry);
329
- const { descriptor } = carried;
330
- registerDescriptor(staged, descriptor);
331
- if (seen.has(descriptor.identity)) {
332
- 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
+ });
333
415
  }
334
- seen.add(descriptor.identity);
335
- layer.push({ descriptor, output: validateValue(subject, carried) });
416
+ seen.set(descriptor.identity, index);
417
+ layer.push({ descriptor, output: validateValue(subject, entry) });
336
418
  }
337
419
  for (const [identity, descriptor] of staged) {
338
420
  slot.descriptors.set(identity, descriptor);
@@ -395,4 +477,4 @@ function storeCommandLayers(slot) {
395
477
  }
396
478
  return store;
397
479
  }
398
- export { buildExtensions, extendStore, extension, isDescriptor, publishStore, readExtension, registerDescriptor, storeCommandLayers, validateLayer, };
480
+ export { appliesTo, buildExtensions, extendStore, extension, isDescriptor, publishStore, readExtension, registerDescriptor, storeCommandLayers, validateLayer, };
package/dist/facts.d.ts CHANGED
@@ -1,39 +1,99 @@
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;
62
+ /**
63
+ * One line of prose: a string that holds a character other than whitespace and no line terminator.
64
+ * Every one-line fact reads this rule, and so does the label a configuration source answers with.
65
+ */
66
+ export declare function isProseLine(value: unknown): value is string;
1
67
  /**
2
68
  * The `description` core fact: one line of prose every projection reads. A value that is not a
3
69
  * string fails the same way a blank one does, because the author reads one rule for one fact.
4
70
  * An omitted description is absent, not a fault, so it passes through as `undefined`.
5
71
  */
6
- export declare function checkDescription(subject: string, value: unknown): string | undefined;
72
+ export declare function checkDescription(site: FactSite, value: unknown): string | undefined;
7
73
  /**
8
74
  * The `hidden` core fact: whether a listing omits this member. It is a Boolean, because a listing
9
75
  * asks one question of it, and an omitted declaration reads `false`. Routing selects and parsing
10
76
  * binds without reading it, so a hidden member behaves as any other.
11
77
  */
12
- export declare function checkHidden(subject: string, value: unknown): boolean;
78
+ export declare function checkHidden(site: FactSite, value: unknown): boolean;
13
79
  /**
14
80
  * The `deprecated` core fact: the one-line migration message a listing shows beside the member.
15
81
  * It answers the rule a description answers, because both are one line of prose a projection
16
82
  * prints. A bare `true` is rejected with every other value that is not prose: a deprecation with
17
83
  * no migration path leaves an operator or an agent with nothing to do.
18
84
  */
19
- export declare function checkDeprecated(subject: string, value: unknown): string | undefined;
85
+ export declare function checkDeprecated(site: FactSite, value: unknown): string | undefined;
20
86
  /**
21
87
  * The declarations that carry neither listing fact: an argument, which cannot leave the grammar it
22
88
  * sits in, and the root, which is every page's entry point. The types remove both keys there, and
23
89
  * a JavaScript author, or a TypeScript author whose argument config is inferred from a value,
24
90
  * reaches this rule instead.
25
91
  */
26
- export declare function checkNoListingFacts(subject: string, declared: object): void;
92
+ export declare function checkNoListingFacts(site: FactSite, declared: object): void;
27
93
  /**
28
94
  * The `version` core fact, which the Application alone carries. Core reads it as an opaque string,
29
95
  * because the convention is the package manifest's own field and no scheme is imposed on it. An
30
96
  * omitted version is `0.0.0`, which means unversioned, and core keeps no record of which one the
31
97
  * author wrote.
32
98
  */
33
- export declare function checkVersion(value: unknown): string;
34
- /**
35
- * A structural value core reads as plain data: an object literal, and never a declaration that
36
- * carries state of its own. The options slots read it to reject a value that is not an options
37
- * object, and inspection reads it to copy a declared value faithfully.
38
- */
39
- export declare function isPlainObject(value: unknown): value is Record<string, unknown>;
99
+ export declare function checkVersion(site: FactSite, value: unknown): string;