@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.
Files changed (100) hide show
  1. package/dist/analyzer.d.ts +1 -1
  2. package/dist/analyzer.d.ts.map +1 -1
  3. package/dist/analyzer.js +157 -63
  4. package/dist/call-graph.d.ts.map +1 -1
  5. package/dist/call-graph.js +7 -1
  6. package/dist/cel-environment.d.ts +12 -0
  7. package/dist/cel-environment.d.ts.map +1 -1
  8. package/dist/cel-environment.js +35 -19
  9. package/dist/cel-scope.d.ts.map +1 -1
  10. package/dist/cel-scope.js +24 -4
  11. package/dist/definition-registry.d.ts +24 -0
  12. package/dist/definition-registry.d.ts.map +1 -1
  13. package/dist/definition-registry.js +41 -0
  14. package/dist/eval-paths.d.ts +68 -0
  15. package/dist/eval-paths.d.ts.map +1 -1
  16. package/dist/eval-paths.js +138 -0
  17. package/dist/flatten-for-analyzer.d.ts +1 -1
  18. package/dist/flatten-for-analyzer.js +1 -1
  19. package/dist/import-resolution-diagnostics.d.ts +1 -1
  20. package/dist/import-resolution-diagnostics.js +1 -1
  21. package/dist/index.d.ts +9 -7
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +6 -5
  24. package/dist/manifest-visitor.d.ts.map +1 -1
  25. package/dist/manifest-visitor.js +8 -1
  26. package/dist/migrations/entries/index.d.ts.map +1 -1
  27. package/dist/migrations/entries/index.js +2 -0
  28. package/dist/migrations/entries/schema-prepare-bucket.json +27 -0
  29. package/dist/peer-binding.d.ts +232 -0
  30. package/dist/peer-binding.d.ts.map +1 -0
  31. package/dist/peer-binding.js +418 -0
  32. package/dist/ref-slot.d.ts +13 -0
  33. package/dist/ref-slot.d.ts.map +1 -1
  34. package/dist/ref-slot.js +26 -0
  35. package/dist/reference-field-map.d.ts +28 -0
  36. package/dist/reference-field-map.d.ts.map +1 -1
  37. package/dist/reference-field-map.js +25 -0
  38. package/dist/referrer-rule.d.ts +17 -0
  39. package/dist/referrer-rule.d.ts.map +1 -1
  40. package/dist/referrer-rule.js +4 -1
  41. package/dist/release/index.d.ts +1 -1
  42. package/dist/release/index.js +1 -1
  43. package/dist/resource-rule.d.ts +40 -27
  44. package/dist/resource-rule.d.ts.map +1 -1
  45. package/dist/resource-rule.js +61 -7
  46. package/dist/rule-condition.d.ts +11 -0
  47. package/dist/rule-condition.d.ts.map +1 -1
  48. package/dist/rule-condition.js +13 -0
  49. package/dist/schema-projection.d.ts +106 -4
  50. package/dist/schema-projection.d.ts.map +1 -1
  51. package/dist/schema-projection.js +206 -18
  52. package/dist/telo-version.d.ts +1 -1
  53. package/dist/telo-version.js +1 -1
  54. package/dist/validate-cel-context.d.ts +1 -17
  55. package/dist/validate-cel-context.d.ts.map +1 -1
  56. package/dist/validate-cel-context.js +5 -70
  57. package/dist/validate-reference-forms.d.ts +10 -0
  58. package/dist/validate-reference-forms.d.ts.map +1 -1
  59. package/dist/validate-reference-forms.js +23 -0
  60. package/dist/validate-references.d.ts.map +1 -1
  61. package/dist/validate-references.js +9 -2
  62. package/dist/validate-referrer-rules.d.ts +38 -4
  63. package/dist/validate-referrer-rules.d.ts.map +1 -1
  64. package/dist/validate-referrer-rules.js +139 -31
  65. package/dist/validate-resource-rules.d.ts +11 -2
  66. package/dist/validate-resource-rules.d.ts.map +1 -1
  67. package/dist/validate-resource-rules.js +18 -12
  68. package/dist/validate-schema-projection.d.ts.map +1 -1
  69. package/dist/validate-schema-projection.js +33 -2
  70. package/dist/with-synthetic-positions.d.ts +1 -1
  71. package/dist/with-synthetic-positions.js +1 -1
  72. package/package.json +3 -3
  73. package/src/analyzer.ts +191 -68
  74. package/src/call-graph.ts +7 -1
  75. package/src/cel-environment.ts +48 -19
  76. package/src/cel-scope.ts +24 -6
  77. package/src/definition-registry.ts +42 -0
  78. package/src/eval-paths.ts +152 -0
  79. package/src/flatten-for-analyzer.ts +1 -1
  80. package/src/import-resolution-diagnostics.ts +1 -1
  81. package/src/index.ts +45 -4
  82. package/src/manifest-visitor.ts +8 -1
  83. package/src/migrations/entries/index.ts +2 -0
  84. package/src/migrations/entries/schema-prepare-bucket.json +27 -0
  85. package/src/peer-binding.ts +641 -0
  86. package/src/ref-slot.ts +36 -0
  87. package/src/reference-field-map.ts +42 -0
  88. package/src/referrer-rule.ts +20 -1
  89. package/src/release/index.ts +1 -1
  90. package/src/resource-rule.ts +69 -7
  91. package/src/rule-condition.ts +15 -0
  92. package/src/schema-projection.ts +283 -20
  93. package/src/telo-version.ts +1 -1
  94. package/src/validate-cel-context.ts +6 -71
  95. package/src/validate-reference-forms.ts +22 -0
  96. package/src/validate-references.ts +15 -2
  97. package/src/validate-referrer-rules.ts +215 -28
  98. package/src/validate-resource-rules.ts +26 -12
  99. package/src/validate-schema-projection.ts +50 -1
  100. 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
+ }
@@ -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. */