@code0-tech/triangulum 0.37.1 → 0.38.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.
@@ -1,5 +1,7 @@
1
1
  import { default as ts, FunctionDeclaration } from 'typescript';
2
- import { FunctionDefinition, LiteralValue, NodeFunction, ReferenceValue, SubFlowValue } from '@code0-tech/sagittarius-graphql-types';
2
+ import { FunctionDefinition } from '@code0-tech/sagittarius-graphql-types';
3
+ import { SuggestionCandidate, SuggestionCertainty, SuggestionValue } from './suggestion.util';
4
+ export type { SuggestionCandidate, SuggestionCertainty, SuggestionMatch, SuggestionMatchReason, SuggestionValue, } from './suggestion.util';
3
5
  /**
4
6
  * Maps a data type identifier to a dedicated input kind.
5
7
  *
@@ -50,7 +52,27 @@ export interface Input {
50
52
  */
51
53
  type?: string;
52
54
  /** Array of suggested values (functions, references, or literals) */
53
- suggestions?: (NodeFunction | ReferenceValue | LiteralValue | SubFlowValue)[];
55
+ suggestions?: SuggestionValue[];
56
+ /**
57
+ * How certainly each entry of `suggestions` produces this slot's type,
58
+ * index-aligned with it and of the same length — `suggestions[i]` is
59
+ * described by `suggestionCertainty[i]`.
60
+ *
61
+ * A suggestion is `exact` when its own type is assignable to the slot's, and
62
+ * `possible` when the match was only reached by weakening one of the two
63
+ * sides: a generic return matched through its constraint, an `any` that fits
64
+ * everything, a union whose branches do not all fit, a value that may be
65
+ * nullish where the slot is not (see {@link SuggestionCertainty}).
66
+ *
67
+ * Both kinds are listed — a `possible` candidate is often the one the user
68
+ * wants — but the split answers two questions the flat list cannot: whether a
69
+ * slot has *dedicated* producers (which a consumer may want to offer ahead of
70
+ * building a value by hand), and whether picking one is the case that may
71
+ * need a cast to type-check.
72
+ *
73
+ * Present exactly when `suggestions` is.
74
+ */
75
+ suggestionCertainty?: SuggestionCertainty[];
54
76
  }
55
77
  /**
56
78
  * Represents a generic input type with no specific structure.
@@ -189,8 +211,42 @@ export interface DataInput extends Input {
189
211
  */
190
212
  export interface ListInput extends Input {
191
213
  input?: "list";
192
- /** Schema or array of schemas for list items */
214
+ /**
215
+ * Schema or array of schemas for list items.
216
+ *
217
+ * In a parameter schema this is *value-driven*: once an array value is
218
+ * entered it holds exactly one entry per entered element, so it says what the
219
+ * list currently contains — not what an element may be. Use
220
+ * {@link ListInput.declaredItems} for the latter.
221
+ *
222
+ * An entered entry only ever *refines* one of the `declaredItems`: it is that
223
+ * option's input kind, properties and suggestions, with the concrete value's
224
+ * `type` and per-element cardinality filled in. A union element type is
225
+ * resolved to the member the entered element picked, so the COLOR entries of a
226
+ * `LIST<COLOR | OBJECT<…>>` stay color inputs and its object entries keep the
227
+ * declared object's property schemas.
228
+ */
193
229
  items?: Schema[];
230
+ /**
231
+ * The item schemas the *declared* element type produces — what `items` holds
232
+ * while no value is entered, and therefore the answer to "what may an element
233
+ * of this list be": its input kind, and the suggestions an element slot offers.
234
+ * A union element contributes one entry per member (e.g. all six methods of a
235
+ * `LIST<HTTP_METHOD>`), so the entries enumerate the element's options rather
236
+ * than the list's contents.
237
+ *
238
+ * Always present alongside `items` on an input slot's schema — an entered
239
+ * value can empty `items` (`[]`) or reduce it to the elements that happen to be
240
+ * there, and neither tells the UI what to render for a new element or which
241
+ * candidates the element slot accepts. An unconstrained element slot (a generic
242
+ * `LIST<T>`, or a list in a slot the declared type does not describe) yields a
243
+ * single generic entry carrying the "accepts anything" suggestion set.
244
+ *
245
+ * Omitted on schemas that describe a *produced* value rather than an input slot
246
+ * (a signature's return type, {@link getTypeSchema}); there `items` is already
247
+ * the declared expansion.
248
+ */
249
+ declaredItems?: Schema[];
194
250
  }
