@telorun/analyzer 0.65.0 → 0.66.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/analyzer.d.ts +1 -1
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/analyzer.js +157 -63
- package/dist/call-graph.d.ts.map +1 -1
- package/dist/call-graph.js +7 -1
- package/dist/cel-environment.d.ts +12 -0
- package/dist/cel-environment.d.ts.map +1 -1
- package/dist/cel-environment.js +35 -19
- package/dist/cel-scope.d.ts.map +1 -1
- package/dist/cel-scope.js +24 -4
- package/dist/definition-registry.d.ts +24 -0
- package/dist/definition-registry.d.ts.map +1 -1
- package/dist/definition-registry.js +41 -0
- package/dist/eval-paths.d.ts +68 -0
- package/dist/eval-paths.d.ts.map +1 -1
- package/dist/eval-paths.js +138 -0
- package/dist/flatten-for-analyzer.d.ts +1 -1
- package/dist/flatten-for-analyzer.js +1 -1
- package/dist/import-resolution-diagnostics.d.ts +1 -1
- package/dist/import-resolution-diagnostics.js +1 -1
- package/dist/index.d.ts +9 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -5
- package/dist/manifest-visitor.d.ts.map +1 -1
- package/dist/manifest-visitor.js +8 -1
- package/dist/migrations/entries/index.d.ts.map +1 -1
- package/dist/migrations/entries/index.js +2 -0
- package/dist/migrations/entries/schema-prepare-bucket.json +27 -0
- package/dist/peer-binding.d.ts +232 -0
- package/dist/peer-binding.d.ts.map +1 -0
- package/dist/peer-binding.js +418 -0
- package/dist/ref-slot.d.ts +13 -0
- package/dist/ref-slot.d.ts.map +1 -1
- package/dist/ref-slot.js +26 -0
- package/dist/reference-field-map.d.ts +28 -0
- package/dist/reference-field-map.d.ts.map +1 -1
- package/dist/reference-field-map.js +25 -0
- package/dist/referrer-rule.d.ts +17 -0
- package/dist/referrer-rule.d.ts.map +1 -1
- package/dist/referrer-rule.js +4 -1
- package/dist/release/index.d.ts +1 -1
- package/dist/release/index.js +1 -1
- package/dist/resource-rule.d.ts +40 -27
- package/dist/resource-rule.d.ts.map +1 -1
- package/dist/resource-rule.js +61 -7
- package/dist/rule-condition.d.ts +11 -0
- package/dist/rule-condition.d.ts.map +1 -1
- package/dist/rule-condition.js +13 -0
- package/dist/schema-projection.d.ts +106 -4
- package/dist/schema-projection.d.ts.map +1 -1
- package/dist/schema-projection.js +206 -18
- package/dist/telo-version.d.ts +1 -1
- package/dist/telo-version.js +1 -1
- package/dist/validate-cel-context.d.ts +1 -17
- package/dist/validate-cel-context.d.ts.map +1 -1
- package/dist/validate-cel-context.js +5 -70
- package/dist/validate-reference-forms.d.ts +10 -0
- package/dist/validate-reference-forms.d.ts.map +1 -1
- package/dist/validate-reference-forms.js +23 -0
- package/dist/validate-references.d.ts.map +1 -1
- package/dist/validate-references.js +9 -2
- package/dist/validate-referrer-rules.d.ts +38 -4
- package/dist/validate-referrer-rules.d.ts.map +1 -1
- package/dist/validate-referrer-rules.js +139 -31
- package/dist/validate-resource-rules.d.ts +11 -2
- package/dist/validate-resource-rules.d.ts.map +1 -1
- package/dist/validate-resource-rules.js +18 -12
- package/dist/validate-schema-projection.d.ts.map +1 -1
- package/dist/validate-schema-projection.js +33 -2
- package/dist/with-synthetic-positions.d.ts +1 -1
- package/dist/with-synthetic-positions.js +1 -1
- package/package.json +3 -3
- package/src/analyzer.ts +191 -68
- package/src/call-graph.ts +7 -1
- package/src/cel-environment.ts +48 -19
- package/src/cel-scope.ts +24 -6
- package/src/definition-registry.ts +42 -0
- package/src/eval-paths.ts +152 -0
- package/src/flatten-for-analyzer.ts +1 -1
- package/src/import-resolution-diagnostics.ts +1 -1
- package/src/index.ts +45 -4
- package/src/manifest-visitor.ts +8 -1
- package/src/migrations/entries/index.ts +2 -0
- package/src/migrations/entries/schema-prepare-bucket.json +27 -0
- package/src/peer-binding.ts +641 -0
- package/src/ref-slot.ts +36 -0
- package/src/reference-field-map.ts +42 -0
- package/src/referrer-rule.ts +20 -1
- package/src/release/index.ts +1 -1
- package/src/resource-rule.ts +69 -7
- package/src/rule-condition.ts +15 -0
- package/src/schema-projection.ts +283 -20
- package/src/telo-version.ts +1 -1
- package/src/validate-cel-context.ts +6 -71
- package/src/validate-reference-forms.ts +22 -0
- package/src/validate-references.ts +15 -2
- package/src/validate-referrer-rules.ts +215 -28
- package/src/validate-resource-rules.ts +26 -12
- package/src/validate-schema-projection.ts +50 -1
- package/src/with-synthetic-positions.ts +1 -1
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `peers:` / `entry` bindings of a referrer rule — a rule's view of the
|
|
3
|
+
* SIBLING declarations a referrer lists beside the one that reached it.
|
|
4
|
+
*
|
|
5
|
+
* A rename marker is wrong only in relation to the *other* declarations a schema
|
|
6
|
+
* lists, and an enum a column names is unlisted only in relation to the same set.
|
|
7
|
+
* A resource rule sees one resource; a referrer rule sees a pair joined by one
|
|
8
|
+
* reference; neither can state a relation between siblings. This is the binding
|
|
9
|
+
* that can: a JSON Pointer naming a collection OF THE REFERRER, resolved.
|
|
10
|
+
*
|
|
11
|
+
* Four decisions carry the design:
|
|
12
|
+
*
|
|
13
|
+
* - **The FIELD MAP decides what is a reference, never the value's shape.** A
|
|
14
|
+
* `{kind, name}` object is what a resolved `!ref` looks like, but it is also
|
|
15
|
+
* author data that happens to carry two common keys — and sniffing for it would
|
|
16
|
+
* either resolve such data to an unrelated manifest or abort the binding as
|
|
17
|
+
* unresolved. The referrer kind's ref-slot paths are the authority, and the
|
|
18
|
+
* caller already holds them.
|
|
19
|
+
* - **Entries bind AS WRITTEN, with the references inside them resolved one
|
|
20
|
+
* level.** A collection's items are not always references — a server's
|
|
21
|
+
* `mounts:` holds `{mount, prefix}` — so a peer is the declaration where the
|
|
22
|
+
* entry is a bare `!ref`, and the declaration with its siblings beside it where
|
|
23
|
+
* it is not. One level only: references inside a resolved declaration stay
|
|
24
|
+
* references, since a self-referencing foreign key and a mutual pair are both
|
|
25
|
+
* cycles.
|
|
26
|
+
* - **`self` is excluded by SLOT PATH, not by identity** — exact, cheap, and
|
|
27
|
+
* correct when the same resource is listed twice. An ARRAY collection excludes
|
|
28
|
+
* by index and a MAP collection by key, because the concrete path spells them
|
|
29
|
+
* differently (`tables[2]` versus `tables.orders`) and reading only the first
|
|
30
|
+
* left every entry of a map judging itself. A `peers:` naming a collection
|
|
31
|
+
* *other* than the one that reached me excludes nothing, because nothing there
|
|
32
|
+
* is me.
|
|
33
|
+
* - **A collection is resolved ONCE per referrer and pointer.** A rule evaluates
|
|
34
|
+
* per entry, so re-resolving and re-cloning the whole collection each time is
|
|
35
|
+
* quadratic in the number of entries — at the editor's keystroke-time analysis,
|
|
36
|
+
* and with the rule budget then reporting a correct rule as a defective one.
|
|
37
|
+
*
|
|
38
|
+
* A bound element is scanned for `!cel` here, where it is known to be a
|
|
39
|
+
* declaration. The evaluator's own scan cannot do it: `findDynamicLeaf` stops at
|
|
40
|
+
* any nested `{kind}` object — the guard that keeps an inline resource's
|
|
41
|
+
* expressions out of an enclosing rule's verdict — and every resolved peer is
|
|
42
|
+
* exactly that shape, so the whole peer set was exempt and a duplicate hidden
|
|
43
|
+
* behind an expression compared against a sentinel and silently held. See
|
|
44
|
+
* `dynamicInDeclaration` for how far the scan reaches and why it stops there.
|
|
45
|
+
* Both halves classify through `dynamicNode`, so a `!ref` is a reference here
|
|
46
|
+
* and a reference there rather than an expression to one of them.
|
|
47
|
+
*
|
|
48
|
+
* **One unresolvable peer fails the whole binding**, rather than binding what
|
|
49
|
+
* resolved and dropping the rest. A peer rule's condition is characteristically
|
|
50
|
+
* an existential or a universal over the set (`peers.exists(…)`,
|
|
51
|
+
* `peers.all(…)`), and a partial set answers both of those WRONGLY and silently:
|
|
52
|
+
* dropping the one peer a duplicate check was about turns a violation into a
|
|
53
|
+
* pass. A skip is reported and names the reference that did not resolve; a
|
|
54
|
+
* verdict over a set quietly missing a member is not.
|
|
55
|
+
*
|
|
56
|
+
* Peers-by-kind was rejected rather than deferred: a binding over "every resource
|
|
57
|
+
* of this kind in the analysis" needs no resolution and is unsound, because a
|
|
58
|
+
* physical name is scoped by its namespace and two schema resources over one
|
|
59
|
+
* connection would report a conflict between objects that never meet. The
|
|
60
|
+
* reference collection is what defines the scope.
|
|
61
|
+
*
|
|
62
|
+
* Browser-safe: no Node built-ins.
|
|
63
|
+
*/
|
|
64
|
+
import type { ResourceManifest } from "@telorun/sdk";
|
|
65
|
+
/**
|
|
66
|
+
* The half of a definition registry both halves below need.
|
|
67
|
+
*
|
|
68
|
+
* Structural, the `ProjectionScope` precedent: a resolver rather than the
|
|
69
|
+
* concrete registry, so this module keeps its own dependencies to the annotation
|
|
70
|
+
* readers and a second host can supply its own.
|
|
71
|
+
*/
|
|
72
|
+
export interface PeerBinderRegistry {
|
|
73
|
+
effectiveSchema(kind: string): Record<string, any> | undefined;
|
|
74
|
+
resolve(kind: string): {
|
|
75
|
+
kind?: string;
|
|
76
|
+
} | undefined;
|
|
77
|
+
getByExtends(kind: string): {
|
|
78
|
+
metadata: {
|
|
79
|
+
module?: string;
|
|
80
|
+
name?: unknown;
|
|
81
|
+
};
|
|
82
|
+
}[];
|
|
83
|
+
getFieldMap(kind: string): Iterable<[string, any]> | undefined;
|
|
84
|
+
getFieldMapForKind(kind: string, aliases: PeerAliasScope): Iterable<[string, any]> | undefined;
|
|
85
|
+
}
|
|
86
|
+
/** The alias scope a reference is resolved in: `moduleForAlias` for the
|
|
87
|
+
* declaration lookup, `resolveKind` for the field map it is handed to. */
|
|
88
|
+
export interface PeerAliasScope {
|
|
89
|
+
moduleForAlias(alias: string): string | undefined;
|
|
90
|
+
resolveKind(kind: string): string | undefined;
|
|
91
|
+
}
|
|
92
|
+
/** What a rule sees for one entry of a referrer's collection. */
|
|
93
|
+
export interface PeerBinding {
|
|
94
|
+
/** The collection's other entries, references resolved one level. */
|
|
95
|
+
readonly peers: unknown[];
|
|
96
|
+
/** The referrer's own entry that reached `self`, resolved the same way. Where
|
|
97
|
+
* the entry is a bare reference this IS the referenced declaration. */
|
|
98
|
+
readonly entry: unknown;
|
|
99
|
+
}
|
|
100
|
+
/** Why a binding could not be produced — reported rather than dropped, because a
|
|
101
|
+
* check whose coverage varies invisibly reads as passing. */
|
|
102
|
+
export interface PeerBindingFailure {
|
|
103
|
+
/**
|
|
104
|
+
* - `unresolved` — a reference names a declaration this analysis has not
|
|
105
|
+
* loaded, so the entry would bind to nothing.
|
|
106
|
+
* - `no-collection` — the pointer resolves to something that is not a
|
|
107
|
+
* collection, absent included.
|
|
108
|
+
* - `dynamic` — a value inside a bound declaration holds a `!cel`, so the
|
|
109
|
+
* comparison would run against a placeholder.
|
|
110
|
+
* - `unknown-shape` — the referrer's kind is not resolvable here, so which of
|
|
111
|
+
* its paths hold references is not known.
|
|
112
|
+
*/
|
|
113
|
+
readonly reason: "unresolved" | "no-collection" | "dynamic" | "unknown-shape";
|
|
114
|
+
/** Where, in the referrer, for the diagnostic. */
|
|
115
|
+
readonly at: string;
|
|
116
|
+
/** For `dynamic`: the noun phrase naming what sits there, quoted verbatim by
|
|
117
|
+
* the diagnostic. A `!ref` is never one — it names a declaration, which is
|
|
118
|
+
* the value a peer rule compares. */
|
|
119
|
+
readonly what?: string;
|
|
120
|
+
}
|
|
121
|
+
export type PeerBindingResult = {
|
|
122
|
+
readonly ok: true;
|
|
123
|
+
readonly binding: PeerBinding;
|
|
124
|
+
} | {
|
|
125
|
+
readonly ok: false;
|
|
126
|
+
readonly failure: PeerBindingFailure;
|
|
127
|
+
};
|
|
128
|
+
/** A reference as it reaches evaluation: the `{kind, name, alias?}` shape Phase
|
|
129
|
+
* 2.5 rewrites a `!ref` into, or the sentinel itself where that pass has not
|
|
130
|
+
* run (a round-trip view). Both are read, so a rule means one thing whichever
|
|
131
|
+
* way the host loaded the manifest. */
|
|
132
|
+
export interface ReferenceValue {
|
|
133
|
+
readonly name: string;
|
|
134
|
+
readonly alias?: string;
|
|
135
|
+
readonly kind?: string;
|
|
136
|
+
}
|
|
137
|
+
/** Resolves a reference to the manifest it names, or `undefined` when this
|
|
138
|
+
* analysis holds no such declaration. Supplied by the caller, which owns the
|
|
139
|
+
* manifest set and the alias scope. */
|
|
140
|
+
export type DeclarationLookup = (ref: ReferenceValue) => ResourceManifest | undefined;
|
|
141
|
+
/** What the binder needs from its host. Both halves are the caller's because
|
|
142
|
+
* only it holds the definition registry and the manifest set. */
|
|
143
|
+
export interface PeerBinderEnv {
|
|
144
|
+
readonly declarationOf: DeclarationLookup;
|
|
145
|
+
/** The ref-slot field paths a referrer kind declares (`tables[]`,
|
|
146
|
+
* `mounts[].mount`, `tables.{}`) — the authority on which paths hold
|
|
147
|
+
* references. `undefined` when the kind is not resolvable, which binds
|
|
148
|
+
* nothing rather than guessing. */
|
|
149
|
+
readonly refSlotsOf: (kind: string) => readonly string[] | undefined;
|
|
150
|
+
}
|
|
151
|
+
/** The reference a value holds, in either shape, or `undefined`. Read only at a
|
|
152
|
+
* path the field map declares to be a reference slot. */
|
|
153
|
+
export declare function referenceValueOf(value: unknown): ReferenceValue | undefined;
|
|
154
|
+
/** True when a concrete path is an instance of a field-map shape — `tables[2]`
|
|
155
|
+
* of `tables[]`, `mounts[1].mount` of `mounts[].mount`, `tables.orders` of
|
|
156
|
+
* `tables.{}`. A map key containing a dot is not distinguishable here, the same
|
|
157
|
+
* ambiguity the concrete path itself carries. */
|
|
158
|
+
export declare function shapeMatches(concrete: string, shape: string): boolean;
|
|
159
|
+
/** The concrete path of the ENTRY a slot path sits in, truncated at the first
|
|
160
|
+
* `[]` / `{}` marker of the shape it matches — which is where an entry begins,
|
|
161
|
+
* everything after it being the entry's own shape. */
|
|
162
|
+
export declare function entryBoundary(concrete: string, shape: string): string;
|
|
163
|
+
/** Navigate a dotted/bracketed path (`mounts[1].mount`) within a value — the
|
|
164
|
+
* spelling the call graph gives an edge, so an edge's own path is what reads
|
|
165
|
+
* the entry it came from. */
|
|
166
|
+
export declare function navigatePath(value: unknown, path: string): unknown;
|
|
167
|
+
/**
|
|
168
|
+
* Binds `peers` and `entry`, resolving each referrer's collection once.
|
|
169
|
+
*
|
|
170
|
+
* One binder per analysis: the cache is what keeps a rule over an n-entry
|
|
171
|
+
* collection linear in resolution work rather than quadratic, and it is shared
|
|
172
|
+
* by the evaluation and the exercised check, which ask the same question.
|
|
173
|
+
*/
|
|
174
|
+
export declare class PeerBinder {
|
|
175
|
+
private readonly env;
|
|
176
|
+
constructor(env: PeerBinderEnv);
|
|
177
|
+
private readonly collections;
|
|
178
|
+
/**
|
|
179
|
+
* @param slotPath concrete path of the edge that reached the referenced
|
|
180
|
+
* resource, e.g. `tables[2]` or `mounts[1].mount`.
|
|
181
|
+
*/
|
|
182
|
+
bind(referrer: ResourceManifest, referrerKind: string, pointer: string, slotPath: string): PeerBindingResult;
|
|
183
|
+
/** True when the rule has something to compare — the input to the
|
|
184
|
+
* never-exercised report, asked through the same cache. */
|
|
185
|
+
hasPeers(referrer: ResourceManifest, referrerKind: string, pointer: string, slotPath: string): boolean;
|
|
186
|
+
private collection;
|
|
187
|
+
private resolveCollection;
|
|
188
|
+
/** The array index or map key `slotPath` occupies in the peers collection, or
|
|
189
|
+
* `undefined` when the path does not run through it at all. */
|
|
190
|
+
private entryKey;
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* What a peer rule's `peers:` pointer names in the kind its `referrer:` filters
|
|
194
|
+
* to — the strict half's question, answered where the binding vocabulary lives
|
|
195
|
+
* so the checker and the binder read one field map.
|
|
196
|
+
*
|
|
197
|
+
* - `unknown` — the referrer kind is not resolvable here; say nothing.
|
|
198
|
+
* - `absent` — the kind declares no such collection.
|
|
199
|
+
* - `plain` — a collection of plain data: nothing in it resolves, so the rule
|
|
200
|
+
* would never see a declaration.
|
|
201
|
+
* - `ok` — a collection whose items are, or contain, a reference.
|
|
202
|
+
*/
|
|
203
|
+
export type PeersTarget = "unknown" | "absent" | "plain" | "ok";
|
|
204
|
+
/**
|
|
205
|
+
* The binder for one analysis run.
|
|
206
|
+
*
|
|
207
|
+
* ONE per run: it caches each referrer's resolved collection, which is what
|
|
208
|
+
* keeps a rule over an n-entry collection from re-resolving that collection once
|
|
209
|
+
* per entry — and both the evaluation and the exercised check ask for it.
|
|
210
|
+
*
|
|
211
|
+
* Built here rather than assembled at the analysis site, the
|
|
212
|
+
* `analyzerContractScope` precedent: both halves are the binding's own rule
|
|
213
|
+
* expressed against the analyzer's registry, and inline they were 40 lines of a
|
|
214
|
+
* 2700-line pass, using two different field-map accessors for one question.
|
|
215
|
+
*/
|
|
216
|
+
export declare function analyzerPeerBinder(registry: PeerBinderRegistry, aliases: PeerAliasScope, manifests: readonly ResourceManifest[]): PeerBinder;
|
|
217
|
+
/**
|
|
218
|
+
* The strict half's resolver for a `peers:` pointer.
|
|
219
|
+
*
|
|
220
|
+
* Liskov in BOTH directions, which is what the check has to be: the filter is
|
|
221
|
+
* usually an abstract (`Sql.Schema`, so one rule serves every backend) while the
|
|
222
|
+
* collection is declared by the backends that implement it, so a pointer
|
|
223
|
+
* resolving on any candidate resolves the rule. Reported only when NO candidate
|
|
224
|
+
* declares it, or when every candidate that does holds plain data — the two
|
|
225
|
+
* shapes where the rule would see no declaration at all.
|
|
226
|
+
*
|
|
227
|
+
* Takes only the registry: it is answered after the registration loop and before
|
|
228
|
+
* the manifest list exists, which is also why it is a separate function rather
|
|
229
|
+
* than a method on one environment object.
|
|
230
|
+
*/
|
|
231
|
+
export declare function analyzerPeersTarget(registry: PeerBinderRegistry): (referrerKind: string, pointer: string) => PeersTarget;
|
|
232
|
+
//# sourceMappingURL=peer-binding.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"peer-binding.d.ts","sourceRoot":"","sources":["../src/peer-binding.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
import { isRefEntry } from "./reference-field-map.js";
|
|
2
|
+
import { dynamicNode, findDynamicLeaf, pointerToPath, resolvePointer, } from "./resource-rule.js";
|
|
3
|
+
import { isIterableSchema, schemaAtPointer } from "./validate-resource-rules.js";
|
|
4
|
+
function isObject(value) {
|
|
5
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
6
|
+
}
|
|
7
|
+
/** The reference a value holds, in either shape, or `undefined`. Read only at a
|
|
8
|
+
* path the field map declares to be a reference slot. */
|
|
9
|
+
export function referenceValueOf(value) {
|
|
10
|
+
if (!isObject(value))
|
|
11
|
+
return undefined;
|
|
12
|
+
if (value.__tagged === true && value.engine === "ref" && typeof value.source === "string") {
|
|
13
|
+
const dot = value.source.indexOf(".");
|
|
14
|
+
return dot > 0
|
|
15
|
+
? { alias: value.source.slice(0, dot), name: value.source.slice(dot + 1) }
|
|
16
|
+
: { name: value.source };
|
|
17
|
+
}
|
|
18
|
+
if (typeof value.name === "string" && typeof value.kind === "string") {
|
|
19
|
+
return {
|
|
20
|
+
name: value.name,
|
|
21
|
+
kind: value.kind,
|
|
22
|
+
...(typeof value.alias === "string" ? { alias: value.alias } : {}),
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
/** True when a concrete path is an instance of a field-map shape — `tables[2]`
|
|
28
|
+
* of `tables[]`, `mounts[1].mount` of `mounts[].mount`, `tables.orders` of
|
|
29
|
+
* `tables.{}`. A map key containing a dot is not distinguishable here, the same
|
|
30
|
+
* ambiguity the concrete path itself carries. */
|
|
31
|
+
export function shapeMatches(concrete, shape) {
|
|
32
|
+
const c = concrete.split(".");
|
|
33
|
+
const s = shape.split(".");
|
|
34
|
+
if (c.length !== s.length)
|
|
35
|
+
return false;
|
|
36
|
+
return s.every((segment, i) => {
|
|
37
|
+
if (segment === "{}")
|
|
38
|
+
return true;
|
|
39
|
+
if (segment.endsWith("[]")) {
|
|
40
|
+
const base = segment.slice(0, -2);
|
|
41
|
+
return c[i].startsWith(`${base}[`) && /^\[\d+\]$/.test(c[i].slice(base.length));
|
|
42
|
+
}
|
|
43
|
+
return segment === c[i];
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
/** The concrete path of the ENTRY a slot path sits in, truncated at the first
|
|
47
|
+
* `[]` / `{}` marker of the shape it matches — which is where an entry begins,
|
|
48
|
+
* everything after it being the entry's own shape. */
|
|
49
|
+
export function entryBoundary(concrete, shape) {
|
|
50
|
+
const c = concrete.split(".");
|
|
51
|
+
const s = shape.split(".");
|
|
52
|
+
for (let i = 0; i < s.length; i++) {
|
|
53
|
+
if (s[i] === "{}" || s[i].endsWith("[]"))
|
|
54
|
+
return c.slice(0, i + 1).join(".");
|
|
55
|
+
}
|
|
56
|
+
return concrete;
|
|
57
|
+
}
|
|
58
|
+
/** Navigate a dotted/bracketed path (`mounts[1].mount`) within a value — the
|
|
59
|
+
* spelling the call graph gives an edge, so an edge's own path is what reads
|
|
60
|
+
* the entry it came from. */
|
|
61
|
+
export function navigatePath(value, path) {
|
|
62
|
+
if (path === "")
|
|
63
|
+
return value;
|
|
64
|
+
let current = value;
|
|
65
|
+
for (const segment of path.split(".")) {
|
|
66
|
+
const parsed = /^([^[\]]*)((?:\[\d+\])*)$/.exec(segment);
|
|
67
|
+
if (!parsed)
|
|
68
|
+
return undefined;
|
|
69
|
+
const [, key, indices] = parsed;
|
|
70
|
+
if (key !== "") {
|
|
71
|
+
if (!isObject(current))
|
|
72
|
+
return undefined;
|
|
73
|
+
current = current[key];
|
|
74
|
+
}
|
|
75
|
+
for (const index of indices.match(/\d+/g) ?? []) {
|
|
76
|
+
if (!Array.isArray(current))
|
|
77
|
+
return undefined;
|
|
78
|
+
current = current[Number(index)];
|
|
79
|
+
}
|
|
80
|
+
if (current === undefined)
|
|
81
|
+
return undefined;
|
|
82
|
+
}
|
|
83
|
+
return current;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* The entry shape a collection's items take — `tables[]` for an array,
|
|
87
|
+
* `tables.{}` for a map — as the field map spells it.
|
|
88
|
+
*
|
|
89
|
+
* Chosen by the collection's RUNTIME type rather than by whichever shape the map
|
|
90
|
+
* lists first: the two spell an entry's concrete path differently (`tables[2]`
|
|
91
|
+
* versus `tables.orders`), so taking the wrong one leaves every entry unable to
|
|
92
|
+
* recognise itself.
|
|
93
|
+
*/
|
|
94
|
+
function entryShapeOf(shapes, collectionPath, isArray) {
|
|
95
|
+
const wanted = isArray ? `${collectionPath}[]` : `${collectionPath}.{}`;
|
|
96
|
+
return shapes.some((shape) => shape === wanted || shape.startsWith(`${wanted}.`))
|
|
97
|
+
? wanted
|
|
98
|
+
: undefined;
|
|
99
|
+
}
|
|
100
|
+
/** Which paths inside one entry hold a reference: the item itself, or the named
|
|
101
|
+
* properties one level in. */
|
|
102
|
+
function entryRefsOf(shapes, entryShape) {
|
|
103
|
+
const properties = new Set();
|
|
104
|
+
let itemIsRef = false;
|
|
105
|
+
for (const shape of shapes) {
|
|
106
|
+
if (shape === entryShape) {
|
|
107
|
+
itemIsRef = true;
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
if (!shape.startsWith(`${entryShape}.`))
|
|
111
|
+
continue;
|
|
112
|
+
const rest = shape.slice(entryShape.length + 1);
|
|
113
|
+
if (!rest.includes("."))
|
|
114
|
+
properties.add(rest);
|
|
115
|
+
}
|
|
116
|
+
return { itemIsRef, properties };
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* The first `!cel` among the fields of a resolved DECLARATION that a peer rule
|
|
120
|
+
* can actually COMPARE — its own top-level scalars.
|
|
121
|
+
*
|
|
122
|
+
* Which fields a condition reads off an element cannot be recovered from the
|
|
123
|
+
* parse: cel-js emits no access chain for a comprehension variable, so
|
|
124
|
+
* `peers.exists(p, p.table == …)` yields only `peers` and nothing about `table`.
|
|
125
|
+
* The bound is drawn structurally instead, and top-level scalars are the honest
|
|
126
|
+
* line: a physical name, a type name, a marker — the identities a peer rule
|
|
127
|
+
* compares. Reaching deeper needs a comprehension over nested config, and a rule
|
|
128
|
+
* of that shape has an unbounded subject anyway.
|
|
129
|
+
*
|
|
130
|
+
* Scanning the whole declaration was tried first and is what this replaced: it
|
|
131
|
+
* disabled every rule over a table the moment any nested field held an
|
|
132
|
+
* expression — a seed's `when:`, a column default — which is noise where the
|
|
133
|
+
* rule was about a physical name.
|
|
134
|
+
*/
|
|
135
|
+
function dynamicInDeclaration(declaration) {
|
|
136
|
+
for (const [key, value] of Object.entries(declaration)) {
|
|
137
|
+
if (key === "metadata")
|
|
138
|
+
continue;
|
|
139
|
+
const dynamic = dynamicNode(value, key);
|
|
140
|
+
if (dynamic)
|
|
141
|
+
return dynamic;
|
|
142
|
+
}
|
|
143
|
+
return undefined;
|
|
144
|
+
}
|
|
145
|
+
/** One entry with the references INSIDE it resolved — the declaration itself for
|
|
146
|
+
* a bare `!ref`, or the entry with each declared reference property replaced. */
|
|
147
|
+
function resolveEntry(value, at, refs, lookup) {
|
|
148
|
+
if (refs.itemIsRef) {
|
|
149
|
+
const reference = referenceValueOf(value);
|
|
150
|
+
// An inline declaration (`{kind, …config}` with no name) is not a reference;
|
|
151
|
+
// it binds as written, exactly as any other non-reference entry does.
|
|
152
|
+
if (reference) {
|
|
153
|
+
const declaration = lookup(reference);
|
|
154
|
+
if (!declaration)
|
|
155
|
+
return { ok: false, failure: { reason: "unresolved", at } };
|
|
156
|
+
const dynamic = dynamicInDeclaration(declaration);
|
|
157
|
+
if (dynamic) {
|
|
158
|
+
return {
|
|
159
|
+
ok: false,
|
|
160
|
+
failure: { reason: "dynamic", at: `${at} → ${dynamic.path}`, what: dynamic.what },
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
return { ok: true, value: declaration };
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
if (!isObject(value))
|
|
167
|
+
return { ok: true, value };
|
|
168
|
+
const out = {};
|
|
169
|
+
for (const [key, child] of Object.entries(value)) {
|
|
170
|
+
if (!refs.properties.has(key)) {
|
|
171
|
+
// The author's OWN entry data beside the reference (`prefix` next to
|
|
172
|
+
// `mount`) — small, and read directly by a rule, so it is scanned whole.
|
|
173
|
+
const dynamic = findDynamicLeaf(child, `${at}.${key}`);
|
|
174
|
+
if (dynamic !== undefined) {
|
|
175
|
+
return { ok: false, failure: { reason: "dynamic", at: dynamic.path, what: dynamic.what } };
|
|
176
|
+
}
|
|
177
|
+
out[key] = child;
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
const reference = referenceValueOf(child);
|
|
181
|
+
if (!reference) {
|
|
182
|
+
out[key] = child;
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
const declaration = lookup(reference);
|
|
186
|
+
if (!declaration)
|
|
187
|
+
return { ok: false, failure: { reason: "unresolved", at: `${at}.${key}` } };
|
|
188
|
+
const dynamic = dynamicInDeclaration(declaration);
|
|
189
|
+
if (dynamic) {
|
|
190
|
+
return {
|
|
191
|
+
ok: false,
|
|
192
|
+
failure: { reason: "dynamic", at: `${at}.${key} → ${dynamic.path}`, what: dynamic.what },
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
out[key] = declaration;
|
|
196
|
+
}
|
|
197
|
+
return { ok: true, value: out };
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Binds `peers` and `entry`, resolving each referrer's collection once.
|
|
201
|
+
*
|
|
202
|
+
* One binder per analysis: the cache is what keeps a rule over an n-entry
|
|
203
|
+
* collection linear in resolution work rather than quadratic, and it is shared
|
|
204
|
+
* by the evaluation and the exercised check, which ask the same question.
|
|
205
|
+
*/
|
|
206
|
+
export class PeerBinder {
|
|
207
|
+
env;
|
|
208
|
+
constructor(env) {
|
|
209
|
+
this.env = env;
|
|
210
|
+
}
|
|
211
|
+
collections = new WeakMap();
|
|
212
|
+
/**
|
|
213
|
+
* @param slotPath concrete path of the edge that reached the referenced
|
|
214
|
+
* resource, e.g. `tables[2]` or `mounts[1].mount`.
|
|
215
|
+
*/
|
|
216
|
+
bind(referrer, referrerKind, pointer, slotPath) {
|
|
217
|
+
const collectionPath = pointerToPath(pointer);
|
|
218
|
+
const shapes = this.env.refSlotsOf(referrerKind);
|
|
219
|
+
if (!shapes)
|
|
220
|
+
return { ok: false, failure: { reason: "unknown-shape", at: collectionPath } };
|
|
221
|
+
const resolved = this.collection(referrer, pointer, collectionPath, shapes);
|
|
222
|
+
if (!resolved.ok)
|
|
223
|
+
return resolved;
|
|
224
|
+
const mine = resolved.entryShape
|
|
225
|
+
? this.entryKey(slotPath, collectionPath, resolved.entryShape)
|
|
226
|
+
: undefined;
|
|
227
|
+
const at = mine === undefined ? -1 : resolved.keys.indexOf(mine);
|
|
228
|
+
const peers = at === -1 ? resolved.values : resolved.values.filter((_, i) => i !== at);
|
|
229
|
+
if (at !== -1)
|
|
230
|
+
return { ok: true, binding: { peers, entry: resolved.values[at] } };
|
|
231
|
+
// The edge runs through a DIFFERENT collection than the one `peers:` names
|
|
232
|
+
// (a rule over a schema's `enums:` while my own entry sits in `tables:`), so
|
|
233
|
+
// the entry is found through the shape this slot path matches.
|
|
234
|
+
const shape = shapes.find((candidate) => shapeMatches(slotPath, candidate));
|
|
235
|
+
if (!shape)
|
|
236
|
+
return { ok: false, failure: { reason: "unknown-shape", at: slotPath } };
|
|
237
|
+
const boundary = entryBoundary(slotPath, shape);
|
|
238
|
+
const entryShape = entryBoundary(shape, shape);
|
|
239
|
+
const entry = resolveEntry(navigatePath(referrer, boundary), boundary, entryRefsOf(shapes, entryShape), this.env.declarationOf);
|
|
240
|
+
if (!entry.ok)
|
|
241
|
+
return entry;
|
|
242
|
+
return { ok: true, binding: { peers, entry: entry.value } };
|
|
243
|
+
}
|
|
244
|
+
/** True when the rule has something to compare — the input to the
|
|
245
|
+
* never-exercised report, asked through the same cache. */
|
|
246
|
+
hasPeers(referrer, referrerKind, pointer, slotPath) {
|
|
247
|
+
const bound = this.bind(referrer, referrerKind, pointer, slotPath);
|
|
248
|
+
return bound.ok && bound.binding.peers.length > 0;
|
|
249
|
+
}
|
|
250
|
+
collection(referrer, pointer, collectionPath, shapes) {
|
|
251
|
+
let byPointer = this.collections.get(referrer);
|
|
252
|
+
if (!byPointer) {
|
|
253
|
+
byPointer = new Map();
|
|
254
|
+
this.collections.set(referrer, byPointer);
|
|
255
|
+
}
|
|
256
|
+
const cached = byPointer.get(pointer);
|
|
257
|
+
if (cached)
|
|
258
|
+
return cached;
|
|
259
|
+
const resolved = this.resolveCollection(referrer, pointer, collectionPath, shapes);
|
|
260
|
+
byPointer.set(pointer, resolved);
|
|
261
|
+
return resolved;
|
|
262
|
+
}
|
|
263
|
+
resolveCollection(referrer, pointer, collectionPath, shapes) {
|
|
264
|
+
const raw = resolvePointer(referrer, pointer);
|
|
265
|
+
// An ABSENT collection is an EMPTY one, not an unbindable one — the line
|
|
266
|
+
// `resolveRuleSubjects` already draws for a resource rule's `in:`. A resource
|
|
267
|
+
// that simply declares none is the loudest case a peer rule has (a column
|
|
268
|
+
// naming an enum its schema lists nowhere), so degrading it to a skip would
|
|
269
|
+
// silence exactly the manifest the rule exists for.
|
|
270
|
+
if (raw === undefined || raw === null)
|
|
271
|
+
return { ok: true, keys: [], values: [] };
|
|
272
|
+
if (!Array.isArray(raw) && !isObject(raw)) {
|
|
273
|
+
return { ok: false, failure: { reason: "no-collection", at: collectionPath } };
|
|
274
|
+
}
|
|
275
|
+
const entryShape = entryShapeOf(shapes, collectionPath, Array.isArray(raw));
|
|
276
|
+
if (!entryShape) {
|
|
277
|
+
return { ok: false, failure: { reason: "unknown-shape", at: collectionPath } };
|
|
278
|
+
}
|
|
279
|
+
const refs = entryRefsOf(shapes, entryShape);
|
|
280
|
+
const keys = Array.isArray(raw) ? raw.map((_, i) => String(i)) : Object.keys(raw);
|
|
281
|
+
const items = Array.isArray(raw) ? raw : Object.values(raw);
|
|
282
|
+
const values = [];
|
|
283
|
+
for (let i = 0; i < items.length; i++) {
|
|
284
|
+
const at = Array.isArray(raw) ? `${collectionPath}[${keys[i]}]` : `${collectionPath}.${keys[i]}`;
|
|
285
|
+
const resolved = resolveEntry(items[i], at, refs, this.env.declarationOf);
|
|
286
|
+
if (!resolved.ok)
|
|
287
|
+
return resolved;
|
|
288
|
+
values.push(resolved.value);
|
|
289
|
+
}
|
|
290
|
+
return { ok: true, keys, values, entryShape };
|
|
291
|
+
}
|
|
292
|
+
/** The array index or map key `slotPath` occupies in the peers collection, or
|
|
293
|
+
* `undefined` when the path does not run through it at all. */
|
|
294
|
+
entryKey(slotPath, collectionPath, entryShape) {
|
|
295
|
+
if (entryShape.endsWith("[]")) {
|
|
296
|
+
if (!slotPath.startsWith(`${collectionPath}[`))
|
|
297
|
+
return undefined;
|
|
298
|
+
const match = /^\[(\d+)\]/.exec(slotPath.slice(collectionPath.length));
|
|
299
|
+
return match ? match[1] : undefined;
|
|
300
|
+
}
|
|
301
|
+
if (!slotPath.startsWith(`${collectionPath}.`))
|
|
302
|
+
return undefined;
|
|
303
|
+
const rest = slotPath.slice(collectionPath.length + 1);
|
|
304
|
+
const dot = rest.indexOf(".");
|
|
305
|
+
return dot === -1 ? rest : rest.slice(0, dot);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* The binder for one analysis run.
|
|
310
|
+
*
|
|
311
|
+
* ONE per run: it caches each referrer's resolved collection, which is what
|
|
312
|
+
* keeps a rule over an n-entry collection from re-resolving that collection once
|
|
313
|
+
* per entry — and both the evaluation and the exercised check ask for it.
|
|
314
|
+
*
|
|
315
|
+
* Built here rather than assembled at the analysis site, the
|
|
316
|
+
* `analyzerContractScope` precedent: both halves are the binding's own rule
|
|
317
|
+
* expressed against the analyzer's registry, and inline they were 40 lines of a
|
|
318
|
+
* 2700-line pass, using two different field-map accessors for one question.
|
|
319
|
+
*/
|
|
320
|
+
export function analyzerPeerBinder(registry, aliases, manifests) {
|
|
321
|
+
const byName = new Map();
|
|
322
|
+
const byModuleAndName = new Map();
|
|
323
|
+
for (const m of manifests) {
|
|
324
|
+
const name = m.metadata?.name;
|
|
325
|
+
if (!name)
|
|
326
|
+
continue;
|
|
327
|
+
byName.set(name, m);
|
|
328
|
+
const mod = m.metadata.module;
|
|
329
|
+
if (mod)
|
|
330
|
+
byModuleAndName.set(`${mod}\0${name}`, m);
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* An alias RESOLVES OR NOTHING DOES. Resource names are module-scoped, so two
|
|
334
|
+
* libraries each exporting a `users` share one bucket in `byName`; falling
|
|
335
|
+
* back to it when the alias names a module this analysis cannot resolve would
|
|
336
|
+
* compare against a declaration from somewhere else entirely — a confident
|
|
337
|
+
* wrong verdict, which is worse than the reported skip an absent binding
|
|
338
|
+
* produces.
|
|
339
|
+
*/
|
|
340
|
+
const declarationOf = (ref) => {
|
|
341
|
+
if (ref.alias && ref.alias !== "Self") {
|
|
342
|
+
const module = aliases.moduleForAlias(ref.alias);
|
|
343
|
+
return module ? byModuleAndName.get(`${module}\0${ref.name}`) : undefined;
|
|
344
|
+
}
|
|
345
|
+
return byName.get(ref.name);
|
|
346
|
+
};
|
|
347
|
+
/**
|
|
348
|
+
* Which paths of a referrer kind hold references — the field map, which is the
|
|
349
|
+
* authority. A binder that sniffed for `{kind, name}` instead would resolve
|
|
350
|
+
* author data carrying those two keys to an unrelated manifest.
|
|
351
|
+
*/
|
|
352
|
+
const refSlotsOf = (kind) => {
|
|
353
|
+
const map = registry.getFieldMapForKind(kind, aliases);
|
|
354
|
+
if (!map)
|
|
355
|
+
return undefined;
|
|
356
|
+
return [...map].filter(([, entry]) => isRefEntry(entry)).map(([path]) => path);
|
|
357
|
+
};
|
|
358
|
+
return new PeerBinder({ declarationOf, refSlotsOf });
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* The strict half's resolver for a `peers:` pointer.
|
|
362
|
+
*
|
|
363
|
+
* Liskov in BOTH directions, which is what the check has to be: the filter is
|
|
364
|
+
* usually an abstract (`Sql.Schema`, so one rule serves every backend) while the
|
|
365
|
+
* collection is declared by the backends that implement it, so a pointer
|
|
366
|
+
* resolving on any candidate resolves the rule. Reported only when NO candidate
|
|
367
|
+
* declares it, or when every candidate that does holds plain data — the two
|
|
368
|
+
* shapes where the rule would see no declaration at all.
|
|
369
|
+
*
|
|
370
|
+
* Takes only the registry: it is answered after the registration loop and before
|
|
371
|
+
* the manifest list exists, which is also why it is a separate function rather
|
|
372
|
+
* than a method on one environment object.
|
|
373
|
+
*/
|
|
374
|
+
export function analyzerPeersTarget(registry) {
|
|
375
|
+
return (referrerKind, pointer) => {
|
|
376
|
+
const implementations = registry
|
|
377
|
+
.getByExtends(referrerKind)
|
|
378
|
+
.map((d) => d.metadata.module ? `${d.metadata.module}.${d.metadata.name}` : String(d.metadata.name));
|
|
379
|
+
const candidates = [referrerKind, ...implementations];
|
|
380
|
+
const path = pointerToPath(pointer);
|
|
381
|
+
let sawSchema = false;
|
|
382
|
+
let sawCollection = false;
|
|
383
|
+
for (const kind of candidates) {
|
|
384
|
+
const schema = registry.effectiveSchema(kind);
|
|
385
|
+
if (!schema)
|
|
386
|
+
continue;
|
|
387
|
+
sawSchema = true;
|
|
388
|
+
const node = schemaAtPointer(schema, pointer);
|
|
389
|
+
if (node === undefined || !isIterableSchema(node))
|
|
390
|
+
continue;
|
|
391
|
+
sawCollection = true;
|
|
392
|
+
const map = registry.getFieldMap(kind);
|
|
393
|
+
if (!map)
|
|
394
|
+
continue;
|
|
395
|
+
for (const [fieldPath, entry] of map) {
|
|
396
|
+
if (!isRefEntry(entry))
|
|
397
|
+
continue;
|
|
398
|
+
if (fieldPath === `${path}[]` ||
|
|
399
|
+
fieldPath.startsWith(`${path}[].`) ||
|
|
400
|
+
fieldPath === `${path}.{}` ||
|
|
401
|
+
fieldPath.startsWith(`${path}.{}.`)) {
|
|
402
|
+
return "ok";
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
if (!sawSchema)
|
|
407
|
+
return "unknown";
|
|
408
|
+
// An ABSTRACT filter with no implementations in this analysis says nothing:
|
|
409
|
+
// the abstract declares the collection structurally and each backend
|
|
410
|
+
// restates the entry as a reference to its OWN kind, so checking a library
|
|
411
|
+
// on its own would report every such rule as pointing at plain data. The
|
|
412
|
+
// consumer's app analysis holds the backends and answers properly there.
|
|
413
|
+
const filter = registry.resolve(referrerKind);
|
|
414
|
+
if (implementations.length === 0 && filter?.kind === "Telo.Abstract")
|
|
415
|
+
return "unknown";
|
|
416
|
+
return sawCollection ? "plain" : "absent";
|
|
417
|
+
};
|
|
418
|
+
}
|
package/dist/ref-slot.d.ts
CHANGED
|
@@ -80,6 +80,19 @@ export interface RefSlot {
|
|
|
80
80
|
/** `x-telo-inline: true` on the slot or any `anyOf` branch — accepts an inline
|
|
81
81
|
* `{kind, ...config}` definition, not only a `!ref`. */
|
|
82
82
|
inline: boolean;
|
|
83
|
+
/** The slot's VALUE branches — `anyOf` / `oneOf` alternatives that carry no
|
|
84
|
+
* `x-telo-ref`, present only when the reference constraint is itself a
|
|
85
|
+
* *branch* rather than the node's own annotation.
|
|
86
|
+
*
|
|
87
|
+
* That narrowing is the whole point. A node-level `x-telo-ref` with branches
|
|
88
|
+
* beneath it (an Application `targets` entry) uses those branches to describe
|
|
89
|
+
* the POST-RESOLUTION structural forms a reference takes — a bare string
|
|
90
|
+
* there is the removed string-reference spelling, and admitting it as a value
|
|
91
|
+
* would retire `INVALID_REFERENCE_FORM` exactly where it still applies. A
|
|
92
|
+
* branch-level constraint says something different: this slot holds either a
|
|
93
|
+
* value of one shape or a reference, and a scalar is then a value, not a
|
|
94
|
+
* malformed reference. */
|
|
95
|
+
valueBranches: Record<string, any>[];
|
|
83
96
|
}
|
|
84
97
|
/** Every use a slot can take, flattening a case map. The conservative reading
|
|
85
98
|
* for a consumer that does not resolve the selector. */
|