@loomcli/core 0.1.1 → 0.2.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.
package/dist/command.js CHANGED
@@ -1,5 +1,7 @@
1
1
  import { commandSentence, commandSubject, DeclarationError, NonCallableCommandError, UnexpectedArgumentError, UnknownCommandError, } from './errors.js';
2
- import { bindGlobals, buildGlobals } from './globals.js';
2
+ import { buildExtensions } from './extension.js';
3
+ import { checkDeprecated, checkDescription, checkHidden, checkNoListingFacts, isPlainObject, } from './facts.js';
4
+ import { bindGlobals, buildGlobals, isGlobalOptions, keyCollision, spellingCollision, } from './globals.js';
3
5
  import { compileOptions, extractGlobals, mergeValues, parseInputs } from './options.js';
4
6
  import { captureConfig, validateValues } from './validation.js';
5
7
  /** Authored values register here, so the public type publishes no state to reach or replace. */
@@ -12,6 +14,22 @@ function nodeOf(parent, child) {
12
14
  }
13
15
  return node;
14
16
  }
17
+ /**
18
+ * The shape of a Command's second argument, read where it is supplied. The retired positional form
19
+ * declares its globals on a value that holds no `globals` key, so without this rule the globals
20
+ * vanish silently and the operator, not the author, meets the consequence as an unknown-option
21
+ * error. It answers before every other rule, because a Command that lost its globals this way would
22
+ * otherwise report the mismatch with its Application instead of the slot that caused it.
23
+ * The facts the slot carried were captured at construction, so this reads the slot's shape alone.
24
+ */
25
+ function checkCommandOptions(name, options) {
26
+ if (isGlobalOptions(options)) {
27
+ throw new DeclarationError(`${commandSentence(name)} takes an options object. Supply { globals } instead of a positional GlobalOptions value.`);
28
+ }
29
+ if (options !== undefined && !isPlainObject(options)) {
30
+ throw new DeclarationError(`${commandSentence(name)} options must be an object. Supply { globals }.`);
31
+ }
32
+ }
15
33
  /** One name rule for every declared name in the graph, so a child and an argument read alike. */
16
34
  function isDeclaredName(name) {
17
35
  return typeof name === 'string' && Boolean(name) && !name.startsWith('-') && !/[\s=]/u.test(name);
@@ -27,17 +45,26 @@ function checkAliasName(command, alias) {
27
45
  throw new DeclarationError(`${commandSentence(command)} declares an alias named "${String(alias)}". Use a nonempty name without a leading hyphen, whitespace, or "=".`);
28
46
  }
29
47
  }
30
- /** The state every declaration starts from. The unnamed root and each named Command share it. */
31
- export function freshState(name, globals) {
48
+ /**
49
+ * The state every declaration starts from. The unnamed root and each named Command share it.
50
+ * The declaration values arrive captured, because a later change to the options object the author
51
+ * passed changes nothing the declaration holds.
52
+ */
53
+ export function freshState(declaration) {
32
54
  return {
33
55
  actions: [],
34
56
  aliases: [],
35
57
  bind: () => ({ args: {}, options: {} }),
36
58
  children: [],
37
- globals,
59
+ deprecated: declaration.deprecated,
60
+ description: declaration.description,
61
+ extensions: declaration.extensions,
62
+ globals: declaration.globals,
63
+ hidden: declaration.hidden,
38
64
  inputs: [],
39
65
  late: [],
40
- name,
66
+ name: declaration.name,
67
+ options: declaration.options,
41
68
  };
42
69
  }
43
70
  /** A declaration after the action is an order fault; build reports the first one recorded. */
@@ -216,18 +243,26 @@ function collectArguments(state, subject) {
216
243
  }
217
244
  return slots;
218
245
  }
