@loomcli/core 0.5.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. package/NOTICE +34 -0
  2. package/dist/application.d.ts +18 -2
  3. package/dist/application.js +302 -75
  4. package/dist/bindings.d.ts +15 -10
  5. package/dist/bindings.js +34 -15
  6. package/dist/capture.d.ts +65 -0
  7. package/dist/capture.js +99 -0
  8. package/dist/chain.d.ts +15 -6
  9. package/dist/chain.js +40 -20
  10. package/dist/command-rules.d.ts +55 -0
  11. package/dist/command-rules.js +142 -0
  12. package/dist/command.d.ts +65 -23
  13. package/dist/command.js +734 -236
  14. package/dist/controls.d.ts +8 -0
  15. package/dist/controls.js +23 -0
  16. package/dist/defect.d.ts +18 -0
  17. package/dist/defect.js +272 -0
  18. package/dist/developer.d.ts +24 -0
  19. package/dist/developer.js +52 -0
  20. package/dist/diagnostic-text.d.ts +81 -0
  21. package/dist/diagnostic-text.js +283 -0
  22. package/dist/diagnostic.d.ts +11 -0
  23. package/dist/diagnostic.js +70 -0
  24. package/dist/errors.d.ts +108 -24
  25. package/dist/errors.js +324 -53
  26. package/dist/exit-codes.d.ts +45 -0
  27. package/dist/exit-codes.js +46 -0
  28. package/dist/extension.d.ts +41 -8
  29. package/dist/extension.js +142 -57
  30. package/dist/facts.d.ts +71 -11
  31. package/dist/facts.js +106 -23
  32. package/dist/globals.d.ts +29 -21
  33. package/dist/globals.js +117 -46
  34. package/dist/glyphs.generated.js +1 -1
  35. package/dist/hints.d.ts +79 -0
  36. package/dist/hints.js +247 -0
  37. package/dist/host.d.ts +13 -0
  38. package/dist/host.js +43 -1
  39. package/dist/identity.d.ts +19 -0
  40. package/dist/identity.js +72 -0
  41. package/dist/index.d.ts +12 -2
  42. package/dist/index.js +6 -0
  43. package/dist/input-rules.d.ts +68 -0
  44. package/dist/input-rules.js +152 -0
  45. package/dist/inspect.d.ts +12 -12
  46. package/dist/inspect.js +92 -48
  47. package/dist/lanes.js +1 -1
  48. package/dist/locate.js +4 -4
  49. package/dist/options.d.ts +30 -2
  50. package/dist/options.js +143 -46
  51. package/dist/output.d.ts +9 -2
  52. package/dist/output.js +18 -2
  53. package/dist/plain.d.ts +56 -0
  54. package/dist/plain.js +238 -0
  55. package/dist/plugin-rules.d.ts +68 -0
  56. package/dist/plugin-rules.js +164 -0
  57. package/dist/plugin-settings.d.ts +18 -0
  58. package/dist/plugin-settings.js +38 -0
  59. package/dist/plugin.d.ts +27 -15
  60. package/dist/plugin.js +429 -144
  61. package/dist/prototypes.d.ts +7 -0
  62. package/dist/prototypes.js +29 -0
  63. package/dist/rendering.d.ts +6 -1
  64. package/dist/rendering.js +23 -5
  65. package/dist/rules.d.ts +51 -0
  66. package/dist/rules.js +115 -0
  67. package/dist/sequence.js +6 -1
  68. package/dist/sources.d.ts +6 -4
  69. package/dist/sources.js +25 -16
  70. package/dist/style-layout.js +2 -2
  71. package/dist/style-width.d.ts +13 -0
  72. package/dist/style-width.js +170 -0
  73. package/dist/style-wire.js +1 -1
  74. package/dist/style.js +1 -1
  75. package/dist/theme.d.ts +4 -0
  76. package/dist/theme.js +25 -5
  77. package/dist/thenable.d.ts +15 -0
  78. package/dist/thenable.js +29 -0
  79. package/dist/translators.d.ts +69 -0
  80. package/dist/translators.js +253 -0
  81. package/dist/types.d.ts +13 -3
  82. package/dist/unicode.generated.d.ts +27 -0
  83. package/dist/unicode.generated.js +1036 -0
  84. package/dist/validation.d.ts +69 -7
  85. package/dist/validation.js +211 -67
  86. package/dist/view.d.ts +61 -22
  87. package/dist/view.js +168 -81
  88. package/licenses/unicode-LICENSE.txt +41 -0
  89. package/licenses/uucode-LICENSE.md +35 -0
  90. package/package.json +9 -5