195
251
  /**
196
252
  * Represents a type input.
@@ -240,14 +296,49 @@ export declare const getSchema: (checker: ts.TypeChecker, node: ts.VariableDecla
240
296
  * concrete value must never surface one; the resolved element types (via
241
297
  * `items`) are kept, and the `list-file` `mimetype` is dropped because a plain
242
298
  * list has no such field.
243
- * - Drops an empty `suggestions` array. Node schemas are built with suggestions
244
- * enabled and therefore always carry the key — even when empty — whereas the
245
- * rest of the pipeline omits it entirely when there are none.
299
+ * - Drops an empty `suggestions` array (and its `suggestionCertainty` twin). Node
300
+ * schemas are built with suggestions enabled and therefore always carry the
301
+ * keys — even when empty — whereas the rest of the pipeline omits them
302
+ * entirely when there are none.
246
303
  *
247
304
  * The function-side schema still drives the final input kind in
248
305
  * {@link mergeSchemas}.
249
306
  */
250
307
  export declare const normalizeNodeSchema: (schema: Schema) => Schema;
308
+ /**
309
+ * Strips `undefined` and `null` from a union type so the *declared* shape can be
310
+ * inspected. An optional parameter (or property) resolves to
311
+ * `<declared> | undefined`, and a union carries none of its members' type flags —
312
+ * so a test like "is this an object" or `checker.isArrayType` answers `false` for
313
+ * every optional slot unless the nullish part is removed first. A union that has
314
+ * more than one real member left is returned unchanged: there is no single
315
+ * declared shape to speak of then.
316
+ */
317
+ export declare const nonNullishType: (type: ts.Type) => ts.Type;
318
+ /**
319
+ * Sets a schema's suggestions to the given candidates, dropping both the
320
+ * `suggestions` key and its `suggestionCertainty` twin entirely when the set is
321
+ * empty or missing. Suggestion-carrying schemas are built with the keys always
322
+ * present (empty arrays when nothing matched), whereas the rest of the pipeline
323
+ * omits them — so every schema that is spread into a result passes through here
324
+ * instead of relying on the spread.
325
+ *
326
+ * Taking candidates rather than two parallel arrays is what keeps the pair
327
+ * aligned: there is no call site that can set one without the other.
328
+ */
329
+ export declare const withSuggestions: <T extends Schema>(schema: T, candidates: SuggestionCandidate[] | undefined) => T;
330
+ /**
331
+ * A declared (function-side) schema kept as the answer for its position, because
332
+ * the node side has no counterpart to merge into it — e.g. a property the entered
333
+ * value does not carry, or a union-typed position whose members no longer line up.
334
+ *
335
+ * The declared schema already carries the suggestions of every level it
336
+ * describes; only a fully generic slot inside it is rewritten to the constant
337
+ * `any` set, which is what the merge itself would have produced for such a slot
338
+ * (see {@link genericNodeSchema}). Empty suggestion arrays are dropped so the
339
+ * result follows the same convention as every other schema.
340
+ */
341
+ export declare const declaredSchema: (schema: Schema, anySuggestions?: SuggestionCandidate[] | undefined) => Schema;
251
342
  /**
252
343
  * Treats a node-side schema as sitting in a fully generic ("accepts anything")
253
344
  * slot: the declared type constrains nothing here, so the value keeps its shape
@@ -264,8 +355,37 @@ export declare const normalizeNodeSchema: (schema: Schema) => Schema;
264
355
  * type-parameter constraint — e.g. `keyof T` — so they are kept). Descendants are
265
356
  * always overridden regardless.
266
357
  */
