@loomcli/core 0.2.0 → 0.4.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 (51) hide show
  1. package/dist/application.d.ts +59 -34
  2. package/dist/application.js +162 -59
  3. package/dist/chain.d.ts +25 -6
  4. package/dist/chain.js +99 -15
  5. package/dist/command.d.ts +146 -42
  6. package/dist/command.js +642 -78
  7. package/dist/environment.d.ts +22 -0
  8. package/dist/environment.js +1 -0
  9. package/dist/errors.d.ts +31 -59
  10. package/dist/errors.js +49 -106
  11. package/dist/extension.d.ts +80 -20
  12. package/dist/extension.js +115 -30
  13. package/dist/globals.d.ts +14 -25
  14. package/dist/globals.js +10 -61
  15. package/dist/glyphs.generated.d.ts +464 -0
  16. package/dist/glyphs.generated.js +491 -0
  17. package/dist/host.js +2 -1
  18. package/dist/index.d.ts +15 -6
  19. package/dist/index.js +5 -2
  20. package/dist/inspect.d.ts +38 -4
  21. package/dist/inspect.js +69 -6
  22. package/dist/lanes.d.ts +26 -0
  23. package/dist/lanes.js +45 -0
  24. package/dist/output.d.ts +93 -15
  25. package/dist/output.js +307 -34
  26. package/dist/plugin.d.ts +32 -16
  27. package/dist/plugin.js +46 -18
  28. package/dist/rendering.d.ts +21 -0
  29. package/dist/rendering.js +72 -0
  30. package/dist/sequence.d.ts +41 -0
  31. package/dist/sequence.js +225 -0
  32. package/dist/style-ansi.d.ts +13 -0
  33. package/dist/style-ansi.js +306 -0
  34. package/dist/style-layout.d.ts +29 -0
  35. package/dist/style-layout.js +228 -0
  36. package/dist/style-resolve.d.ts +6 -0
  37. package/dist/style-resolve.js +26 -0
  38. package/dist/style-state.d.ts +14 -0
  39. package/dist/style-state.js +179 -0
  40. package/dist/style-wire.d.ts +31 -0
  41. package/dist/style-wire.js +201 -0
  42. package/dist/style.d.ts +86 -0
  43. package/dist/style.js +201 -0
  44. package/dist/theme.d.ts +3 -0
  45. package/dist/theme.js +22 -0
  46. package/dist/types.d.ts +174 -21
  47. package/dist/validation.d.ts +8 -1
  48. package/dist/validation.js +17 -2
  49. package/dist/view.d.ts +180 -0
  50. package/dist/view.js +307 -0
  51. package/package.json +2 -1