@@ -0,0 +1,152 @@
1
+ import { registerRule } from './diagnostic-text.js';
2
+ /*
3
+ * Core's rules for the declaration faults of arguments, options, validators, defaults, global
4
+ * options, and environment bindings. Each is declared once here and shared by every site that
5
+ * raises it, as a plugin's rules are.
6
+ */
7
+ /** An option declared with a type other than string or Boolean. */
8
+ const optionType = registerRule('@loomcli/core/option-type', {
9
+ explanation: 'The type decides how the parser reads an option: a string option consumes a value, and a Boolean option consumes none. Core reads no other kind.',
10
+ headline: 'Invalid option type',
11
+ });
12
+ /** A short alias that is not one ASCII letter. */
13
+ const shortAlias = registerRule('@loomcli/core/short-alias', {
14
+ explanation: 'An operator types a short alias as a hyphen and one letter, and several combine into one short group such as -tm, so each is one ASCII letter the parser can split apart.',
15
+ headline: 'Invalid short alias',
16
+ });
17
+ /**
18
+ * A yes-or-no declaration key, such as `required`, `hidden`, or an extension descriptor's
19
+ * `collect`, that holds a value other than a Boolean.
20
+ */
21
+ const flagNotBoolean = registerRule('@loomcli/core/flag-not-boolean', {
22
+ explanation: 'hidden, shortOnly, multiple, required, variadic, validateOmitted, and an extension\'s collect each answer one yes-or-no question about a declaration, so each holds true or false. A value such as the string "false" would read as true.',
23
+ headline: 'Flag not a Boolean',
24
+ });
25
+ /** `shortOnly` on an option that declares no short alias. */
26
+ const shortOnlyWithoutShort = registerRule('@loomcli/core/short-only-without-short', {
27
+ explanation: 'shortOnly removes every long spelling of an option, so an option with no short alias would leave an operator no spelling to type.',
28
+ headline: 'Short only with no short alias',
29
+ });
30
+ /** `multiple` on a Boolean option. */
31
+ const booleanOptionMultiple = registerRule('@loomcli/core/boolean-option-multiple', {
32
+ explanation: 'A Boolean option reports whether its spelling was supplied, so a repeat has no second value to collect. multiple collects each occurrence of a string option into an array.',
33
+ headline: 'Boolean option takes one value',
34
+ });
35
+ /** `polarity` on a string option. */
36
+ const polarityOnString = registerRule('@loomcli/core/polarity-on-string', {
37
+ explanation: 'Polarity chooses which long forms a Boolean option accepts and what its absence means. A string option takes its value from the operator, so it has no polarity.',
38
+ headline: 'Polarity on a string option',
39
+ });
40
+ /** A polarity outside the three settings. */
41
+ const optionPolarity = registerRule('@loomcli/core/option-polarity', {
42
+ explanation: "Polarity chooses a Boolean option's long forms and its absent value from three settings: positive, both, and negative.",
43
+ headline: 'Invalid polarity',
44
+ });
45
+ /** `polarity: 'both'` beside `shortOnly`. */
46
+ const shortOnlyBothPolarities = registerRule('@loomcli/core/short-only-both-polarities', {
47
+ explanation: 'Polarity both gives an option one spelling that turns it on and one that turns it off. shortOnly leaves the option its short alias alone, and one spelling sets one value.',
48
+ headline: 'Both polarities with short only',
49
+ });
50
+ /**
51
+ * One spelling that two options in one scope claim, the application's, a plugin's, or one a
52
+ * plugin's hook declared.
53
+ */
54
+ const spellingTaken = registerRule('@loomcli/core/spelling-taken', {
55
+ explanation: "The parser reads each spelling as one option, and a Command's own options share one invocation with the global options and every installed plugin's options. A spelling two options claim, a short alias or a generated negative form included, would reach only one of them.",
56
+ headline: 'Spelling used twice',
57
+ });
58
+ /**
59
+ * Two options with one declared name in one scope, the application's, a plugin's, or one a
60
+ * plugin's hook declared.
61
+ */
62
+ const optionDeclaredTwice = registerRule('@loomcli/core/option-declared-twice', {
63
+ explanation: "An action reads the global options and its Command's own options from one options object, each under its declared name, and the pre-scan reads the global options and every installed plugin's options from one table. Two options with one name in either leave one of them unreadable.",
64
+ headline: 'Option declared twice',
65
+ });
66
+ /**
67
+ * An input a plugin's `onCommandAttach` hook declares under a name that an input of the other kind
68
+ * already holds in the Command's scope: an argument under an option's name, or an option under an
69
+ * argument's name.
70
+ */
71
+ const nameSharedAcrossKinds = registerRule('@loomcli/core/name-shared-across-kinds', {
72
+ explanation: "An onCommandAttach hook adds inputs to a Command whose other inputs the plugin did not declare, so each name a hook declares stays apart from every argument and option in the Command's scope, whichever kind holds it. An author who gives an argument and an option one name does so knowingly, but a hook cannot see the Command's inputs, so a shared name there is an accident the author did not choose.",
73
+ headline: 'Argument and option share a name',
74
+ });
75
+ /** `required` or `validateOmitted` on a global option. */
76
+ const globalPresenceRule = registerRule('@loomcli/core/global-presence-rule', {
77
+ explanation: 'A global option is validated on every Command, the Commands of plugins included, so a rule that its value must exist would fail a Command that never reads it. An omitted global option is absent.',
78
+ headline: 'Presence rule on a global option',
79
+ });
80
+ /** `globalOption()` after the application's own `command()` or `action()`. */
81
+ const globalOptionAfterCommand = registerRule('@loomcli/core/global-option-after-command', {
82
+ explanation: 'A Command attached with command() and the root action read their types from the global options declared before them, so a global option declared later would reach an action whose types never name it.',
83
+ headline: 'Global option after a Command',
84
+ });
85
+ /** An environment binding on a multiple option. */
86
+ const envOnMultiple = registerRule('@loomcli/core/env-on-multiple', {
87
+ explanation: 'A variable holds one string, and core never splits it, so it cannot supply the several values a multiple option collects. The configuration source supplies a list.',
88
+ headline: 'Environment binding on a list',
89
+ });
90
+ /** An environment binding whose name is outside the variable name grammar. */
91
+ const envName = registerRule('@loomcli/core/env-name', {
92
+ explanation: 'The input-source stage reads the variable an option binds by its name, and a shell sets a variable only under a name of letters, digits, and underscores that does not start with a digit.',
93
+ headline: 'Invalid variable name',
94
+ });
95
+ /** An environment binding on an argument. */
96
+ const envOnArgument = registerRule('@loomcli/core/env-on-argument', {
97
+ explanation: 'An argument is identified by its place among the bare tokens, so no variable can stand in for it. An option names itself on the command line, which lets a variable fill it.',
98
+ headline: 'Environment binding on an argument',
99
+ });
100
+ /** One variable that two options in one invocation's scope bind. */
101
+ const variableBoundTwice = registerRule('@loomcli/core/variable-bound-twice', {
102
+ explanation: "Within one invocation's scope a variable fills one option, so two options that bind it would both take the value an operator set for one of them.",
103
+ headline: 'Variable bound twice',
104
+ });
105
+ /** `validateOmitted` on a declaration whose absence another rule already decides. */
106
+ const omissionAlreadyDecided = registerRule('@loomcli/core/omission-already-decided', {
107
+ explanation: 'validateOmitted sends an omitted value to its validator, which only an input with no other absence rule needs. An omitted required input fails, a default fills an omitted value, and an omitted multiple option or variadic argument receives an empty array.',
108
+ headline: 'Absence already decided',
109
+ });
110
+ /** `validateOmitted` on a declaration with no validator. */
111
+ const omissionWithoutValidator = registerRule('@loomcli/core/omission-without-validator', {
112
+ explanation: "validateOmitted sends an omitted value to the input's validator, so an input with no validator has nothing to receive it.",
113
+ headline: 'Omission with no validator',
114
+ });
115
+ /** `validate`, `default`, `required`, or `validateOmitted` on a Boolean option. */
116
+ const booleanOptionValueRule = registerRule('@loomcli/core/boolean-option-value-rule', {
117
+ explanation: 'A Boolean option consumes no value, so there is nothing to validate, and its polarity decides the value an absent option reads. validate, default, required, and validateOmitted belong to inputs that take a value.',
118
+ headline: 'Value rule on a Boolean option',
119
+ });
120
+ /** A required input that also declares a default. */
121
+ const requiredWithDefault = registerRule('@loomcli/core/required-with-default', {
122
+ explanation: 'A default fills an omitted value, and a required input fails when it is omitted, so a required input never reads its default.',
123
+ headline: 'Default on a required input',
124
+ });
125
+ /** A `validate` value that is not a Standard Schema v1 object. */
126
+ const notAValidator = registerRule('@loomcli/core/not-a-validator', {
127
+ explanation: "Core validates every value through the Standard Schema v1 interface: the object's ~standard property, with version 1, a vendor, and a validate function. It calls nothing else.",
128
+ headline: 'Not a Standard Schema',
129
+ });
130
+ /** A default of the wrong raw shape for its declaration. */
131
+ const defaultShape = registerRule('@loomcli/core/default-shape', {
132
+ explanation: "A default stands in for the value an operator would supply. Without a validator it is that raw value, a string or, for an input that takes several values, an array of strings; with one, it is the validator's input, and an input that takes several values still takes an array of them.",
133
+ headline: 'Default of the wrong shape',
134
+ });
135
+ /** The most arrays and plain objects any path through a declared default may hold. */
136
+ const defaultLevels = 10;
137
+ /** A declared default nested deeper than `defaultLevels`. */
138
+ const defaultDepth = registerRule('@loomcli/core/default-depth', {
139
+ explanation: `A default stands in for the value an operator would supply, and every reader of the graph, help and the manifest included, walks it. Core keeps every path through a default within ${String(defaultLevels)} levels of arrays and plain objects, and a default that holds itself nests without end, so every reader stays far inside the call stack on every runtime.`,
140
+ headline: 'Default nested too deep',
141
+ });
142
+ /** A declared default its validator rejected. */
143
+ const invalidDefault = registerRule('@loomcli/core/invalid-default', {
144
+ explanation: 'Each run passes every declared default through its validator before it reads a token, because a default reaches the action as a validated value. A default the validator rejects would reach no action, whatever the operator supplies.',
145
+ headline: 'Default rejected',
146
+ });
147
+ /** A validator's JSON Schema converter that threw or returned a value that is not a plain object. */
148
+ const schemaConverterFailed = registerRule('@loomcli/core/schema-converter-failed', {
149
+ explanation: "Help, the manifest, and completion read what an input accepts from the JSON Schema its validator publishes. A converter that throws or returns anything but a plain object publishes no shape, so a distributed build reads the input's schema as null.",
150
+ headline: 'Schema converter failed',
151
+ });
152
+ export { booleanOptionMultiple, booleanOptionValueRule, defaultDepth, defaultLevels, defaultShape, envName, envOnArgument, envOnMultiple, flagNotBoolean, globalOptionAfterCommand, globalPresenceRule, invalidDefault, notAValidator, omissionAlreadyDecided, nameSharedAcrossKinds, omissionWithoutValidator, optionDeclaredTwice, optionPolarity, optionType, polarityOnString, requiredWithDefault, schemaConverterFailed, shortAlias, shortOnlyBothPolarities, shortOnlyWithoutShort, spellingTaken, variableBoundTwice, };
package/dist/inspect.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { BuiltCommand, BuiltGraph } from './command.js';
2
+ import { InternalError } from './errors.js';
2
3
  import type { DeclaredResult } from './types.js';