267
- export declare const genericNodeSchema: (schema: Schema, anySuggestions?: Input["suggestions"], overrideRoot?: boolean) => Schema;
268
- export declare const mergeSchemas: (functionSchema: Schema | undefined, nodeSchema: Schema, valueProvided?: boolean, anySuggestions?: Input["suggestions"], topLevel?: boolean) => Schema;
358
+ /**
359
+ * The declared element expansion of a list slot: the function-side `items`, each
360
+ * kept as the declared answer for its position (see {@link declaredSchema}). An
361
+ * element the declared type leaves unconstrained — a generic `LIST<T>`, or a list
362
+ * in a slot with no declared type at all — yields a single generic entry carrying
363
+ * the "accepts anything" set, which is what such an element slot accepts.
364
+ *
365
+ * This is what {@link ListInput.declaredItems} carries, and it never depends on the
366
+ * entered value.
367
+ */
368
+ export declare const declaredItemsOf: (functionItems: Schema[] | undefined, anySuggestions?: SuggestionCandidate[] | undefined) => Schema[];
369
+ export declare const genericNodeSchema: (schema: Schema, anySuggestions?: SuggestionCandidate[] | undefined, overrideRoot?: boolean) => Schema;
370
+ export declare const mergeSchemas: (functionSchema: Schema | undefined, nodeSchema: Schema, valueProvided?: boolean, anySuggestions?: SuggestionCandidate[] | undefined, topLevel?: boolean) => Schema;
371
+ /**
372
+ * Checks whether a union's members are the *options of one input* rather than
373
+ * alternative inputs: a string/number literal union (`'GET' | 'POST' | …`, which
374
+ * renders as a single select offering all of them) or `boolean` (internally the
375
+ * union `true | false`, which renders as a single boolean input).
376
+ *
377
+ * The distinction decides whether an entered value may narrow a union-typed slot
378
+ * to the single member it picked. For an options union it must not: narrowing
379
+ * `HTTP_METHOD` to the entered "GET" would hide the five other options of the
380
+ * very select the user is filling. For a union of alternative inputs
381
+ * (`COLOR | OBJECT<…>`, `TEXT | NUMBER`) it must: the member the value picked is
382
+ * the one input of the set that describes it, and it is also the entry the slot's
383
+ * {@link ListInput.declaredItems} already offers for it.
384
+ *
385
+ * @param type - The type to check
386
+ * @returns True if the type is a union whose members are options of one input
387
+ */
388
+ export declare function isOptionsUnion(type: ts.Type): boolean;
269
389
  /**
270
390
  * Checks if a type is a callable type (has call signatures).
271
391
  *
@@ -1,5 +1,6 @@
1
1
  import { default as ts } from 'typescript';
2
- import { FunctionDefinition, SubFlowValue } from '@code0-tech/sagittarius-graphql-types';
2
+ import { FunctionDefinition } from '@code0-tech/sagittarius-graphql-types';
3
+ import { SuggestionCandidate } from './suggestion.util';
3
4
  /**
4
5
  * Extracts and creates a list of SubFlowValue objects from function declarations
5
6
  * that are compatible with the given parameter type.
@@ -13,7 +14,8 @@ import { FunctionDefinition, SubFlowValue } from '@code0-tech/sagittarius-graphq
13
14
  * @param {FunctionDefinition[]} functions - Array of function definitions for reference lookup
14
15
  * @param {ts.Type} paramType - The target parameter type that functions must be compatible with
15
16
  *
16
- * @returns {SubFlowValue[]} An array of SubFlowValue objects for compatible functions,
17
- * or an empty array if the paramType is not a sub-flow type
17
+ * @returns {SuggestionCandidate[]} An array of sub-flow candidates for compatible
18
+ * functions, each carrying how certainly it satisfies the slot, or an
19
+ * empty array if the paramType is not a sub-flow type
18
20
  */