@@ -0,0 +1,22 @@
1
+ import type { Plugin } from './plugin.js';
2
+ /** The shallow type information an Application supplies before its Command graph is composed. */
3
+ interface ApplicationEnvironment<Globals = {}, Plugins extends readonly Plugin[] = readonly []> {
4
+ readonly globals: Globals;
5
+ readonly plugins: Plugins;
6
+ }
7
+ interface RegistrationShape {
8
+ environment?: ApplicationEnvironment<unknown, readonly Plugin[]>;
9
+ }
10
+ /** The Application augments this interface once in its own TypeScript compilation context. */
11
+ interface Register extends RegistrationShape {
12
+ }
13
+ type RegisteredEnvironment = Register extends {
14
+ environment: infer Environment;
15
+ } ? Environment extends ApplicationEnvironment<unknown, readonly Plugin[]> ? Environment : never : ApplicationEnvironment;
16
+ type RegisteredGlobals = RegisteredEnvironment['globals'];
17
+ /** A private type marker avoids pretending parsed global values exist before an invocation. */
18
+ declare const applicationEnvironment: unique symbol;
19
+ type EnvironmentOf<Value extends {
20
+ readonly [applicationEnvironment]: ApplicationEnvironment<unknown, readonly Plugin[]>;
21
+ }> = Value[typeof applicationEnvironment];
22
+ export type { ApplicationEnvironment, EnvironmentOf, Register, RegisteredEnvironment, RegisteredGlobals, applicationEnvironment, };
@@ -0,0 +1 @@
1
+ export {};
package/dist/errors.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { StandardSchemaV1 } from '@standard-schema/spec';
2
- import type { InputIdentity, Renderer } from './types.js';
2
+ import type { InputIdentity } from './types.js';
3
3
  /**
4
4
  * The two short-group faults. A value option that is not last in its group names that option's
5
5
  * spelling; a group that mixes scopes names the whole group and the two letters that disagree.
@@ -14,28 +14,25 @@ type ShortGroupFault = {
14
14
  global: string;
15
15
  other: string;
16
16
  };
17
- /** One registration: the class it names, read as the prototype and the name that class holds. */
18
- interface Registration {
19
- name: string;
20
- prototype: unknown;
21
- render: (failure: LoomError) => unknown;
22
- }
23
- /** Phantom key. It marks a failure registration and holds no runtime value. */
24
- declare const failureRegistration: unique symbol;
25
- /** The runtime value `renderFailure` returns. Its pair lives in the registry above. */
26
- declare class RegisteredFailure {
27
- readonly [failureRegistration]: true;
28
- constructor(registration: Registration);
29
- }
17
+ /**
18
+ * Core's own text for one failure: its message under the category prefix its class carries, with
19
+ * the trailing newline every view's text carries. The four categories are disjoint branches of the
20
+ * hierarchy, so one ordered test reads every class, and a class without a prefix of its own writes
21
+ * the sentence alone. It is the default view of every failure class and the text the plain
22
+ * fallback path writes, so it runs no application code and nothing downstream composes its newline.
23
+ */
24
+ export declare function defaultText(failure: LoomError): string;
30
25
  /** How a diagnostic names one Command inside a sentence: by name, or as the unnamed root. */
31
26
  export declare function commandSubject(name: string | null): string;
27
+ /** The routed path names the Command a sentence speaks of; an empty path is the root. */
28
+ export declare function routedSubject(command: readonly string[]): string;
32
29
  /** The same subject at the start of a sentence. */
33
30
  export declare function commandSentence(name: string | null): string;
34
31
  /**
35
32
  * Every failure `run()` reports is an instance of a public class. Each class carries the facts its
36
- * sentence interpolates, so a renderer reads them instead of parsing prose, and the exit status is
33
+ * sentence interpolates, so a view reads them instead of parsing prose, and the exit status is
37
34
  * a field of the base, so a subclass inherits it. `message` never carries a category prefix; the
38
- * default renderers add it.
35
+ * default views add it.
39
36
  */