3
4
  /** The plain JSON Schema a validated input publishes, or `null` where the graph holds no shape. */
4
5
  type InputSchema = Readonly<Record<string, unknown>> | null;
@@ -109,23 +110,17 @@ interface CommandGraph {
109
110
  readonly globals: readonly OptionNode[];
110
111
  readonly root: CommandNode;
111
112
  }
112
- /**
113
- * A snapshot of one value. Arrays and plain objects are copied and frozen to any depth, so a
114
- * consumer cannot reach the source through the copy, and a later call reports the value again.
115
- * Primitives and library objects, such as a class instance or a `Date` a schema produced, are
116
- * reported as they are, because core cannot copy them meaningfully. The graph reads it for a
117
- * declared value and the chain reads it for the request one middleware holds.
118
- */
119
- export declare function snapshot(value: unknown): unknown;
120
113
  /** The declared result as plain data, or `null` on a Command that declares none. */
121
114
  declare function resultNode(result: DeclaredResult | undefined): ResultNode | null;
122
115
  /**
123
- * Renders one built graph as frozen plain data. Nothing here reads a host fact or a schema. The
124
- * globals list holds the application's own options, then each installed plugin's in installation
125
- * order, which is the order the globals table holds them in.
116
+ * Renders one built graph as frozen plain data. Nothing here reads a host fact; each validated
117
+ * input's converter is asked for its input schema, and in a development build a converter that
118
+ * fails is a declaration fault. The globals list holds the application's own options, then each
119
+ * installed plugin's in installation order, which is the order the globals table holds them in.
126
120
  */