246
+ /**
247
+ * One Command's own options against the shared globals table. The table holds the application's
248
+ * globals and every plugin option, so a local collision reads the same sentence whichever scope on
249
+ * the other side claimed the name or the spelling.
250
+ */
219
251
  function compileLocalOptions(state, globals, subject) {
220
252
  const declarations = state.inputs.filter((input) => input.kind === 'option');
253
+ const local = { kind: 'local', subject };
254
+ const application = { kind: 'application' };
221
255
  for (const declaration of declarations) {
222
- if (globals.names.has(declaration.name)) {
223
- throw new DeclarationError(`Option "${declaration.name}" is declared as a global option and as a local option on ${subject}. Rename the local option.`);
256
+ const claimed = globals.names.get(declaration.name);
257
+ if (claimed) {
258
+ throw keyCollision(declaration.name, claimed, local);
224
259
  }
225
260
  }
226
261
  const options = compileOptions(declarations, subject);
227
262
  for (const [spelling, option] of options) {
228
263
  const global = globals.options.get(spelling);
229
264
  if (global) {
230
- throw new DeclarationError(`Option spelling "${spelling}" is used by the global option "${global.name}" and the local option "${option.name}" on ${subject}. Change one declaration.`);
265
+ throw spellingCollision(spelling, { name: global.name, owner: globals.names.get(global.name) ?? application }, { name: option.name, owner: local });
231
266
  }
232
267
  }
233
268
  return options;
@@ -249,7 +284,7 @@ function checkGroup(state, children) {
249
284
  }
250
285
  /** Binds one Command's declarations to its action, so an action reads only validated values. */
251
286
  function bindDispatch(state, action) {
252
- return ({ host, out, passthrough, values }) => {
287
+ return ({ host, out, passthrough, signal, values }) => {
253
288
  const bound = state.bind(values);
254
289
  return action({
255
290
  args: bound.args,
@@ -257,6 +292,7 @@ function bindDispatch(state, action) {
257
292
  options: { ...bindGlobals(state.globals, values), ...bound.options },
258
293
  out,
259
294
  passthrough,
295
+ signal,
260
296
  });
261
297
  };
262
298
  }
@@ -265,6 +301,34 @@ export function buildCommand(state, context) {
265
301
  const { actions, name } = state;
266
302
  const { globals } = context;
267
303
  const subject = commandSubject(name);
304
+ checkCommandOptions(name, state.options);
305
+ const description = checkDescription(commandSentence(name), state.description);
306
+ const hidden = checkHidden(commandSentence(name), state.hidden);
307
+ const deprecated = checkDeprecated(commandSentence(name), state.deprecated);
308
+ const extensions = buildExtensions({
309
+ declared: state.extensions,
310
+ descriptors: context.descriptors,
311
+ subject: { phrase: `on ${subject}`, sentence: commandSentence(name) },
312
+ target: 'command',
313
+ });
314
+ // Each declaration's own facts, in authoring order, before the rules that pair declarations.
315
+ for (const input of state.inputs) {
316
+ const sentence = `${commandSentence(name)} ${input.kind} "${input.name}"`;
317
+ checkDescription(sentence, input.config.description);
318
+ if (input.kind === 'argument') {
319
+ checkNoListingFacts(sentence, input.config);
320
+ }
321
+ else {
322
+ checkHidden(sentence, input.config.hidden);
323
+ checkDeprecated(sentence, input.config.deprecated);
324
+ }
325
+ context.extensions.set(input, buildExtensions({
326
+ declared: input.config.extensions,
327
+ descriptors: context.descriptors,
328
+ subject: { phrase: `on ${subject} ${input.kind} "${input.name}"`, sentence },
329
+ target: input.kind,
330
+ }));
331
+ }
268
332
  if (state.globals !== globals.source) {
269
333
  throw new DeclarationError(`${commandSentence(name)} holds a different GlobalOptions value than its Application. Share one GlobalOptions value across the declarations.`);
270
334
  }
@@ -302,7 +366,11 @@ export function buildCommand(state, context) {
302
366
  aliases,
303
367
  arguments: slots,
304
368
  children,
369
+ deprecated,
370
+ description,
305
371
  dispatch: action ? bindDispatch(state, action) : undefined,
372
+ extensions,
373
+ hidden,
306
374
  inputs: state.inputs,
307
375
  name,
308
376
  options,
@@ -310,9 +378,18 @@ export function buildCommand(state, context) {
310
378
  };
311
379
  }
312
380
  /** The globals table and the owners record each compile once per invocation and the whole graph shares them. */
313
- export function buildGraph(root) {
314
- const context = { globals: buildGlobals(root.globals), owners: new Map() };
315
- return { globals: context.globals, root: buildCommand(root, context) };
381
+ export function buildGraph(root, install) {
382
+ const context = {
383
+ descriptors: install.descriptors,
384
+ extensions: install.extensions,
385
+ globals: buildGlobals(root.globals, install.plugins, install),
386
+ owners: new Map(),
387
+ };
388
+ return {
389
+ extensions: context.extensions,
390
+ globals: context.globals,
391
+ root: buildCommand(root, context),
392
+ };
316
393
  }
317
394
  export class CommandBuilder {
318
395
  #state;
@@ -340,8 +417,8 @@ export class CommandBuilder {
340
417
  return new CommandBuilder(declareOption(this.#state, input));
341
418
  }
342
419
  /**
343
- * Hidden aliases are other bare tokens that route to this Command. They invalidate no call, and
344
- * the tuple rest parameter rejects a call that names none.
420
+ * Aliases are other bare tokens that route to this Command. They invalidate no call, and the
421
+ * tuple rest parameter rejects a call that names none.
345
422
  */
346
423
  alias(...names) {
347
424
  return new CommandBuilder(declareAlias(this.#state, names));
@@ -364,11 +441,21 @@ export class CommandBuilder {
364
441
  * signature of the constructor interface publishes.
365
442
  */
366
443
  class CommandDeclaration extends CommandBuilder {
367
- constructor(name, globals) {
368
- super(freshState(name, globals));
444
+ constructor(name, options) {
445
+ // The options slot is read defensively, never inspected: an invalid value still yields
446
+ // `globals`, the core facts, and `extensions` of some kind, and `buildCommand` reports it.
447
+ super(freshState({
448
+ deprecated: options?.deprecated,
449
+ description: options?.description,
450
+ extensions: options?.extensions,
451
+ globals: options?.globals,
452
+ hidden: options?.hidden,
453
+ name,
454
+ options,
455
+ }));
369
456
  }
370
457
  }
371
- /** The public constructor requires a name and narrows the globals type to the supplied value. */
458
+ /** The public constructor takes a name and one options object, as the Application does. */
372
459
  export const Command = CommandDeclaration;
373
460
  /** Every declaration in the graph, so defaults are validated before any token is read. */
374
461
  export function collectInputs(command) {
@@ -377,6 +464,14 @@ export function collectInputs(command) {
377
464
  ...[...command.children.values()].flatMap((child) => collectInputs(child)),
378
465
  ];
379
466
  }
467
+ /**
468
+ * The names a routing failure offers: the canonical names of the visible children, in authoring
469
+ * order. A candidate list is a listing, so a hidden child is absent from it, and a parent whose
470
+ * children are all hidden offers none.
471
+ */
472
+ function candidatesOf(command) {
473
+ return [...command.children].filter(([, child]) => !child.hidden).map(([name]) => name);
474
+ }
380
475
  /** Bare tokens, names or aliases, select children until a Command has none; a hyphen commits. */
381
476
  export function route(root, tokens) {
382
477
  let command = root;
@@ -389,7 +484,7 @@ export function route(root, tokens) {
389
484
  }
390
485
  const child = command.routes.get(token);
391
486
  if (!child) {
392
- throw new UnknownCommandError(token, [...command.children.keys()]);
487
+ throw new UnknownCommandError(token, candidatesOf(command));
393
488
  }
394
489
  // An alias routes like the canonical name, and the path it walks reports that name alone.
395
490
  command = child.command;
@@ -428,25 +523,44 @@ function bindArguments(command, path, positionals) {
428
523
  }
429
524
  return values;
430
525
  }
431
- /** Consumes globals, routes to a Command, then validates globals and locals in one pass. */
432
- export async function selectCommand(graph, invocation) {
433
- const { host } = invocation;
434
- const scan = extractGlobals(graph.globals.options, [...host.argv]);
435
- const { command, path, tokens: rest } = route(graph.root, scan.rest);
526
+ /** Consumes the globals table, then routes the remaining bare tokens to a Command. */
527
+ export function routeInvocation(graph, argv) {
528
+ const scan = extractGlobals(graph.globals.options, [...argv]);
529
+ const routed = route(graph.root, scan.rest);
530
+ return {
531
+ command: routed.command,
532
+ path: routed.path,
533
+ scan: scan.values,
534
+ tokens: routed.tokens,
535
+ };
536
+ }
537
+ /**
538
+ * The phases the middleware chain terminates in: the callable check, local parsing, validation, and
539
+ * the action. It answers with the call that dispatches, so the caller records that the action was
540
+ * invoked at the moment it invokes it and no earlier failure reads as a dispatch.
541
+ */
542
+ export async function prepareDispatch(graph, routed, invocation) {
543
+ const { command, path, scan } = routed;
436
544
  const { dispatch } = command;
437
545
  // A group answers no invocation of its own, so it fails with the routing errors above it.
438
546
  if (!dispatch) {
439
- throw new NonCallableCommandError(path, [...command.children.keys()]);
547
+ throw new NonCallableCommandError(path, candidatesOf(command));
440
548
  }
441
- const parsed = parseInputs(command.options, rest);
549
+ const parsed = parseInputs(command.options, routed.tokens);
442
550
  const args = bindArguments(command, path, parsed.positionals);
443
551
  const values = await validateValues({
444
552
  command: path,
445
553
  defaults: invocation.defaults,
446
- host,
554
+ host: invocation.host,
447
555
  inputs: { globals: graph.globals.inputs, locals: command.inputs },
448
556
  passthrough: parsed.passthrough,
449
- supplied: { args, options: mergeValues(scan.values, parsed.options) },
557
+ supplied: { args, options: mergeValues(scan, parsed.options) },
558
+ });
559
+ return () => dispatch({
560
+ host: invocation.host,
561
+ out: invocation.out,
562
+ passthrough: parsed.passthrough,
563
+ signal: invocation.signal,
564
+ values,
450
565
  });
451
- return { dispatch, passthrough: parsed.passthrough, values };
452
566
  }
package/dist/errors.d.ts CHANGED
@@ -134,8 +134,16 @@ export type FailureRenderer = Pick<RegisteredFailure, typeof failureRegistration
134
134
  export declare function renderFailure<Failure extends LoomError>(type: abstract new (...args: never[]) => Failure, renderer: Renderer<Failure>): FailureRenderer;
135
135
  /** The renderers one application registered, keyed by the class each one names. */
136
136
  export type FailureRegistry = ReadonlyMap<unknown, Registration>;
137
- /** One class answers to one renderer, so a second registration for it is a declaration fault. */
138
- export declare function buildFailures(failures: readonly FailureRenderer[]): FailureRegistry;
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;
139
147
  /**
140
148
  * The report of one failure: the text core writes, and whether a registered renderer produced it.
141
149
  * An unrendered report carries core's own text, which the plain fallback path writes beside the
package/dist/errors.js CHANGED
@@ -3,6 +3,13 @@ function routedSentence(command) {
3
3
  const name = command.at(-1);
4
4
  return name === undefined ? 'The root Command' : commandSentence(name);
5
5
  }
6
+ /**
7
+ * The clause that offers the candidates, which is absent when there are none: every child of the
8
+ * Command is hidden, so the diagnostic ends after the sentence that names the fault.
9
+ */
10
+ function offering(candidates) {
11
+ return candidates.length === 0 ? '' : ` Use one of: ${candidates.join(', ')}.`;
12
+ }
6
13
  function shortGroupMessage(fault) {
7
14
  return fault.reason === 'value-position'
8
15
  ? `Value option "${fault.token}" must be last in its short group. Supply its value in the next token.`
@@ -46,10 +53,10 @@ class RegisteredFailure {
46
53
  }
47
54
  }
48
55
  /** Reads the pair behind a registered value; anything else is a declaration error. */
49
- function nodeOf(value) {
56
+ function nodeOf(value, subject) {
50
57
  const registration = nodes.get(value);
51
58
  if (!registration) {
52
- throw new DeclarationError('The Application holds a value that is not a failure renderer. Supply the value returned by renderFailure(type, renderer).');
59
+ throw new DeclarationError(`${subject} holds a value that is not a failure renderer. Supply the value returned by renderFailure(type, renderer).`);
53
60
  }
54
61
  return registration;
55
62
  }
@@ -96,7 +103,7 @@ export class UnknownCommandError extends UsageError {
96
103
  token;
97
104
  candidates;
98
105
  constructor(token, candidates) {
99
- super(`Unknown command "${token}". Use one of: ${candidates.join(', ')}.`);
106
+ super(`Unknown command "${token}".${offering(candidates)}`);
100
107
  this.candidates = candidates;
101
108
  this.name = 'UnknownCommandError';
102
109
  this.token = token;
@@ -107,7 +114,7 @@ export class NonCallableCommandError extends UsageError {
107
114
  command;
108
115
  candidates;
109
116
  constructor(command, candidates) {
110
- super(`${routedSentence(command)} requires a subcommand. Use one of: ${candidates.join(', ')}.`);
117
+ super(`${routedSentence(command)} requires a subcommand.${offering(candidates)}`);
111
118
  this.candidates = candidates;
112
119
  this.command = command;
113
120
  this.name = 'NonCallableCommandError';
@@ -225,18 +232,36 @@ export function renderFailure(type, renderer) {
225
232
  render: (failure) => (failure instanceof type ? renderer.render(failure) : undefined),
226
233
  });
227
234
  }
228
- /** One class answers to one renderer, so a second registration for it is a declaration fault. */
229
- export function buildFailures(failures) {
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') {
230
240
  const registry = new Map();
231
241
  for (const failure of failures) {
232
- const registration = nodeOf(failure);
242
+ const registration = nodeOf(failure, subject);
233
243
  if (registry.has(registration.prototype)) {
234
- throw new DeclarationError(`The Application registers two failure renderers for "${registration.name}". Remove one registration.`);
244
+ throw new DeclarationError(`${subject} registers two failure renderers for "${registration.name}". Remove one registration.`);
235
245
  }
236
246
  registry.set(registration.prototype, registration);
237
247
  }
238
248
  return registry;
239
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
+ }
240
265
  /**
241
266
  * The text core writes for one failure. Resolution walks the failure's prototype chain most
242
267
  * derived first through the application's registrations, then falls to core's own text, so a
@@ -0,0 +1,95 @@
1
+ import type { StandardSchemaV1 } from '@standard-schema/spec';
2
+ import type { ArgumentNode, CommandNode, OptionNode } from './inspect.js';
3
+ /** The three declaration kinds an extension can name, each with its own node in the graph. */
4
+ type ExtensionTarget = 'argument' | 'command' | 'option';
5
+ /**
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.
10
+ */
11
+ interface AnyExtension {
12
+ readonly identity: string;
13
+ readonly target: ExtensionTarget;
14
+ }
15
+ /** One carried value: the input its author supplied and the descriptor that produced it. */
16
+ interface CarriedValue {
17
+ descriptor: AnyExtension;
18
+ input: unknown;
19
+ }
20
+ /** Phantom key. It brands an extension value with its target and holds no runtime value. */
21
+ declare const extensionTarget: unique symbol;
22
+ /**
23
+ * The value one extension produces, branded with its target so a value on the wrong declaration is
24
+ * a compile error. It publishes the brand alone, so a value is never mistaken for the descriptor
25
+ * that produced it, and the input it carries stays private to this package.
26
+ */
27
+ declare class ExtensionCarrier<Target extends ExtensionTarget> {
28
+ readonly [extensionTarget]: (target: Target) => Target;
29
+ constructor(record: CarriedValue);
30
+ }
31
+ /** A branded extension value, keyed by its extension's identity and typed by its target. */
32
+ type ExtensionValue<Target extends ExtensionTarget> = Pick<ExtensionCarrier<Target>, typeof extensionTarget>;
33
+ /**
34
+ * 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.
36
+ */
37
+ interface Extension<Target extends ExtensionTarget = ExtensionTarget, Schema extends StandardSchemaV1 = StandardSchemaV1> extends AnyExtension {
38
+ (input: StandardSchemaV1.InferInput<Schema>): ExtensionValue<Target>;
39
+ readonly schema: Schema;
40
+ readonly target: Target;
41
+ }
42
+ /**
43
+ * 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.
45
+ */
46
+ declare function extension<Target extends ExtensionTarget, Schema extends StandardSchemaV1>(identity: string, config: {
47
+ schema: Schema;
48
+ target: Target;
49
+ }): 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;
52
+ /** Stored output is plain data the graph froze, so every read of it is read-only to any depth. */
53
+ type DeepReadonly<Value> = Value extends readonly (infer Item)[] ? readonly DeepReadonly<Item>[] : Value extends object ? {
54
+ readonly [Key in keyof Value]: DeepReadonly<Value[Key]>;
55
+ } : Value;
56
+ /**
57
+ * The typed read of one extension value. It takes the node kind the descriptor targets, returns the
58
+ * stored output or `undefined`, compares the descriptor by reference with the one that produced the
59
+ * value, and runs no schema.
60
+ */
61
+ declare function readExtension<Target extends ExtensionTarget, Schema extends StandardSchemaV1>(node: NodeFor<Target>, descriptor: Extension<Target, Schema>): DeepReadonly<StandardSchemaV1.InferOutput<Schema>> | undefined;
62
+ /** The declaration one `extensions` slot belongs to, as its own diagnostics name it. */
63
+ interface ExtensionSubject {
64
+ /** The subject after a preposition, such as `on Command "get"`. */
65
+ phrase: string;
66
+ /** The subject at the start of a sentence, such as `Command "get"`. */
67
+ sentence: string;
68
+ }
69
+ /** Every descriptor one build has met, so a second descriptor under one identity is visible. */
70
+ type DescriptorRegistry = Map<string, AnyExtension>;
71
+ /**
72
+ * The record each declaration published during one build, keyed by the declaration itself. The
73
+ * records live here rather than on the declaration, because one build's outputs belong to that
74
+ * build alone and inspection reads them back with the nodes it renders.
75
+ */
76
+ 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;
81
+ /** Everything one `extensions` slot needs to answer: whose it is, and what it may carry. */
82
+ interface ExtensionSlot {
83
+ declared: unknown;
84
+ descriptors: DescriptorRegistry;
85
+ subject: ExtensionSubject;
86
+ target: ExtensionTarget;
87
+ }
88
+ /**
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.
92
+ */
93
+ 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 };