19
- export declare const getSubFlows: (checker: ts.TypeChecker, functionDeclarations: ts.FunctionDeclaration[], functions: FunctionDefinition[], paramType: ts.Type) => SubFlowValue[];
21
+ export declare const getSubFlows: (checker: ts.TypeChecker, functionDeclarations: ts.FunctionDeclaration[], functions: FunctionDefinition[], paramType: ts.Type) => SuggestionCandidate[];
@@ -0,0 +1,161 @@
1
+ import { default as ts } from 'typescript';
2
+ import { LiteralValue, NodeFunction, ReferenceValue, SubFlowValue } from '@code0-tech/sagittarius-graphql-types';
3
+ /**
4
+ * A value a slot can be filled with: a literal option of the slot's own type, a
5
+ * reference to something already in scope, a function node whose result is fed
6
+ * into the slot, or a function bound as a sub-flow.
7
+ */
8
+ export type SuggestionValue = NodeFunction | ReferenceValue | LiteralValue | SubFlowValue;
9
+ /**
10
+ * How certain it is that a suggestion actually produces the slot's type.
11
+ *
12
+ * - `exact`: the candidate's own type is assignable to the slot's type. Picking
13
+ * it always yields a value the slot accepts.
14
+ * - `possible`: the match was only reached by weakening one of the two sides —
15
+ * the candidate *can* produce a fitting value, but is not guaranteed to. See
16
+ * {@link SuggestionMatchReason} for what was weakened.
17
+ *
18
+ * Both kinds are offered: a `possible` candidate is frequently the one the user
19
+ * wants (a generic `std::object::get`, a reference into an optional field). The
20
+ * distinction exists so a consumer can tell "this slot has dedicated producers"
21
+ * from "everything happens to fit here", and because a `possible` pick is the
22
+ * case that may need a cast to type-check.
23
+ */
24
+ export type SuggestionMatch = "exact" | "possible";
25
+ /**
26
+ * Why a suggestion matched only possibly.
27
+ *
28
+ * - `unconstrained`: one of the two sides constrains nothing (`any`, `unknown`,
29
+ * `{}`) — the candidate fits every slot, or the slot is fit by every candidate.
30
+ * Either way the match says nothing about this pairing.
31
+ * - `generic`: the candidate's type is a type parameter and the match was made
32
+ * against its constraint. A concrete instantiation may be narrower.
33
+ * - `union-member`: the candidate is a union and only some of its members fit.
34
+ * Which member materializes is a runtime decision.
35
+ * - `nullable`: the candidate may be `null`/`undefined` where the slot is not.
36
+ *
37
+ * Listed weakest first: a candidate weakened on several axes reports the first
38
+ * of these that applies (see {@link weakestCertainty}).
39
+ */
40
+ export type SuggestionMatchReason = "unconstrained" | "generic" | "union-member" | "nullable";
41
+ /**
42
+ * The certainty of a single suggestion. `reason` is present exactly when
43
+ * `match` is `possible`.
44
+ */
45
+ export interface SuggestionCertainty {
46
+ match: SuggestionMatch;
47
+ reason?: SuggestionMatchReason;
48
+ }
49
+ /**
50
+ * A suggestion together with its certainty — the form suggestions travel in
51
+ * while a schema is being built.
52
+ *
53
+ * On the finished schema the two halves are split into the index-aligned
54
+ * `suggestions` / `suggestionCertainty` pair (see {@link Input}) so the
55
+ * suggestion values stay plain GraphQL values a consumer can use as-is. Every
56
+ * step in between passes candidates around instead of the two arrays, so the
57
+ * alignment cannot drift.
58
+ */
59
+ export interface SuggestionCandidate {
60
+ value: SuggestionValue;
61
+ certainty: SuggestionCertainty;
62
+ }
63
+ /** The certainty of a candidate that is guaranteed to fit its slot. */
64
+ export declare const EXACT_MATCH: SuggestionCertainty;
65
+ /** The certainty of a candidate that fits only under the given condition. */
66
+ export declare const possibleMatch: (reason: SuggestionMatchReason) => SuggestionCertainty;
67
+ /** Pairs each value with {@link EXACT_MATCH}, for sources that cannot be weakened. */
68
+ export declare const exactCandidates: (values: SuggestionValue[]) => SuggestionCandidate[];
69
+ /**
70
+ * Picks the stronger of two certainties for the same value — `exact` wins, and
71
+ * among two `possible` ones the first is kept. Used when the same suggestion is
72
+ * collected twice against differently scoped types (see the merge in
73
+ * getSignatureSchema): the slot accepts it for the better of the two reasons.
74
+ */
75
+ export declare const strongerCertainty: (a: SuggestionCertainty, b: SuggestionCertainty) => SuggestionCertainty;
76
+ /**
77
+ * Picks the weakest of the given certainties: a candidate weakened on more than
78
+ * one axis (e.g. a generic return that also turns out to be `any`) is only as
79
+ * strong as its weakest link. `exact` is the result only when nothing was
80
+ * weakened at all.
81
+ */
82
+ export declare const weakestCertainty: (...certainties: SuggestionCertainty[]) => SuggestionCertainty;
83
+ /**
84
+ * True for a type that constrains nothing, so an assignability test against it
85
+ * is vacuous: `any` and `unknown`, the empty object type — `{}`, which every
86
+ * non-nullish value satisfies and which is what a generic `OBJECT<T>` or an
87
+ * uninstantiated payload bound resolves to — and an object whose only structure
88
+ * is an index signature that says nothing about its values either. That last
89
+ * case is what an `OBJECT<T>` instantiated without an argument resolves to:
90
+ * `{[key: string]: any}`, a slot every object fits. A signature with a real
91
+ * value type (`Record<string, NUMBER>`) does constrain, and keeps its slot
92
+ * concrete.
93
+ *
94
+ * It matters on both sides of a match. A candidate of this type fits every slot,
95
+ * and a *slot* of this type is fitted by every candidate — the second case is
96
+ * the one that would otherwise report a hundred exact "producers" for a slot
97
+ * that simply accepts anything.
98
+ *
99
+ * A callable, an array and anything with properties is excluded: those
100
+ * constrain their values. An index signature is inspected only `depth` levels
101
+ * deep (one, from the outside) because an index type may refer back to its own
102
+ * container (`type Json = {[key: string]: Json}`).
103
+ */
104
+ export declare const isUnconstrainedType: (type: ts.Type | undefined, depth?: number) => boolean;
105
+ /**
106
+ * Decides whether a candidate type fits an expected (slot) type, and how
107
+ * certainly — the shared rule behind every assignability-based suggestion
108
+ * source.
109
+ *
110
+ * Returns `null` when the candidate does not fit at all. Otherwise the match is
111
+ * `exact`, or `possible` with the reason the fit is not guaranteed:
112
+ *
113
+ * - either side constrains nothing (`any`, `unknown`, `{}`) → `unconstrained`.
114
+ * For the candidate that means it fits everywhere; for the slot that it is
115
+ * fitted by everything. Neither tells the consumer that this candidate is a
116
+ * producer *of this type*, which is the whole point of the distinction. This
117
+ * only *weakens* the verdict — whether the candidate fits at all is still
118
+ * decided by assignability, so an open candidate is not waved through into a
119
+ * slot that would reject it;
120
+ * - the candidate is a union and only some members fit → `union-member`. A
121
+ * union is only assignable as a whole when *every* member is, so the partial
122
+ * case is exactly the one strict assignability would reject while the slot can
123
+ * still be served at runtime;
124
+ * - the candidate may be nullish where the slot is not → `nullable`. The nullish
125
+ * part is deliberately ignored for matching (a `string | null` producer is a
126
+ * useful suggestion for a `string` slot), which is what makes the distinction
127
+ * worth reporting.
128
+ *
129
+ * A purely nullish candidate strips down to `never`, which is assignable to
130
+ * anything, and is therefore rejected explicitly.
131
+ */
132
+ export declare const typeMatchCertainty: (type: ts.Type, checker: ts.TypeChecker, expectedType: ts.Type) => SuggestionCertainty | null;
133
+ /**
134
+ * Reads a schema's suggestions back as candidates. A schema that carries
135
+ * suggestions without certainties (produced before the pair existed, or hand
136
+ * written) is read as fully exact rather than rejected.
137
+ */
138
+ export declare const suggestionCandidates: (schema: {
139
+ suggestions?: SuggestionValue[];
140
+ suggestionCertainty?: SuggestionCertainty[];
141
+ }) => SuggestionCandidate[];
142
+ /**
143
+ * Splits candidates into the schema's index-aligned pair of keys, both keys
144
+ * always present — the convention of a freshly collected schema, which carries
145
+ * the keys even when nothing matched (an empty array marks "this is an input
146
+ * slot and these are all its candidates", which normalizeNodeSchema then
147
+ * collapses). Use {@link suggestionFields} everywhere else.
148
+ */
149
+ export declare const suggestionPair: (candidates: SuggestionCandidate[]) => {
150
+ suggestions: SuggestionValue[];
151
+ suggestionCertainty: SuggestionCertainty[];
152
+ };
153
+ /**
154
+ * Splits candidates back into the schema's index-aligned pair of keys. Both keys
155
+ * are omitted for an empty set, matching the convention that a schema without
156
+ * suggestions carries neither key.
157
+ */
158
+ export declare const suggestionFields: (candidates: SuggestionCandidate[] | undefined) => {
159
+ suggestions?: SuggestionValue[];
160
+ suggestionCertainty?: SuggestionCertainty[];
161
+ };