127
121
  declare function inspectGraph(name: string, graph: BuiltGraph, facts: {
128
122
  description: string | undefined;
123
+ development: boolean;
129
124
  version: string;
130
125
  }): CommandGraph;
131
126
  /**
@@ -142,6 +137,11 @@ interface GraphLink {
142
137
  * programming error rather than an invocation fault.
143
138
  */
144
139
  declare function linkOf(graph: CommandGraph): GraphLink;
140
+ /**
141
+ * The defect a graph reports when its nodes disagree with the build it was rendered from, so a
142
+ * node core looked up is missing.
143
+ */
144
+ declare function graphMismatch(sentence: string): InternalError;
145
145
  /**
146
146
  * The routed node inside the inspected graph, which routing already proved reachable. A missing
147
147
  * segment means the two readings of one graph disagree, so the run stops rather than hand a
@@ -149,4 +149,4 @@ declare function linkOf(graph: CommandGraph): GraphLink;
149
149
  */
150
150
  declare function nodeAt(graph: CommandGraph, path: readonly string[]): CommandNode;
151
151
  export type { ArgumentNode, CommandGraph, CommandNode, OptionNode, ResultNode };
152
- export { inspectGraph, linkOf, nodeAt, resultNode };
152
+ export { graphMismatch, inspectGraph, linkOf, nodeAt, resultNode };
package/dist/inspect.js CHANGED
@@ -1,6 +1,9 @@
1
- import { InternalError } from './errors.js';
2
- import { isPlainObject } from './facts.js';
3
- import { validatesOmission } from './validation.js';
1
+ import { asSentence, DeclarationError, InternalError, reasonOf } from './errors.js';
2
+ import { partOf, siteFinding } from './facts.js';
3
+ import { schemaConverterFailed } from './input-rules.js';
4
+ import { isPlainObject, snapshotRecord } from './plain.js';
5
+ import { foreignGraph, foreignGraphCorrection } from './rules.js';
6
+ import { declaringSite, inputPlace, validatesOmission } from './validation.js';
4
7
  /** A declaration that carries no extension value publishes one shared, empty frozen record. */