40
37
  export declare abstract class LoomError extends Error {
41
38
  readonly exitCode: 1 | 2;
@@ -109,58 +106,33 @@ export declare class DeclarationError extends LoomError {
109
106
  export declare class FatalError extends LoomError {
110
107
  constructor(message: string);
111
108
  }
112
- /** Exit 1: an unexpected exception, a non-error throw, or a renderer that could not answer. */
109
+ /** Exit 1: an unexpected exception, a non-error throw, or a view that could not answer. */
113
110
  export declare class InternalError extends LoomError {
114
111
  readonly cause: unknown;
115
112
  constructor(message: string, cause: unknown);
116
113
  }
114
+ /** The four ways the results lane is broken, each named where core meets it. */
115
+ export type ResultFault = 'missing' | 'repeated' | 'undeclared' | 'middleware';
116
+ /**
117
+ * Exit 1: the promise a declared result makes was not kept. It wraps no thrown value, so its
118
+ * `cause` is `undefined`, and it extends `InternalError`, so an override of that class brands it
119
+ * and its default text carries the same prefix, while an override keyed by this class reaches it
120
+ * alone.
121
+ */
122
+ export declare class ResultError extends InternalError {
123
+ readonly path: readonly string[];
124
+ readonly kind: ResultFault;
125
+ readonly cause: undefined;
126
+ constructor(kind: ResultFault, path: readonly string[]);
127
+ }
117
128
  /** What a diagnostic says about an unexpected value, whether or not it was an Error. */
118
129
  export declare function reasonOf(thrown: unknown): string;
119
130
  /**
120
- * Why a returned value is not the text a renderer owes. A renderer is synchronous, so a returned
121
- * promise is a non-string return like any other, and its rejection is adopted and swallowed here:
122
- * an unobserved rejection would end the process before the invocation could report anything.
131
+ * Why a returned value is not the text a view owes. A view is synchronous, so a returned promise is
132
+ * a non-string return like any other, and its rejection is adopted and swallowed here: an
133
+ * unobserved rejection would end the process before the invocation could report anything.
123
134
  */
124
135
  export declare function notTextReason(value: unknown): string;
125
136
  /** Every thrown value reaches reporting as a failure class; anything else is internal. */
126
137
  export declare function toFailure(thrown: unknown): LoomError;
127
- /** An opaque registration pairing one failure class with a renderer for its instances. */
128
- export type FailureRenderer = Pick<RegisteredFailure, typeof failureRegistration>;
129
- /**
130
- * A registration pairing one failure class with a renderer for its instances. The helper is the
131
- * typed path for a class-keyed list, because an array literal cannot carry a different type
132
- * parameter per element.
133
- */
134
- export declare function renderFailure<Failure extends LoomError>(type: abstract new (...args: never[]) => Failure, renderer: Renderer<Failure>): FailureRenderer;
135
- /** The renderers one application registered, keyed by the class each one names. */
136
- export type FailureRegistry = ReadonlyMap<unknown, Registration>;
137
- /**
138
- * One class answers to one renderer inside one contributor, so a second registration for it is a
139
- * declaration fault. The subject names the contributor: the Application, or an installed plugin.
140
- */
141
- export declare function buildFailures(failures: readonly FailureRenderer[], subject?: string): FailureRegistry;
142
- /**
143
- * One registry from every contributor's own, resolving first-in-wins: the application's
144
- * registrations, then each installed plugin's in installation order, then core's text.
145
- */
146
- export declare function mergeFailures(registries: readonly FailureRegistry[]): FailureRegistry;
147
- /**
148
- * The report of one failure: the text core writes, and whether a registered renderer produced it.
149
- * An unrendered report carries core's own text, which the plain fallback path writes beside the
150
- * diagnostic naming the renderer that could not answer.
151
- */
152
- export type FailureReport = {
153
- kind: 'rendered';
154
- text: string;
155
- } | {
156
- kind: 'unrendered';
157
- text: string;
158
- reason: string;
159
- };
160
- /**
161
- * The text core writes for one failure. Resolution walks the failure's prototype chain most
162
- * derived first through the application's registrations, then falls to core's own text, so a
163
- * registration for a base class brands every failure below it.
164
- */
165
- export declare function describeFailure(registry: FailureRegistry, failure: LoomError): FailureReport;
166
138
  export {};
package/dist/errors.js CHANGED
@@ -1,7 +1,20 @@
1
- /** The routed path names the Command a token fault belongs to; an empty path is the root. */
1
+ /** The same subject at the start of a sentence, where a token fault names its Command. */
2
2
  function routedSentence(command) {
3
- const name = command.at(-1);
4
- return name === undefined ? 'The root Command' : commandSentence(name);
3
+ const subject = routedSubject(command);
4
+ return `${subject.slice(0, 1).toUpperCase()}${subject.slice(1)}`;
5
+ }
6
+ /** The sentence one results-lane fault reports, which names the Command that holds it. */
7
+ function resultMessage(kind, command) {
8
+ if (kind === 'missing') {
9
+ return `${routedSentence(command)} declares a result and its action returned without emitting one. Call out.results() once.`;
10
+ }
11
+ if (kind === 'repeated') {
12
+ return `${routedSentence(command)} emitted its result twice. Call out.results() once.`;
13
+ }
14
+ if (kind === 'undeclared') {
15
+ return `${routedSentence(command)} declares no result. Declare one with result() or rows() before action().`;
16
+ }
17
+ return `A middleware called out.results() on ${routedSubject(command)}. Only the action emits a result.`;
5
18
  }
6
19
  /**
7
20
  * The clause that offers the candidates, which is absent when there are none: every child of the
@@ -17,12 +30,12 @@ function shortGroupMessage(fault) {
17
30
  }
18
31
  /**
19
32
  * Core's own text for one failure: its message under the category prefix its class carries, with
20
- * the trailing newline every renderer's text carries. The four categories are disjoint branches of
21
- * the hierarchy, so one ordered test reads every class, and a class without a prefix of its own
22
- * writes the sentence alone. A default rendering is the same kind of value as a custom one, so
23
- * nothing downstream composes the newline for it.
33
+ * the trailing newline every view's text carries. The four categories are disjoint branches of the
34
+ * hierarchy, so one ordered test reads every class, and a class without a prefix of its own writes
35
+ * the sentence alone. It is the default view of every failure class and the text the plain
36
+ * fallback path writes, so it runs no application code and nothing downstream composes its newline.
24
37
  */
25
- function defaultText(failure) {
38
+ export function defaultText(failure) {
26
39
  if (failure instanceof UsageError) {
27
40
  return `Invalid input: ${failure.message}\n`;
28
41
  }
@@ -34,36 +47,15 @@ function defaultText(failure) {
34
47
  }
35
48
  return `${failure.message}\n`;
36
49
  }
37
- /** Every prototype in a failure's chain, most derived first, so one walk reads the registry. */
38
- function chainOf(failure) {
39
- const chain = [];
40
- let prototype = Object.getPrototypeOf(failure);
41
- while (prototype !== null) {
42
- chain.push(prototype);
43
- prototype = Object.getPrototypeOf(prototype);
44
- }
45
- return chain;
46
- }
47
- /** Authored registrations register here, so the public type publishes nothing to reach. */
48
- const nodes = new WeakMap();
49
- /** The runtime value `renderFailure` returns. Its pair lives in the registry above. */
50
- class RegisteredFailure {
51
- constructor(registration) {
52
- nodes.set(this, registration);
53
- }
54
- }
55
- /** Reads the pair behind a registered value; anything else is a declaration error. */
56
- function nodeOf(value, subject) {
57
- const registration = nodes.get(value);
58
- if (!registration) {
59
- throw new DeclarationError(`${subject} holds a value that is not a failure renderer. Supply the value returned by renderFailure(type, renderer).`);
60
- }
61
- return registration;
62
- }
63
50
  /** How a diagnostic names one Command inside a sentence: by name, or as the unnamed root. */
64
51
  export function commandSubject(name) {
65
52
  return name === null ? 'the root Command' : `Command "${name}"`;
66
53
  }
54
+ /** The routed path names the Command a sentence speaks of; an empty path is the root. */
55
+ export function routedSubject(command) {
56
+ const name = command.at(-1);
57
+ return name === undefined ? 'the root Command' : commandSubject(name);
58
+ }
67
59
  /** The same subject at the start of a sentence. */
68
60
  export function commandSentence(name) {
69
61
  const subject = commandSubject(name);
@@ -71,9 +63,9 @@ export function commandSentence(name) {
71
63
  }
72
64
  /**
73
65
  * Every failure `run()` reports is an instance of a public class. Each class carries the facts its
74
- * sentence interpolates, so a renderer reads them instead of parsing prose, and the exit status is
66
+ * sentence interpolates, so a view reads them instead of parsing prose, and the exit status is
75
67
  * a field of the base, so a subclass inherits it. `message` never carries a category prefix; the
76
- * default renderers add it.
68
+ * default views add it.
77
69
  */
78
70
  export class LoomError extends Error {
79
71
  exitCode;
@@ -193,7 +185,7 @@ export class FatalError extends LoomError {
193
185
  this.name = 'FatalError';
194
186
  }
195
187
  }
196
- /** Exit 1: an unexpected exception, a non-error throw, or a renderer that could not answer. */
188
+ /** Exit 1: an unexpected exception, a non-error throw, or a view that could not answer. */
197
189
  export class InternalError extends LoomError {
198
190
  cause;
199
191
  constructor(message, cause) {
@@ -202,85 +194,36 @@ export class InternalError extends LoomError {
202
194
  this.name = 'InternalError';
203
195
  }
204
196
  }
197
+ /**
198
+ * Exit 1: the promise a declared result makes was not kept. It wraps no thrown value, so its
199
+ * `cause` is `undefined`, and it extends `InternalError`, so an override of that class brands it
200
+ * and its default text carries the same prefix, while an override keyed by this class reaches it
201
+ * alone.
202
+ */
203
+ export class ResultError extends InternalError {
204
+ path;
205
+ kind;
206
+ constructor(kind, path) {
207
+ super(resultMessage(kind, path), undefined);
208
+ this.kind = kind;
209
+ this.name = 'ResultError';
210
+ this.path = path;
211
+ }
212
+ }
205
213
  /** What a diagnostic says about an unexpected value, whether or not it was an Error. */
206
214
  export function reasonOf(thrown) {
207
215
  return thrown instanceof Error ? thrown.message : 'An unknown error occurred.';
208
216
  }
209
217
  /**
210
- * Why a returned value is not the text a renderer owes. A renderer is synchronous, so a returned
211
- * promise is a non-string return like any other, and its rejection is adopted and swallowed here:
212
- * an unobserved rejection would end the process before the invocation could report anything.
218
+ * Why a returned value is not the text a view owes. A view is synchronous, so a returned promise is
219
+ * a non-string return like any other, and its rejection is adopted and swallowed here: an
220
+ * unobserved rejection would end the process before the invocation could report anything.
213
221
  */
214
222
  export function notTextReason(value) {
215
223
  void Promise.resolve(value).catch(() => undefined);
216
- return `The renderer returned ${typeof value} instead of a string.`;
224
+ return `The view returned ${typeof value} instead of a string.`;
217
225
  }
218
226
  /** Every thrown value reaches reporting as a failure class; anything else is internal. */
219
227
  export function toFailure(thrown) {
220
228
  return thrown instanceof LoomError ? thrown : new InternalError(reasonOf(thrown), thrown);
221
229
  }
222
- /**
223
- * A registration pairing one failure class with a renderer for its instances. The helper is the
224
- * typed path for a class-keyed list, because an array literal cannot carry a different type
225
- * parameter per element.
226
- */
227
- export function renderFailure(type, renderer) {
228
- return new RegisteredFailure({
229
- name: 'name' in type && typeof type.name === 'string' ? type.name : 'a failure class',
230
- prototype: 'prototype' in type ? type.prototype : undefined,
231
- // Resolution reaches this registration through the same class, so the test always holds.
232
- render: (failure) => (failure instanceof type ? renderer.render(failure) : undefined),
233
- });
234
- }
235
- /**
236
- * One class answers to one renderer inside one contributor, so a second registration for it is a
237
- * declaration fault. The subject names the contributor: the Application, or an installed plugin.
238
- */
239
- export function buildFailures(failures, subject = 'The Application') {
240
- const registry = new Map();
241
- for (const failure of failures) {
242
- const registration = nodeOf(failure, subject);
243
- if (registry.has(registration.prototype)) {
244
- throw new DeclarationError(`${subject} registers two failure renderers for "${registration.name}". Remove one registration.`);
245
- }
246
- registry.set(registration.prototype, registration);
247
- }
248
- return registry;
249
- }
250
- /**
251
- * One registry from every contributor's own, resolving first-in-wins: the application's
252
- * registrations, then each installed plugin's in installation order, then core's text.
253
- */
254
- export function mergeFailures(registries) {
255
- const merged = new Map();
256
- for (const registry of registries) {
257
- for (const [type, registration] of registry) {
258
- if (!merged.has(type)) {
259
- merged.set(type, registration);
260
- }
261
- }
262
- }
263
- return merged;
264
- }
265
- /**
266
- * The text core writes for one failure. Resolution walks the failure's prototype chain most
267
- * derived first through the application's registrations, then falls to core's own text, so a
268
- * registration for a base class brands every failure below it.
269
- */
270
- export function describeFailure(registry, failure) {
271
- const registration = chainOf(failure)
272
- .map((prototype) => registry.get(prototype))
273
- .find((entry) => entry !== undefined);
274
- if (!registration) {
275
- return { kind: 'rendered', text: defaultText(failure) };
276
- }
277
- try {
278
- const text = registration.render(failure);
279
- return typeof text === 'string'
280
- ? { kind: 'rendered', text }
281
- : { kind: 'unrendered', reason: notTextReason(text), text: defaultText(failure) };
282
- }
283
- catch (error) {
284
- return { kind: 'unrendered', reason: reasonOf(error), text: defaultText(failure) };
285
- }
286
- }
@@ -1,17 +1,22 @@
1
1
  import type { StandardSchemaV1 } from '@standard-schema/spec';
2
2
  import type { ArgumentNode, CommandNode, OptionNode } from './inspect.js';
3
+ import type { AttachedCommand } from './types.js';
3
4
  /** The three declaration kinds an extension can name, each with its own node in the graph. */
4
5
  type ExtensionTarget = 'argument' | 'command' | 'option';
5
6
  /**
6
- * The descriptor supertype a plugin's `extensions` list uses. It publishes the identity and the
7
- * target and erases both the schema and the factory call signature, because a schema-typed call
8
- * signature relates only by schema identity and a list cannot name one schema per element. A
9
- * descriptor is assignable to it; an extension value, which publishes its brand alone, is not.
7
+ * The descriptor supertype a plugin's `extensions` list uses. It publishes the identity, the
8
+ * target, and whether the extension collects, and erases both the schema and the factory call
9
+ * signature, because a schema-typed call signature relates only by schema identity and a list
10
+ * cannot name one schema per element. A descriptor is assignable to it; an extension value, which
11
+ * publishes its brand alone, is not.
10
12
  */
11
13
  interface AnyExtension {
12
14
  readonly identity: string;
13
15
  readonly target: ExtensionTarget;
16
+ readonly collect: boolean;
14
17
  }
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'>;
15
20
  /** One carried value: the input its author supplied and the descriptor that produced it. */
16
21
  interface CarriedValue {
17
22
  descriptor: AnyExtension;
@@ -32,33 +37,52 @@ declare class ExtensionCarrier<Target extends ExtensionTarget> {
32
37
  type ExtensionValue<Target extends ExtensionTarget> = Pick<ExtensionCarrier<Target>, typeof extensionTarget>;
33
38
  /**
34
39
  * A descriptor that is also a factory: calling it with the schema's input returns the branded value
35
- * a declaration carries. The schema is public so a typed read recovers the output type.
40
+ * a declaration carries. The schema is public so a typed read recovers the output type, and
41
+ * `collect` says whether the values a declaration carries accumulate or replace each other.
36
42
  */
37
- interface Extension<Target extends ExtensionTarget = ExtensionTarget, Schema extends StandardSchemaV1 = StandardSchemaV1> extends AnyExtension {
43
+ interface Extension<Target extends ExtensionTarget = ExtensionTarget, Schema extends StandardSchemaV1 = StandardSchemaV1, Collect extends boolean = false> extends AnyExtension {
38
44
  (input: StandardSchemaV1.InferInput<Schema>): ExtensionValue<Target>;
39
45
  readonly schema: Schema;
40
46
  readonly target: Target;
47
+ readonly collect: Collect;
41
48
  }
42
49
  /**
43
50
  * One typed fact a plugin defines for one target. The descriptor is compared by reference wherever
44
- * it appears, so one identity means one descriptor and a duplicated package copy is visible.
51
+ * it appears, so one identity means one descriptor and a duplicated package copy is visible. With
52
+ * `collect: true` it is a collecting extension, whose values accumulate on a declaration in order.
45
53
  */
46
54
  declare function extension<Target extends ExtensionTarget, Schema extends StandardSchemaV1>(identity: string, config: {
47
55
  schema: Schema;
48
56
  target: Target;
57
+ collect?: false | undefined;
49
58
  }): Extension<Target, Schema>;
50
- /** The node kind one descriptor's target names, so a read against another kind cannot compile. */
51
- type NodeFor<Target extends ExtensionTarget> = Target extends 'command' ? CommandNode : Target extends 'option' ? OptionNode : ArgumentNode;
59
+ declare function extension<Target extends ExtensionTarget, Schema extends StandardSchemaV1>(identity: string, config: {
60
+ schema: Schema;
61
+ target: Target;
62
+ collect: true;
63
+ }): Extension<Target, Schema, true>;
64
+ /**
65
+ * The node kind one descriptor's target names, so a read against another kind cannot compile. A
66
+ * Command-target read also takes the value a lifecycle hook receives, which publishes the record
67
+ * as it stands at that hook.
68
+ */
69
+ type NodeFor<Target extends ExtensionTarget> = Target extends 'command' ? CommandNode | AttachedCommand : Target extends 'option' ? OptionNode : ArgumentNode;
52
70
  /** Stored output is plain data the graph froze, so every read of it is read-only to any depth. */
53
71
  type DeepReadonly<Value> = Value extends readonly (infer Item)[] ? readonly DeepReadonly<Item>[] : Value extends object ? {
54
72
  readonly [Key in keyof Value]: DeepReadonly<Value[Key]>;
55
73
  } : Value;
74
+ /**
75
+ * What one read answers, decided by the descriptor's own `collect` type: a collecting extension's
76
+ * outputs as a read-only list, or an ordinary extension's output or nothing.
77
+ */
78
+ type ExtensionRead<Schema extends StandardSchemaV1, Collect extends boolean> = Collect extends true ? readonly DeepReadonly<StandardSchemaV1.InferOutput<Schema>>[] : DeepReadonly<StandardSchemaV1.InferOutput<Schema>> | undefined;
56
79
  /**
57
80
  * The typed read of one extension value. It takes the node kind the descriptor targets, returns the
58
81
  * stored output or `undefined`, compares the descriptor by reference with the one that produced the
59
- * value, and runs no schema.
82
+ * value, and runs no schema. Through a collecting descriptor it returns every collected output in
83
+ * collection order, and an empty list where the node carries none.
60
84
  */
61
- declare function readExtension<Target extends ExtensionTarget, Schema extends StandardSchemaV1>(node: NodeFor<Target>, descriptor: Extension<Target, Schema>): DeepReadonly<StandardSchemaV1.InferOutput<Schema>> | undefined;
85
+ declare function readExtension<Target extends ExtensionTarget, Schema extends StandardSchemaV1, Collect extends boolean = false>(node: NodeFor<Target>, descriptor: Extension<Target, Schema, Collect>): ExtensionRead<Schema, Collect>;
62
86
  /** The declaration one `extensions` slot belongs to, as its own diagnostics name it. */
63
87
  interface ExtensionSubject {
64
88
  /** The subject after a preposition, such as `on Command "get"`. */
@@ -74,10 +98,13 @@ type DescriptorRegistry = Map<string, AnyExtension>;
74
98
  * build alone and inspection reads them back with the nodes it renders.
75
99
  */
76
100
  type ExtensionRecords = Map<object, Readonly<Record<string, unknown>>>;
77
- /** Whether a value is a descriptor: a callable object carrying an identity and a declared target. */
78
- declare function isDescriptor(value: unknown): value is AnyExtension;
79
- /** One identity means one descriptor, wherever on the graph that descriptor appears. */
80
- declare function registerDescriptor(descriptors: DescriptorRegistry, descriptor: AnyExtension): void;
101
+ /**
102
+ * 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.
104
+ */
105
+ 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;
81
108
  /** Everything one `extensions` slot needs to answer: whose it is, and what it may carry. */
82
109
  interface ExtensionSlot {
83
110
  declared: unknown;
@@ -85,11 +112,44 @@ interface ExtensionSlot {
85
112
  subject: ExtensionSubject;
86
113
  target: ExtensionTarget;
87
114
  }
115
+ /** One carried value, validated against its descriptor's schema and ready to store. */
116
+ interface ValidatedValue {
117
+ descriptor: AnyExtension;
118
+ output: unknown;
119
+ }
120
+ /**
121
+ * One layer's values, each validated once, synchronously, in authoring order. A layer holds at
122
+ * most one value of an extension, collecting or not, so a second one is a declaration fault.
123
+ */
124
+ declare function validateLayer(slot: ExtensionSlot): readonly ValidatedValue[];
125
+ /**
126
+ * The values one declaration has validated so far, by identity in the order each first appeared:
127
+ * an ordinary extension's latest output alone, and a collecting extension's every output in
128
+ * collection order. A store is never changed; adding a layer answers a new one.
129
+ */
130
+ interface ExtensionStore {
131
+ readonly entries: ReadonlyMap<string, {
132
+ readonly descriptor: AnyExtension;
133
+ readonly outputs: readonly unknown[];
134
+ }>;
135
+ }
136
+ /**
137
+ * The store with one more validated layer. An ordinary value replaces the earlier one in place, so
138
+ * a key keeps its first position, and a collecting value joins the values before it.
139
+ */
140
+ declare function extendStore(store: ExtensionStore, layer: readonly ValidatedValue[]): ExtensionStore;
88
141
  /**
89
- * The frozen record one declaration publishes: each carried value validated once, synchronously,
90
- * and stored under its extension's identity. The descriptors that produced them are recorded beside
91
- * the record, so a typed read compares by reference without the graph carrying a reference.
142
+ * The frozen record a store publishes: each ordinary extension's output, and each collecting
143
+ * extension's frozen list of outputs, under its identity. The descriptors that produced them are
144
+ * recorded beside the record, so a typed read compares by reference without the graph carrying a
145
+ * reference.
92
146
  */
147
+ declare function publishStore(store: ExtensionStore): Readonly<Record<string, unknown>>;
148
+ /** The frozen record of one `extensions` slot, which is one layer. */
93
149
  declare function buildExtensions(slot: ExtensionSlot): Readonly<Record<string, unknown>>;
94
- export type { AnyExtension, DeepReadonly, DescriptorRegistry, Extension, ExtensionRecords, ExtensionSubject, ExtensionTarget, ExtensionValue, };
95
- export { buildExtensions, extension, isDescriptor, readExtension, registerDescriptor };
150
+ /** A Command's author layers, validated in authoring order into the store its hooks extend. */
151
+ declare function storeCommandLayers(slot: Omit<ExtensionSlot, 'declared' | 'target'> & {
152
+ layers: readonly unknown[];
153
+ }): ExtensionStore;
154
+ export type { AnyExtension, DeepReadonly, DescriptorRegistry, Extension, ExtensionRecords, ExtensionStore, ExtensionSubject, ExtensionTarget, ExtensionValue, };
155
+ export { buildExtensions, extendStore, extension, isDescriptor, publishStore, readExtension, registerDescriptor, storeCommandLayers, validateLayer, };