@code0-tech/triangulum 0.37.2 → 0.38.1

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.
@@ -274,9 +296,10 @@ export declare const getSchema: (checker: ts.TypeChecker, node: ts.VariableDecla
274
296
  * concrete value must never surface one; the resolved element types (via
275
297
  * `items`) are kept, and the `list-file` `mimetype` is dropped because a plain
276
298
  * list has no such field.
277
- * - Drops an empty `suggestions` array. Node schemas are built with suggestions
278
- * enabled and therefore always carry the key — even when empty — whereas the
279
- * 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.
280
303
  *
281
304
  * The function-side schema still drives the final input kind in
282
305
  * {@link mergeSchemas}.
@@ -293,13 +316,17 @@ export declare const normalizeNodeSchema: (schema: Schema) => Schema;
293
316
  */
294
317
  export declare const nonNullishType: (type: ts.Type) => ts.Type;
295
318
  /**
296
- * Sets a schema's `suggestions` to the given set, dropping the key entirely when
297
- * the set is empty or missing. Suggestion-carrying schemas are built with the
298
- * key always present (an empty array when nothing matched), whereas the rest of
299
- * the pipeline omits it — so every schema that is spread into a result passes
300
- * through here instead of relying on the spread.
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.
301
328
  */
302
- export declare const withSuggestions: <T extends Schema>(schema: T, suggestions: Input["suggestions"] | undefined) => T;
329
+ export declare const withSuggestions: <T extends Schema>(schema: T, candidates: SuggestionCandidate[] | undefined) => T;
303
330
  /**
304
331
  * A declared (function-side) schema kept as the answer for its position, because
305
332
  * the node side has no counterpart to merge into it — e.g. a property the entered
@@ -311,7 +338,7 @@ export declare const withSuggestions: <T extends Schema>(schema: T, suggestions:
311
338
  * (see {@link genericNodeSchema}). Empty suggestion arrays are dropped so the
312
339
  * result follows the same convention as every other schema.
313
340
  */
314
- export declare const declaredSchema: (schema: Schema, anySuggestions?: Input["suggestions"]) => Schema;
341
+ export declare const declaredSchema: (schema: Schema, anySuggestions?: SuggestionCandidate[] | undefined) => Schema;
315
342
  /**
316
343
  * Treats a node-side schema as sitting in a fully generic ("accepts anything")
317
344
  * slot: the declared type constrains nothing here, so the value keeps its shape
@@ -338,9 +365,9 @@ export declare const declaredSchema: (schema: Schema, anySuggestions?: Input["su
338
365
  * This is what {@link ListInput.declaredItems} carries, and it never depends on the
339
366
  * entered value.
340
367
  */
341
- export declare const declaredItemsOf: (functionItems: Schema[] | undefined, anySuggestions?: Input["suggestions"]) => Schema[];
342
- export declare const genericNodeSchema: (schema: Schema, anySuggestions?: Input["suggestions"], overrideRoot?: boolean) => Schema;
343
- export declare const mergeSchemas: (functionSchema: Schema | undefined, nodeSchema: Schema, valueProvided?: boolean, anySuggestions?: Input["suggestions"], topLevel?: boolean) => Schema;
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;
344
371
  /**
345
372
  * Checks whether a union's members are the *options of one input* rather than
346
373
  * alternative inputs: a string/number literal union (`'GET' | 'POST' | …`, which
@@ -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
+ };