5
8
  const noExtensions = Object.freeze({});
6
9
  /**
@@ -18,28 +21,12 @@ function spellingsOf(table, name) {
18
21
  return spellings;
19
22
  }
20
23
  /**
21
- * A snapshot of one value. Arrays and plain objects are copied and frozen to any depth, so a
22
- * consumer cannot reach the source through the copy, and a later call reports the value again.
23
- * Primitives and library objects, such as a class instance or a `Date` a schema produced, are
24
- * reported as they are, because core cannot copy them meaningfully. The graph reads it for a
25
- * declared value and the chain reads it for the request one middleware holds.
24
+ * A declared default is wrapped, so `default: undefined` reads apart from no default at all. The
25
+ * value is the frozen snapshot the declaring call took, the one a run validates, so inspection
26
+ * copies nothing and every reader sees one value.
26
27
  */
27
- export function snapshot(value) {
28
- if (Array.isArray(value)) {
29
- return Object.freeze(value.map((entry) => snapshot(entry)));
30
- }
31
- if (isPlainObject(value)) {
32
- return snapshotRecord(value);
33
- }
34
- return value;
35
- }
36
- /** The snapshot of one plain object, under the record type the caller already established. */
37
- function snapshotRecord(value) {
38
- return Object.freeze(Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, snapshot(entry)])));
39
- }
40
- /** A declared default is wrapped, so `default: undefined` reads apart from no default at all. */
41
28
  function declaredDefault(config) {
42
- return 'default' in config ? Object.freeze({ value: snapshot(config.default) }) : undefined;
29
+ return 'default' in config ? Object.freeze({ value: config.default }) : undefined;
43
30
  }
44
31
  /**
45
32
  * The JSON Schema draft build asks every converter for, with no library options. Every converter
@@ -57,37 +44,68 @@ function publishesSchema(schema) {
57
44
  'output' in props.jsonSchema &&
58
45
  typeof props.jsonSchema.output === 'function');
59
46
  }
47
+ /**
48
+ * The converter fault a development build reports for one input, with the way it failed and, when
49
+ * the converter threw, the thrown value as its cause.
50
+ */
51
+ function converterFault(input, check, failed) {
52
+ const site = declaringSite(input, inputPlace(input, check));
53
+ const parts = {
54
+ correction: 'Fix the converter so it returns a JSON Schema object, or declare a validator that publishes none.',
55
+ findings: [siteFinding(site, partOf(site, 'validate'))],
56
+ sentence: `${site.subject} validator's JSON Schema converter ${failed.failure}`,
57
+ };
58
+ return 'cause' in failed
59
+ ? new DeclarationError(schemaConverterFailed, parts, { cause: failed.cause })
60
+ : new DeclarationError(schemaConverterFailed, parts);
61
+ }
60
62
  /**
61
63
  * The input-side schema a declaration's validator publishes, snapshotted the way a declared
62
64
  * default is, or `null` where the graph holds no published shape: no validator, a validator with
63
- * no converter, or a converter that throws or returns anything but a plain object. The contract of
64
- * 2026-09-19 made that last case a declaration error `inspect()` alone reports; that diagnostic is
65
- * held while the question of how a run tells development from a distributed application is
66
- * decided, so it reads `null` on both paths.
65
+ * no converter, or, in a distributed build, a converter that throws or returns anything but a plain
66
+ * object. A development build reports that last case as a declaration fault instead.
67
67
  */
68
- function inputSchema(config) {
68
+ function inputSchema(input, check) {
69
+ const { config } = input;
69
70
  const schema = 'validate' in config ? config.validate : undefined;
70
71
  if (schema === undefined) {
71
72
  return null;
72
73
  }
73
- // The converter is the library's code from the first property read.
74
- // A throw on reaching it and a throw on calling it are one failure.
74
+ let copied = undefined;
75
+ // The converter is the library's code from the first property read to the last key of its answer.
76
+ // A throw on reaching it, on calling it, or on reading what it answered is one failure.
75
77
  try {
76
78
  if (!publishesSchema(schema)) {
77
79
  return null;
78
80
  }
79
81
  const published = schema['~standard'].jsonSchema.input(schemaTarget);
80
- return isPlainObject(published) ? snapshotRecord(published) : null;
82
+ copied = isPlainObject(published) ? snapshotRecord(published) : undefined;
81
83
  }
82
- catch {
84
+ catch (error) {
85
+ if (check.development) {
86
+ throw converterFault(input, check, {
87
+ cause: error,
88
+ failure: `failed for target "${schemaTarget.target}": ${asSentence(reasonOf(error))}`,
89
+ });
90
+ }
83
91
  return null;
84
92
  }
93
+ if (copied !== undefined) {
94
+ return copied;
95
+ }
96
+ if (check.development) {
97
+ throw converterFault(input, check, {
98
+ failure: `answered target "${schemaTarget.target}" with a value that is not a plain object.`,
99
+ });
100
+ }
101
+ return null;
85
102
  }
86
103
  /** One declaration's extension record, which is the shared empty one when it carries no value. */
87
104
  function extensionsOf(records, declaration) {
88
105
  return records.get(declaration) ?? noExtensions;
89
106
  }
90
- function optionNode(input, { records, scope, table }) {
107
+ function optionNode(input, read) {
108
+ const { check, records, scope, table } = read;
91
109
  const { config, name } = input;
92
110
  const { long, negative, short } = spellingsOf(table, name);
93
111
  const extensions = extensionsOf(records, input);
@@ -119,7 +137,7 @@ function optionNode(input, { records, scope, table }) {
119
137
  multiple: config.multiple === true,
120
138
  name,
121
139
  required: config.required === true,
122
- schema: inputSchema(config),
140
+ schema: inputSchema(input, check),
123
141
  scope,
124
142
  short,
125
143
  type: 'string',
@@ -129,7 +147,8 @@ function optionNode(input, { records, scope, table }) {
129
147
  return Object.freeze(node);
130
148
  }
131
149
  /** The built slots already answer presence and arity, so the node repeats no config reading. */
132
- function argumentNode(slot, records) {
150
+ function argumentNode(slot, read) {
151
+ const { check, records } = read;
133
152
  const { config, name } = slot.input;
134
153
  const node = {
135
154
  default: declaredDefault(config),
@@ -137,7 +156,7 @@ function argumentNode(slot, records) {
137
156
  extensions: extensionsOf(records, slot.input),
138
157
  name,
139
158
  required: slot.required,
140
- schema: inputSchema(config),
159
+ schema: inputSchema(slot.input, check),
141
160
  validateOmitted: validatesOmission(slot.input),
142
161
  validated: config.validate !== undefined,
143
162
  variadic: slot.variadic,
@@ -152,18 +171,24 @@ function optionNodes(inputs, read) {
152
171
  * and the root reports the Application's, which is why the caller supplies that one.
153
172
  */
154
173
  function commandNode(command, place) {
155
- const { nodes, path, records } = place;
174
+ const { development, nodes, path, records } = place;
175
+ const check = { development, global: false, path };
156
176
  const node = {
157
177
  aliases: Object.freeze([...command.aliases]),
158
- arguments: Object.freeze(command.arguments.map((slot) => argumentNode(slot, records))),
159
- children: Object.freeze([...command.children].map(([name, child]) => commandNode(child, { nodes, path: Object.freeze([...path, name]), records }))),
178
+ arguments: Object.freeze(command.arguments.map((slot) => argumentNode(slot, { check, records }))),
179
+ children: Object.freeze([...command.children].map(([name, child]) => commandNode(child, { development, nodes, path: Object.freeze([...path, name]), records }))),
160
180
  deprecated: command.deprecated,
161
181
  description: 'description' in place ? place.description : command.description,
162
182
  extensions: command.extensions,
163
183
  hasAction: command.dispatch !== undefined,
164
184
  hidden: command.hidden,
165
185
  name: command.name,
166
- options: Object.freeze(optionNodes(command.inputs, { records, scope: 'application', table: command.options })),
186
+ options: Object.freeze(optionNodes(command.inputs, {
187
+ check,
188
+ records,
189
+ scope: 'application',
190
+ table: command.options,
191
+ })),
167
192
  path,
168
193
  result: resultNode(command.result),
169
194
  };
@@ -183,23 +208,27 @@ function resultNode(result) {
183
208
  });
184
209
  }
185
210
  /**
186
- * Renders one built graph as frozen plain data. Nothing here reads a host fact or a schema. The
187
- * globals list holds the application's own options, then each installed plugin's in installation
188
- * order, which is the order the globals table holds them in.
211
+ * Renders one built graph as frozen plain data. Nothing here reads a host fact; each validated
212
+ * input's converter is asked for its input schema, and in a development build a converter that
213
+ * fails is a declaration fault. The globals list holds the application's own options, then each
214
+ * installed plugin's in installation order, which is the order the globals table holds them in.
189
215
  */
190
216
  function inspectGraph(name, graph, facts) {
217
+ const { development } = facts;
191
218
  const records = graph.extensions;
192
219
  const table = graph.globals.options;
193
220
  const nodes = new WeakMap();
221
+ const check = { development, global: true, path: [] };
194
222
  const inspected = {
195
223
  description: facts.description,
196
224
  globals: Object.freeze([
197
- ...optionNodes(graph.globals.inputs, { records, scope: 'application', table }),
198
- ...graph.globals.plugins.flatMap((installed) => optionNodes(installed.inputs, { records, scope: 'plugin', table })),
225
+ ...optionNodes(graph.globals.inputs, { check, records, scope: 'application', table }),
226
+ ...graph.globals.plugins.flatMap((installed) => optionNodes(installed.inputs, { check, records, scope: 'plugin', table })),
199
227
  ]),
200
228
  name,
201
229
  root: commandNode(graph.root, {
202
230
  description: facts.description,
231
+ development,
203
232
  nodes,
204
233
  path: Object.freeze([]),
205
234
  records,
@@ -219,10 +248,25 @@ const links = new WeakMap();
219
248
  function linkOf(graph) {
220
249
  const link = links.get(graph);
221
250
  if (!link) {
222
- throw new InternalError('The graph was not produced by inspect(). Pass the graph inspect() returned.', undefined);
251
+ throw new InternalError(foreignGraph, {
252
+ cause: undefined,
253
+ correction: 'Pass the graph inspect() returned.',
254
+ sentence: 'The graph was not produced by inspect().',
255
+ });
223
256
  }
224
257
  return link;
225
258
  }
259
+ /**
260
+ * The defect a graph reports when its nodes disagree with the build it was rendered from, so a
261
+ * node core looked up is missing.
262
+ */
263
+ function graphMismatch(sentence) {
264
+ return new InternalError(foreignGraph, {
265
+ cause: undefined,
266
+ correction: foreignGraphCorrection,
267
+ sentence,
268
+ });
269
+ }
226
270
  /**
227
271
  * The routed node inside the inspected graph, which routing already proved reachable. A missing
228
272
  * segment means the two readings of one graph disagree, so the run stops rather than hand a
@@ -233,10 +277,10 @@ function nodeAt(graph, path) {
233
277
  for (const name of path) {
234
278
  const child = node.children.find((entry) => entry.name === name);
235
279
  if (!child) {
236
- throw new InternalError(`The routed command "${path.join(' ')}" is not in the inspected graph.`, undefined);
280
+ throw graphMismatch(`The routed command "${path.join(' ')}" is not in the inspected graph.`);
237
281
  }
238
282
  node = child;
239
283
  }
240
284
  return node;
241
285
  }
242
- export { inspectGraph, linkOf, nodeAt, resultNode };
286
+ export { graphMismatch, inspectGraph, linkOf, nodeAt, resultNode };
package/dist/lanes.js CHANGED
@@ -2,7 +2,7 @@ import { routedSubject } from './errors.js';
2
2
  import { glyph } from './glyphs.generated.js';
3
3
  import { view } from './view.js';
4
4
  /**
5
- * The identity prefix core's own views take, the package name by the plugin-identity convention.
5
+ * The identity prefix core's own views take, the package name by the plugin identity convention.
6
6
  * Core compiles from `src` alone, so the name is spelled here rather than read from the manifest.
7
7
  */
8
8
  const core = '@loomcli/core';
package/dist/locate.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { argumentSlot, readsAsChild, route } from './command.js';
2
- import { InternalError, UsageError } from './errors.js';
3
- import { linkOf } from './inspect.js';
2
+ import { UsageError } from './errors.js';
3
+ import { graphMismatch, linkOf } from './inspect.js';
4
4
  import { isOptionToken, longStringOption, longToken, scanGlobals, scanInputs } from './options.js';
5
5
  const none = Object.freeze({ kind: 'none' });
6
6
  /**
@@ -57,7 +57,7 @@ function valueOf(scope, name, word) {
57
57
  const option = graph.globals.find((entry) => entry.name === name) ??
58
58
  command.options.find((entry) => entry.name === name);
59
59
  if (!option) {
60
- throw new InternalError(`Option "${name}" is not in the inspected graph.`, undefined);
60
+ throw graphMismatch(`Option "${name}" is not in the inspected graph.`);
61
61
  }
62
62
  return { command, kind: 'value', lead: word.lead, option, prefix: word.prefix };
63
63
  }
@@ -114,7 +114,7 @@ function locate(graph, words) {
114
114
  }
115
115
  const command = link.nodes.get(earlier.command);
116
116
  if (!command) {
117
- throw new InternalError('The routed command is not in the inspected graph.', undefined);
117
+ throw graphMismatch('The routed command is not in the inspected graph.');
118
118
  }
119
119
  return lastWord({ built: link.graph, command, earlier, graph }, words.at(-1) ?? '');
120
120
  }
package/dist/options.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import type { InputSite } from './facts.js';
1
2
  import type { OptionConfig } from './types.js';
2
3
  export interface OptionDeclaration {
3
4
  name: string;
@@ -13,7 +14,7 @@ export interface OptionValues {
13
14
  /** Every parsed value lands in one of these maps; `lists` holds the repeated string options. */
14
15
  export declare function emptyValues(): OptionValues;
15
16
  /** Which accepted form a table entry is. The table owns the convention, so readers never re-derive it. */
16
- type SpellingRole = 'long' | 'negative' | 'short';
17
+ export type SpellingRole = 'long' | 'negative' | 'short';
17
18
  type OptionForm = {
18
19
  type: 'string';
19
20
  name: string;
@@ -26,6 +27,29 @@ type OptionForm = {
26
27
  type OptionSpelling = OptionForm & {
27
28
  role: SpellingRole;
28
29
  };
30
+ /**
31
+ * The part of one declaration that yields a spelling of the given role, which a spelling fault
32
+ * marks: the declared name for the long form, `short` for the short alias, and the `polarity` that
33
+ * generates a negative form.
34
+ */
35
+ export declare function spellingMark(site: InputSite, role: SpellingRole): string;
36
+ /** The declared name answers the declared-name rule an argument's name answers. */
37
+ export declare function checkOptionName(name: unknown, site: InputSite): void;
38
+ /** Whether a declared short alias is one ASCII letter, the rule every short spelling answers. */
39
+ export declare function isShortAlias(short: unknown): boolean;
40
+ /** The sentence and correction of the short-alias fault for the option `subject` names. */
41
+ export declare function shortAliasText(subject: string): {
42
+ sentence: string;
43
+ correction: string;
44
+ };
45
+ /**
46
+ * The scope one table compiles: the phrase a repeated name names it by, and where each of its
47
+ * options was declared, which every fault's findings rebuild.
48
+ */
49
+ export interface CompileScope<Declaration extends OptionDeclaration> {
50
+ readonly subject: string;
51
+ readonly siteOf: (declaration: Declaration) => InputSite;
52
+ }
29
53
  /**
30
54
  * One Boolean option's value for one invocation: the value the parser consumed, or the value its
31
55
  * declared polarity gives an absent option. A negative-only option is absent as `true`, because its
@@ -33,7 +57,11 @@ type OptionSpelling = OptionForm & {
33
57
  * declaration answer the same rule.
34
58
  */
35
59
  export declare function booleanValue(values: OptionValues, name: string, config: OptionConfig): boolean;
36
- export declare function compileOptions(declarations: readonly OptionDeclaration[], subject: string): Map<string, OptionSpelling>;
60
+ /**
61
+ * One scope's options compiled into the spelling table the parser reads, with every rule one
62
+ * declaration answers alone and every rule two of them answer together.
63
+ */
64
+ export declare function compileOptions<Declaration extends OptionDeclaration>(declarations: readonly Declaration[], scope: CompileScope<Declaration>): Map<string, OptionSpelling>;
37
65
  /**
38
66
  * Whether a token reads as an option: it starts with a hyphen. Routing stops at one, and a
39
67
  * separate value is never one. The parser and `locate` read each token through this rule.