@telorun/analyzer 0.62.1 → 0.63.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 (142) hide show
  1. package/dist/analysis-registry.d.ts.map +1 -1
  2. package/dist/analysis-registry.js +6 -0
  3. package/dist/analyzer.d.ts +1 -1
  4. package/dist/analyzer.d.ts.map +1 -1
  5. package/dist/analyzer.js +343 -13
  6. package/dist/call-graph.d.ts.map +1 -1
  7. package/dist/call-graph.js +2 -7
  8. package/dist/flatten-for-analyzer.js +1 -1
  9. package/dist/import-resolution-diagnostics.d.ts +9 -2
  10. package/dist/import-resolution-diagnostics.d.ts.map +1 -1
  11. package/dist/import-resolution-diagnostics.js +26 -7
  12. package/dist/index.d.ts +15 -1
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/index.js +13 -1
  15. package/dist/invocation-contract.d.ts +6 -1
  16. package/dist/invocation-contract.d.ts.map +1 -1
  17. package/dist/invocation-contract.js +27 -4
  18. package/dist/loaded-types.d.ts +12 -3
  19. package/dist/loaded-types.d.ts.map +1 -1
  20. package/dist/manifest-loader.d.ts +0 -1
  21. package/dist/manifest-loader.d.ts.map +1 -1
  22. package/dist/manifest-loader.js +62 -41
  23. package/dist/manifest-schemas.d.ts +699 -0
  24. package/dist/manifest-schemas.d.ts.map +1 -1
  25. package/dist/manifest-schemas.js +306 -14
  26. package/dist/module-compatibility.d.ts +40 -0
  27. package/dist/module-compatibility.d.ts.map +1 -0
  28. package/dist/module-compatibility.js +52 -0
  29. package/dist/ref-slot-reading.d.ts +30 -0
  30. package/dist/ref-slot-reading.d.ts.map +1 -0
  31. package/dist/ref-slot-reading.js +139 -0
  32. package/dist/referrer-rule.d.ts +67 -0
  33. package/dist/referrer-rule.d.ts.map +1 -0
  34. package/dist/referrer-rule.js +96 -0
  35. package/dist/release/index.d.ts +1 -1
  36. package/dist/release/index.d.ts.map +1 -1
  37. package/dist/release/index.js +1 -1
  38. package/dist/release/version-stamp.d.ts +16 -0
  39. package/dist/release/version-stamp.d.ts.map +1 -1
  40. package/dist/release/version-stamp.js +52 -0
  41. package/dist/release/workspace-config.d.ts +10 -6
  42. package/dist/release/workspace-config.d.ts.map +1 -1
  43. package/dist/release/workspace-config.js +10 -6
  44. package/dist/resolve-schema-ref-kinds.d.ts +4 -3
  45. package/dist/resolve-schema-ref-kinds.d.ts.map +1 -1
  46. package/dist/resolve-schema-ref-kinds.js +16 -0
  47. package/dist/resolve-throws-union.d.ts.map +1 -1
  48. package/dist/resolve-throws-union.js +4 -3
  49. package/dist/resolve-zone-containment.d.ts +99 -0
  50. package/dist/resolve-zone-containment.d.ts.map +1 -0
  51. package/dist/resolve-zone-containment.js +212 -0
  52. package/dist/resolve-zone-requirements.d.ts +5 -0
  53. package/dist/resolve-zone-requirements.d.ts.map +1 -1
  54. package/dist/resolve-zone-requirements.js +48 -7
  55. package/dist/resource-rule.d.ts +106 -0
  56. package/dist/resource-rule.d.ts.map +1 -0
  57. package/dist/resource-rule.js +236 -0
  58. package/dist/rule-condition.d.ts +50 -0
  59. package/dist/rule-condition.d.ts.map +1 -0
  60. package/dist/rule-condition.js +108 -0
  61. package/dist/schema-keywords.d.ts.map +1 -1
  62. package/dist/schema-keywords.js +20 -0
  63. package/dist/schema-projection.d.ts +171 -0
  64. package/dist/schema-projection.d.ts.map +1 -0
  65. package/dist/schema-projection.js +317 -0
  66. package/dist/step-slot.d.ts +39 -0
  67. package/dist/step-slot.d.ts.map +1 -0
  68. package/dist/step-slot.js +49 -0
  69. package/dist/telo-version.d.ts +1 -1
  70. package/dist/telo-version.js +1 -1
  71. package/dist/validate-cel-context.d.ts.map +1 -1
  72. package/dist/validate-cel-context.js +17 -3
  73. package/dist/validate-durable-regions.d.ts +15 -0
  74. package/dist/validate-durable-regions.d.ts.map +1 -0
  75. package/dist/validate-durable-regions.js +336 -0
  76. package/dist/validate-references.d.ts.map +1 -1
  77. package/dist/validate-references.js +44 -2
  78. package/dist/validate-referrer-rules.d.ts +120 -0
  79. package/dist/validate-referrer-rules.d.ts.map +1 -0
  80. package/dist/validate-referrer-rules.js +255 -0
  81. package/dist/validate-resource-rules.d.ts +114 -0
  82. package/dist/validate-resource-rules.d.ts.map +1 -0
  83. package/dist/validate-resource-rules.js +283 -0
  84. package/dist/validate-schema-projection.d.ts +32 -0
  85. package/dist/validate-schema-projection.d.ts.map +1 -0
  86. package/dist/validate-schema-projection.js +110 -0
  87. package/dist/validate-step-inputs.d.ts +2 -1
  88. package/dist/validate-step-inputs.d.ts.map +1 -1
  89. package/dist/validate-step-inputs.js +5 -3
  90. package/dist/validate-throws-coverage.d.ts.map +1 -1
  91. package/dist/validate-throws-coverage.js +11 -5
  92. package/dist/validate-zone-slots.d.ts +2 -2
  93. package/dist/validate-zone-slots.d.ts.map +1 -1
  94. package/dist/validate-zone-slots.js +202 -6
  95. package/dist/validate-zone-violations.d.ts +12 -0
  96. package/dist/validate-zone-violations.d.ts.map +1 -0
  97. package/dist/validate-zone-violations.js +169 -0
  98. package/dist/value-type-keyword.d.ts +1 -1
  99. package/dist/value-type-keyword.d.ts.map +1 -1
  100. package/dist/value-type-keyword.js +4 -0
  101. package/dist/zone-slot.d.ts +61 -0
  102. package/dist/zone-slot.d.ts.map +1 -1
  103. package/dist/zone-slot.js +86 -5
  104. package/package.json +2 -2
  105. package/src/analysis-registry.ts +6 -0
  106. package/src/analyzer.ts +395 -14
  107. package/src/call-graph.ts +2 -8
  108. package/src/flatten-for-analyzer.ts +1 -1
  109. package/src/import-resolution-diagnostics.ts +25 -7
  110. package/src/index.ts +73 -0
  111. package/src/invocation-contract.ts +45 -2
  112. package/src/loaded-types.ts +12 -3
  113. package/src/manifest-loader.ts +87 -55
  114. package/src/manifest-schemas.ts +328 -14
  115. package/src/module-compatibility.ts +72 -0
  116. package/src/ref-slot-reading.ts +156 -0
  117. package/src/referrer-rule.ts +116 -0
  118. package/src/release/index.ts +1 -0
  119. package/src/release/version-stamp.ts +58 -0
  120. package/src/release/workspace-config.ts +10 -6
  121. package/src/resolve-schema-ref-kinds.ts +28 -4
  122. package/src/resolve-throws-union.ts +4 -3
  123. package/src/resolve-zone-containment.ts +329 -0
  124. package/src/resolve-zone-requirements.ts +67 -7
  125. package/src/resource-rule.ts +252 -0
  126. package/src/rule-condition.ts +126 -0
  127. package/src/schema-keywords.ts +24 -0
  128. package/src/schema-projection.ts +403 -0
  129. package/src/step-slot.ts +64 -0
  130. package/src/telo-version.ts +1 -1
  131. package/src/validate-cel-context.ts +17 -3
  132. package/src/validate-durable-regions.ts +401 -0
  133. package/src/validate-references.ts +57 -2
  134. package/src/validate-referrer-rules.ts +373 -0
  135. package/src/validate-resource-rules.ts +401 -0
  136. package/src/validate-schema-projection.ts +184 -0
  137. package/src/validate-step-inputs.ts +5 -3
  138. package/src/validate-throws-coverage.ts +12 -5
  139. package/src/validate-zone-slots.ts +192 -8
  140. package/src/validate-zone-violations.ts +190 -0
  141. package/src/value-type-keyword.ts +4 -0
  142. package/src/zone-slot.ts +114 -5
@@ -0,0 +1,336 @@
1
+ import { isCompiledValue, VALUE_TYPES } from "@telorun/sdk";
2
+ import { auditCalls, buildCelEnvironment, isTaggedSentinel, CEL_ENGINE } from "@telorun/templating";
3
+ import { DiagnosticSeverity } from "./types.js";
4
+ import { findZoneRegions, } from "./resolve-zone-containment.js";
5
+ const SOURCE = "telo";
6
+ /**
7
+ * The impure functions one CEL source actually CALLS, in source order.
8
+ *
9
+ * Parsed, never text-matched. A regex over the source is wrong in both
10
+ * directions and in exactly the ways this repo has already retired elsewhere:
11
+ * it fires on a function name inside a string literal, on an unrelated receiver
12
+ * method that happens to share a name, and on text inside an interpolation it
13
+ * has no business reading — while `analyze` already returns every call site the
14
+ * parser found, each carrying the `deterministic` flag resolved from the
15
+ * registry. One expression, one verdict, and it is the engine's.
16
+ *
17
+ * `deterministic === false` specifically: `undefined` means the name resolved to
18
+ * nothing, or to a function carrying no determinism metadata, and absent is not
19
+ * "impure" any more than it is "pure" — a name the registry cannot account for
20
+ * is `CEL_UNKNOWN_FUNCTION`'s to report, not this rule's to guess at.
21
+ */
22
+ function impureCalls(source) {
23
+ let ast;
24
+ try {
25
+ ast = CEL_ENV.parse(source).ast;
26
+ }
27
+ catch {
28
+ // Unparseable CEL is `CEL_SYNTAX_ERROR`'s to report, and it will be, from
29
+ // the pass that owns the expression. Reporting nothing here is right:
30
+ // guessing at the calls in text that does not parse is exactly the
31
+ // text-matching this replaced.
32
+ return [];
33
+ }
34
+ const names = [];
35
+ for (const call of auditCalls(source, ast, CEL_ENV).calls) {
36
+ if (call.deterministic === false && !names.includes(call.name))
37
+ names.push(call.name);
38
+ }
39
+ return names;
40
+ }
41
+ /** One environment for the whole pass: it carries the function registry, which
42
+ * is what resolves a call's determinism, and nothing manifest-specific — the
43
+ * typed scope belongs to the CEL validation pass, which asks a different
44
+ * question of the same expressions. */
45
+ const CEL_ENV = buildCelEnvironment();
46
+ /** Every CEL source string reachable in a value tree, with the path that holds
47
+ * it. Stops at nested `{ kind }` declarations, which belong to another
48
+ * resource. */
49
+ function celSources(value, path, out) {
50
+ // A raw `${{ }}` string, which is what a consumer holding an unprecompiled
51
+ // manifest sees. Each interpolation is pushed SEPARATELY: the surrounding
52
+ // literal text is not CEL, and handing the whole string to a CEL parser
53
+ // fails — which would silently report no calls at all for every expression
54
+ // written this way.
55
+ if (typeof value === "string") {
56
+ for (const expression of interpolatedExpressions(value))
57
+ out.push([path, expression]);
58
+ return;
59
+ }
60
+ if (!value || typeof value !== "object")
61
+ return;
62
+ // By the time this pass runs the loader has PRECOMPILED every CEL slot, so the
63
+ // common shape is a CompiledValue rather than a string or a tag sentinel.
64
+ // Reading only the authored spellings is why this check was silent on every
65
+ // manifest that reached it — the one shape it never met was the one it always
66
+ // gets. An interpolated string keeps its expressions in `parts`, so those are
67
+ // descended into rather than read off the joined source.
68
+ if (isCompiledValue(value)) {
69
+ if (typeof value.source === "string")
70
+ out.push([path, value.source]);
71
+ for (const [i, part] of (value.parts ?? []).entries()) {
72
+ if (typeof part !== "string")
73
+ celSources(part, `${path}[${i}]`, out);
74
+ }
75
+ return;
76
+ }
77
+ // An unprecompiled `!cel` tag — what a round-trip consumer (the editor) holds,
78
+ // and what reaches this pass whenever precompilation was not requested. Read
79
+ // through the templating predicates rather than by testing a marker key: the
80
+ // sentinel's shape is that package's, and spelling it here is a second place
81
+ // it would have to be kept right.
82
+ if (isTaggedSentinel(value) && value.engine === CEL_ENGINE) {
83
+ out.push([path, value.source]);
84
+ return;
85
+ }
86
+ if (Array.isArray(value)) {
87
+ value.forEach((item, i) => celSources(item, `${path}[${i}]`, out));
88
+ return;
89
+ }
90
+ // STOP at a nested resource declaration. Skipping the `kind` key alone was not
91
+ // that: it walked straight into the declaration's own configuration and
92
+ // reported its CEL against the enclosing resource, which is both the wrong
93
+ // anchor and a claim about a region that expression is not in. The boundary is
94
+ // the presence of `kind`, the same line Phase-5 injection and the include walk
95
+ // already draw.
96
+ const record = value;
97
+ if (path && typeof record.kind === "string")
98
+ return;
99
+ for (const [key, child] of Object.entries(record)) {
100
+ if (key === "kind" || key === "metadata")
101
+ continue;
102
+ celSources(child, path ? `${path}.${key}` : key, out);
103
+ }
104
+ }
105
+ /** The expressions inside a `${{ … }}` interpolated string, unwrapped. Balanced
106
+ * on `}}` rather than on the first `}`, so an expression containing a map
107
+ * literal is not cut in half. */
108
+ function interpolatedExpressions(text) {
109
+ const out = [];
110
+ let at = 0;
111
+ for (;;) {
112
+ const open = text.indexOf("${{", at);
113
+ if (open < 0)
114
+ return out;
115
+ const close = text.indexOf("}}", open + 3);
116
+ if (close < 0)
117
+ return out;
118
+ const expression = text.slice(open + 3, close).trim();
119
+ if (expression)
120
+ out.push(expression);
121
+ at = close + 2;
122
+ }
123
+ }
124
+ /**
125
+ * `DURABLE_NONDETERMINISM` — impure CEL inside an `idempotent` region.
126
+ *
127
+ * **It keys on `idempotent`, and only there does it say something true.** Impure
128
+ * CEL in a *journaled* position is not a defect at all — it is the correct
129
+ * semantic: `now()` in a step's inputs is recorded on first execution and
130
+ * replayed identically, which is exactly what a durable timestamp should do.
131
+ *
132
+ * What makes the idempotent case different is that such a region re-runs on
133
+ * resume with its prior effects INTACT, because nothing discarded them — the
134
+ * region's whole claim is that re-running is a no-op. Impure CEL falsifies
135
+ * exactly that claim: `uuid()` as a key writes record A on the first pass and
136
+ * record B on the second, so the re-run is not a no-op and the assertion the
137
+ * author signed is false. The diagnostic can therefore say precisely that.
138
+ *
139
+ * **`atomic` is the wrong trigger, in both directions.** Too wide: collapse
140
+ * there is conditional on a runtime attestation, so a transaction sharing the
141
+ * journal's own transaction is NOT collapsed and its decisions genuinely are
142
+ * journaled — a static trigger would fire on the configuration this design
143
+ * recommends. Too narrow: an atomic zone that does re-run is a RETRY, not a
144
+ * replay — its effects were discarded, so a fresh timestamp is simply a fresh
145
+ * attempt.
146
+ */
147
+ function checkNondeterminism(region, graph, reportModules, diagnostics) {
148
+ for (const { node } of region.contents.values()) {
149
+ const [manifest, base, prefix] = celScopeOf(node, graph);
150
+ if (!manifest || !owned(manifest, reportModules))
151
+ continue;
152
+ const found = [];
153
+ celSources(base, prefix, found);
154
+ for (const [path, source] of found) {
155
+ for (const fn of impureCalls(source)) {
156
+ diagnostics.push({
157
+ severity: DiagnosticSeverity.Error,
158
+ code: "DURABLE_NONDETERMINISM",
159
+ source: SOURCE,
160
+ message: `'${fn}()' is evaluated inside a region declared idempotent by ` +
161
+ `${region.provider.kind} '${region.provider.name}' — "${region.reason}". A ` +
162
+ `region with that claim re-runs on a resume with its earlier effects intact, ` +
163
+ `so an expression that produces a different value each time makes the ` +
164
+ `re-run something other than a no-op and the claim false. Pin the value ` +
165
+ `once for the region instead of computing it per pass.`,
166
+ data: {
167
+ resource: { kind: manifest.kind, name: manifest.metadata?.name },
168
+ filePath: manifest.metadata?.source,
169
+ path,
170
+ },
171
+ });
172
+ // One diagnostic per expression: naming every impure call in a single
173
+ // expression would repeat one fix several times.
174
+ break;
175
+ }
176
+ }
177
+ }
178
+ }
179
+ /**
180
+ * `DURABLE_DETACH_FORBIDDEN` — a detached dispatch inside a `replayed` region.
181
+ *
182
+ * Journal-on-completion would record the DISPATCH as done while the work runs
183
+ * on, so a resume skips it and a crash loses it — durability's exact inverse.
184
+ *
185
+ * The replacement is better than what it forbids, which is why this is an error
186
+ * rather than a warning: what an author wants there is a nested durable run
187
+ * started without awaiting, so the step's outcome is a journalable run id, the
188
+ * child gets its own identity and its own durability, and nothing is lost on
189
+ * either side. That is what Temporal's child workflows and Restate's one-way
190
+ * send already are.
191
+ *
192
+ * It keys off the dispatch BEING detached rather than off any kind, so it covers
193
+ * a detaching decorator's configured mode as readily as an explicit detach.
194
+ */
195
+ function checkDetach(region, reportModules, diagnostics) {
196
+ for (const boundary of region.boundaries) {
197
+ if (!boundary.escaping.includes("detached"))
198
+ continue;
199
+ if (!owned(boundary.from.manifest, reportModules))
200
+ continue;
201
+ diagnostics.push({
202
+ severity: DiagnosticSeverity.Error,
203
+ code: "DURABLE_DETACH_FORBIDDEN",
204
+ source: SOURCE,
205
+ message: `${boundary.from.kind} '${boundary.from.name}' dispatches '${boundary.edge.slot}' ` +
206
+ `detached inside a durable region (${region.provider.kind} ` +
207
+ `'${region.provider.name}'). Progress is recorded when a step COMPLETES, so a ` +
208
+ `detached dispatch would be recorded as done while its work was still running — ` +
209
+ `a resume would skip it and a crash would lose it, which is the opposite of what ` +
210
+ `durability provides. Start a nested durable run instead and let the step record ` +
211
+ `its run id: the work keeps its own identity and its own recovery, and nothing is ` +
212
+ `awaited.`,
213
+ data: {
214
+ resource: { kind: boundary.from.kind, name: boundary.from.name },
215
+ filePath: boundary.from.manifest.metadata?.source,
216
+ path: boundary.edge.path,
217
+ },
218
+ });
219
+ }
220
+ }
221
+ /**
222
+ * `DURABLE_UNJOURNALABLE_RESULT` — a live value in a journaled position.
223
+ *
224
+ * A live handle is consumed by reading, so it exists exactly once and a
225
+ * recording of it is a recording of nothing. The rule keys off the `live` field
226
+ * of the value-type vocabulary rather than off a type NAME, for the reason that
227
+ * vocabulary is data: a live type added later is covered by its entry alone, and
228
+ * the analyzer names a representation rather than a type.
229
+ *
230
+ * A WARNING rather than an error, and the runtime is the gate: what a step
231
+ * actually produces is only as knowable as its declared contract, and a kind
232
+ * that declares none falls back to a permissive shape that proves nothing. The
233
+ * runtime raises `ERR_DURABLE_UNJOURNALABLE_VALUE` at the step path that
234
+ * produced the value, which is as actionable and is true regardless of what was
235
+ * declared.
236
+ */
237
+ function checkUnjournalableResults(region, resolveDef, graph, reportModules, diagnostics) {
238
+ for (const { node } of region.contents.values()) {
239
+ if (node.type !== "step")
240
+ continue;
241
+ for (const edge of graph.edgesFrom(node.id)) {
242
+ const target = edge.to ? graph.nodes.get(edge.to) : undefined;
243
+ if (!target || target.type !== "resource")
244
+ continue;
245
+ const def = resolveDef(target.kind, target.manifest.metadata?.module);
246
+ const declared = target.manifest.outputType ??
247
+ def?.outputType;
248
+ if (!isLiveSchema(declared))
249
+ continue;
250
+ const owner = graph.nodes.get(node.owner);
251
+ const manifest = owner?.type === "resource" ? owner.manifest : target.manifest;
252
+ if (!owned(manifest, reportModules))
253
+ continue;
254
+ diagnostics.push({
255
+ severity: DiagnosticSeverity.Warning,
256
+ code: "DURABLE_UNJOURNALABLE_RESULT",
257
+ source: SOURCE,
258
+ message: `Step '${node.name ?? node.path}' invokes ` +
259
+ `${target.kind} '${target.name}', whose declared output is a live value, inside a ` +
260
+ `durable region (${region.provider.kind} '${region.provider.name}'). A live handle ` +
261
+ `is produced by consuming it, so it cannot be recorded and replayed — the run will ` +
262
+ `fail at this step with ERR_DURABLE_UNJOURNALABLE_VALUE. Collect what you need ` +
263
+ `from it into a plain value inside the step, or move the streaming work outside ` +
264
+ `the durable body.`,
265
+ data: {
266
+ resource: { kind: manifest.kind, name: manifest.metadata?.name },
267
+ filePath: manifest.metadata?.source,
268
+ path: node.path,
269
+ },
270
+ });
271
+ }
272
+ }
273
+ }
274
+ /** Is this declared type a live-representation value? Reads the annotation the
275
+ * vocabulary defines rather than testing against a type name. */
276
+ function isLiveSchema(schema) {
277
+ if (!schema || typeof schema !== "object")
278
+ return false;
279
+ const annotation = schema["x-telo-type"];
280
+ const name = typeof annotation === "string"
281
+ ? annotation
282
+ : annotation?.name;
283
+ if (!name) {
284
+ const inner = schema.schema;
285
+ return inner !== undefined && inner !== schema ? isLiveSchema(inner) : false;
286
+ }
287
+ // The one place a name is compared, and it is compared against the LIVE flag
288
+ // of the vocabulary entry rather than against a literal — so this stays true
289
+ // when a second live type is declared.
290
+ return LIVE_TYPE_NAMES.has(name);
291
+ }
292
+ /** Names of the value types whose representation is live, derived from the
293
+ * vocabulary itself. A constant rather than one-shot mutable state: the
294
+ * vocabulary is fixed at module load, so there is nothing to initialize and
295
+ * nothing an initialization order could get wrong. */
296
+ const LIVE_TYPE_NAMES = new Set([...VALUE_TYPES.values()].filter((e) => e.live).map((e) => e.name));
297
+ /**
298
+ * Where a node's CEL lives, and which manifest a diagnostic about it anchors on.
299
+ *
300
+ * A STEP is where most of a body's CEL actually is — a step's `inputs`, its
301
+ * `when`, a branch predicate — and a step node carries no manifest of its own,
302
+ * so it anchors on the resource whose body declares it. Reading only resource
303
+ * nodes would have made this check silent for exactly the position it exists to
304
+ * cover.
305
+ */
306
+ function celScopeOf(node, graph) {
307
+ if (node.type === "resource")
308
+ return [node.manifest, node.manifest, ""];
309
+ const owner = graph.nodes.get(node.owner);
310
+ const manifest = owner?.type === "resource" ? owner.manifest : undefined;
311
+ return [manifest, node.step, node.path];
312
+ }
313
+ /**
314
+ * Is this manifest the entry's to fix?
315
+ *
316
+ * Scoped by the manifest's own DECLARING MODULE, not by its file path: a
317
+ * published dependency's body is not the consumer's to fix (the
318
+ * `X_TELO_REF_UNRESOLVED` precedent), and a path is not the module — a fixture
319
+ * nested under one module's directory belongs to its own.
320
+ */
321
+ function owned(manifest, reportModules) {
322
+ const module = manifest.metadata?.module;
323
+ return !module || reportModules.size === 0 || reportModules.has(module);
324
+ }
325
+ /** Every durable-region diagnostic, over one graph. */
326
+ export function validateDurableRegions(args) {
327
+ const diagnostics = [];
328
+ for (const region of findZoneRegions(args.graph, args.resolveDef, "replayed")) {
329
+ checkDetach(region, args.reportModules, diagnostics);
330
+ checkUnjournalableResults(region, args.resolveDef, args.graph, args.reportModules, diagnostics);
331
+ }
332
+ for (const region of findZoneRegions(args.graph, args.resolveDef, "idempotent")) {
333
+ checkNondeterminism(region, args.graph, args.reportModules, diagnostics);
334
+ }
335
+ return diagnostics;
336
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"validate-references.d.ts","sourceRoot":"","sources":["../src/validate-references.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAMrD,OAAO,EAAsB,KAAK,kBAAkB,EAAE,KAAK,eAAe,EAAE,MAAM,YAAY,CAAC;AAuD/F;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,kBAAkB,CAChC,SAAS,EAAE,gBAAgB,EAAE,EAC7B,OAAO,EAAE,eAAe,GACvB,kBAAkB,EAAE,CA8ctB"}
1
+ {"version":3,"file":"validate-references.d.ts","sourceRoot":"","sources":["../src/validate-references.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAOrD,OAAO,EAAsB,KAAK,kBAAkB,EAAE,KAAK,eAAe,EAAE,MAAM,YAAY,CAAC;AAuD/F;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,kBAAkB,CAChC,SAAS,EAAE,gBAAgB,EAAE,EAC7B,OAAO,EAAE,eAAe,GACvB,kBAAkB,EAAE,CAogBtB"}
@@ -1,8 +1,9 @@
1
1
  import { isRefSentinel } from "@telorun/templating";
2
2
  import { visitManifest } from "./manifest-visitor.js";
3
3
  import { isInlineResource, resolveFieldEntries, resolveFieldValues } from "./reference-field-map.js";
4
- import { navigateJsonPointer } from "./schema-compat.js";
4
+ import { navigateJsonPointer, substituteCelFields } from "./schema-compat.js";
5
5
  import { REF_VALIDATION_SKIP_KINDS as SYSTEM_KINDS } from "./system-kinds.js";
6
+ import { resolveTypeFieldToSchema } from "./validate-cel-context.js";
6
7
  import { DiagnosticSeverity } from "./types.js";
7
8
  const SOURCE = "telo-analyzer";
8
9
  /**
@@ -392,11 +393,16 @@ export function validateReferences(resources, context) {
392
393
  const ownerScope = (owningModule ? aliasesByModule?.get(owningModule) : undefined) ?? aliases;
393
394
  const targetKind = ownerScope.resolveKind(anchorName);
394
395
  if (!targetKind) {
396
+ // Names the ALIAS, not the whole kind path: the alias is what an
397
+ // author declares, and the usual cause is the import that binds it
398
+ // having failed — which is reported on its own line.
399
+ const aliasName = anchorName.slice(0, anchorName.indexOf("."));
395
400
  diagnostics.push({
396
401
  severity: DiagnosticSeverity.Error,
397
402
  code: "SCHEMA_FROM_MISSING_PATH",
398
403
  source: SOURCE,
399
- message: `${resourceLabel}: x-telo-schema-from at '${fieldPath}' → cannot resolve alias '${anchorName}'`,
404
+ message: `${resourceLabel}: x-telo-schema-from at '${fieldPath}' → cannot resolve alias ` +
405
+ `'${aliasName}' (in '${anchorName}'). Check the import that declares it.`,
400
406
  data: { resource: resourceData, filePath, path: fieldPath },
401
407
  });
402
408
  return;
@@ -465,6 +471,42 @@ export function validateReferences(resources, context) {
465
471
  const refVal = anchorVal;
466
472
  if (typeof refVal.kind !== "string")
467
473
  continue;
474
+ // THE INSTANCE FIRST, then the kind — the layering
475
+ // `x-telo-context-ref-from` already uses, and for the same reason. A
476
+ // kind that declares one fixed shape declares it on its definition; a
477
+ // kind whose shape is per instance declares it as a FIELD, and reading
478
+ // only the definition would type every instance against nothing. A
479
+ // `Durable.Await`'s `outputType:` is exactly the second: what a
480
+ // delivery carries is a property of that await and of no other, so a
481
+ // delivery naming it must be checked against the instance's own.
482
+ const target = typeof refVal.name === "string" ? byName.get(refVal.name) : undefined;
483
+ const perInstance = target === undefined
484
+ ? undefined
485
+ : navigateJsonPointer(target, jsonPointer);
486
+ if (perInstance !== undefined) {
487
+ const instanceSchema = resolveTypeFieldToSchema(perInstance, resources);
488
+ if (instanceSchema) {
489
+ // CEL leaves become schema-shaped placeholders first. A payload is
490
+ // overwhelmingly written as expressions over the call's inputs, so
491
+ // validating it raw would report every one of them as a type
492
+ // error — the check would fire only on the literal case, which is
493
+ // the case nobody writes. What survives substitution is exactly
494
+ // what is worth reporting: a missing required field, an unknown
495
+ // property, a literal of the wrong type. The same treatment
496
+ // `x-telo-value-schema-from` documents.
497
+ const substituted = substituteCelFields(fieldValue, instanceSchema);
498
+ for (const issue of registry.validateWithRefs(substituted, instanceSchema)) {
499
+ diagnostics.push({
500
+ severity: DiagnosticSeverity.Error,
501
+ code: "DEPENDENT_SCHEMA_MISMATCH",
502
+ source: SOURCE,
503
+ message: `${resourceLabel}: '${concretePath}' does not match the schema '${refVal.name}' declares at '${jsonPointer}': ${issue}`,
504
+ data: { resource: resourceData, filePath, path: concretePath },
505
+ });
506
+ }
507
+ continue;
508
+ }
509
+ }
468
510
  const refResolvedKind = aliases.resolveKind(refVal.kind) ?? refVal.kind;
469
511
  const refDef = registry.resolve(refVal.kind) ?? registry.resolve(refResolvedKind);
470
512
  if (!refDef?.schema) {
@@ -0,0 +1,120 @@
1
+ /**
2
+ * The strict half of the `x-telo-referrer-rules` accessor split
3
+ * (`validate-resource-rules.ts` / `validate-zone-slots.ts` precedent), plus the
4
+ * evaluation pass that runs a kind's rules against the resources referencing it.
5
+ *
6
+ * Both halves live here because they fail in opposite directions and must agree
7
+ * about what a rule MEANS: a declaration the reader cannot parse is silently
8
+ * unenforced — the check reads as passing when it never ran — while a rule that
9
+ * throws would otherwise be reported against a consumer's manifest, blaming an
10
+ * author for a defect in someone else's kind.
11
+ *
12
+ * A CONSUMER of the call graph with no traversal of its own: the referrers are
13
+ * `edgesTo` on the resource's node, exactly as the zone projection consumes the
14
+ * outgoing direction. What it hands back is plain findings; the caller pushes
15
+ * them.
16
+ *
17
+ * Ownership splits by whose defect it is, and lands one hop further out than for
18
+ * a resource rule. A VIOLATION belongs to the data — but the offending data is
19
+ * the REFERRER's, so the finding names that manifest and the analyzer reports it
20
+ * when the referrer is the entry's own. A rule that throws or exhausts its
21
+ * budget is a defect in the rule, anchored on the declaring definition.
22
+ *
23
+ * Browser-safe: no Node built-ins.
24
+ */
25
+ import type { ResourceManifest } from "@telorun/sdk";
26
+ import { type ReferrerRule } from "./referrer-rule.js";
27
+ export interface ReferrerRuleIssue {
28
+ code: "REFERRER_RULE_INVALID";
29
+ manifest: ResourceManifest;
30
+ path: string;
31
+ message: string;
32
+ }
33
+ /** One resource that reaches the resource under test, and where it does so. */
34
+ export interface Referrer {
35
+ readonly manifest: ResourceManifest;
36
+ readonly kind: string;
37
+ readonly name: string;
38
+ /** Concrete path of the slot in the referrer (`mounts[1].mount`) — the anchor. */
39
+ readonly path: string;
40
+ }
41
+ /** One rule's verdict on one referrer. */
42
+ export type ReferrerRuleFinding = {
43
+ kind: "violation";
44
+ rule: ReferrerRule;
45
+ referrer: Referrer;
46
+ message: string;
47
+ } | {
48
+ kind: "skipped";
49
+ rule: ReferrerRule;
50
+ referrer: Referrer;
51
+ dynamicAt: string;
52
+ } | {
53
+ kind: "failed";
54
+ rule: ReferrerRule;
55
+ referrer?: Referrer;
56
+ reason: string;
57
+ } | {
58
+ kind: "over-budget";
59
+ rule: ReferrerRule;
60
+ referrer: Referrer;
61
+ elapsedMs: number;
62
+ };
63
+ /**
64
+ * Report every way a kind's referrer-rule declarations are malformed. Runs on
65
+ * the `Telo.Definition` / `Telo.Abstract` doc, so a defect lands on the line the
66
+ * kind's author wrote rather than on a consumer's resource.
67
+ *
68
+ * The `referrer:` filter is NOT resolved here: it is canonicalized in the
69
+ * declaring module's scope by `resolveSchemaRefKinds`, the only pass holding
70
+ * that scope, and a name resolving to nothing is reported from there.
71
+ */
72
+ export declare function validateReferrerRuleDeclarations(manifest: ResourceManifest): ReferrerRuleIssue[];
73
+ /**
74
+ * Run a kind's referrer rules against every resource that references one of its
75
+ * resources.
76
+ *
77
+ * `self` binds the referenced resource, `referrer` the one that reached it. A
78
+ * referrer reaching the same resource through several slots is judged ONCE — by
79
+ * manifest identity, since a name alone is module-scoped — because the condition
80
+ * reads the two manifests and nothing about the site, so a second site could only
81
+ * produce the identical verdict at a different path.
82
+ */
83
+ export declare function evaluateReferrerRules(manifest: ResourceManifest, definitionSchema: unknown, referrers: readonly Referrer[],
84
+ /** Whether a referrer of `kind` satisfies a rule's `referrer:` filter —
85
+ * Liskov-substitutable, so a child of the named kind matches. Supplied by the
86
+ * caller, which holds the definition registry. */
87
+ kindMatches: (filter: string, kind: string) => boolean): ReferrerRuleFinding[];
88
+ /** Whether a rule found any referrer to judge — the input to the never-exercised
89
+ * report, which is how a mistyped `referrer:` filter would otherwise disable a
90
+ * check in silence. */
91
+ export declare function referrerRuleExercised(rule: ReferrerRule, referrers: readonly Referrer[], kindMatches: (filter: string, kind: string) => boolean): boolean;
92
+ /** Where a referrer-rule finding is reported, and how loudly. Plain data, so the
93
+ * caller pushes it exactly as it does for the resource-rule reports. */
94
+ export interface ReferrerRuleDiagnostic {
95
+ code: "REFERRER_RULE_VIOLATED" | "REFERRER_RULE_SKIPPED" | "REFERRER_RULE_INVALID" | "REFERRER_RULE_UNEXERCISED";
96
+ severity: "error" | "warning" | "information";
97
+ message: string;
98
+ /** The resource the finding is reported ON — the REFERRER for a violation,
99
+ * the declaring definition for a defect in the rule itself. */
100
+ manifest: ResourceManifest;
101
+ path?: string;
102
+ rule: string;
103
+ }
104
+ /**
105
+ * Map one resource's findings to what should be reported.
106
+ *
107
+ * A violation names the declaring kind in the message. For a resource rule the
108
+ * reported resource IS of the kind that declared it, so the origin is implicit;
109
+ * here it is not — the diagnostic lands on an `Http.Server` for a rule
110
+ * `HttpServer.Reference` wrote — and leaving that to the author's prose would
111
+ * make the trail depend on remembering to write it.
112
+ */
113
+ export declare function reportReferrerRules(manifest: ResourceManifest, definition: ResourceManifest | undefined, findings: readonly ReferrerRuleFinding[],
114
+ /** Whether the DECLARING definition is one of the entry's own modules. */
115
+ declarationIsOurs: boolean): ReferrerRuleDiagnostic[];
116
+ /** The report for a rule nothing ever exercised — here that means no resource of
117
+ * the kind was referenced by anything the filter matches, which is what a typo
118
+ * in `referrer:` looks like from the outside. */
119
+ export declare function reportUnexercisedReferrerRule(definition: ResourceManifest, rule: ReferrerRule): ReferrerRuleDiagnostic;
120
+ //# sourceMappingURL=validate-referrer-rules.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validate-referrer-rules.d.ts","sourceRoot":"","sources":["../src/validate-referrer-rules.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AACrD,OAAO,EAIL,KAAK,YAAY,EAClB,MAAM,oBAAoB,CAAC;AAI5B,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,uBAAuB,CAAC;IAC9B,QAAQ,EAAE,gBAAgB,CAAC;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,+EAA+E;AAC/E,MAAM,WAAW,QAAQ;IACvB,QAAQ,CAAC,QAAQ,EAAE,gBAAgB,CAAC;IACpC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,kFAAkF;IAClF,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAED,0CAA0C;AAC1C,MAAM,MAAM,mBAAmB,GAC3B;IAAE,IAAI,EAAE,WAAW,CAAC;IAAC,IAAI,EAAE,YAAY,CAAC;IAAC,QAAQ,EAAE,QAAQ,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GAC9E;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,IAAI,EAAE,YAAY,CAAC;IAAC,QAAQ,EAAE,QAAQ,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,GAC9E;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,IAAI,EAAE,YAAY,CAAC;IAAC,QAAQ,CAAC,EAAE,QAAQ,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAC3E;IAAE,IAAI,EAAE,aAAa,CAAC;IAAC,IAAI,EAAE,YAAY,CAAC;IAAC,QAAQ,EAAE,QAAQ,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,CAAC;AAMvF;;;;;;;;GAQG;AACH,wBAAgB,gCAAgC,CAAC,QAAQ,EAAE,gBAAgB,GAAG,iBAAiB,EAAE,CA2EhG;AAED;;;;;;;;;GASG;AACH,wBAAgB,qBAAqB,CACnC,QAAQ,EAAE,gBAAgB,EAC1B,gBAAgB,EAAE,OAAO,EACzB,SAAS,EAAE,SAAS,QAAQ,EAAE;AAC9B;;mDAEmD;AACnD,WAAW,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,OAAO,GACrD,mBAAmB,EAAE,CAmEvB;AAED;;wBAEwB;AACxB,wBAAgB,qBAAqB,CACnC,IAAI,EAAE,YAAY,EAClB,SAAS,EAAE,SAAS,QAAQ,EAAE,EAC9B,WAAW,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,OAAO,GACrD,OAAO,CAIT;AAED;yEACyE;AACzE,MAAM,WAAW,sBAAsB;IACrC,IAAI,EACA,wBAAwB,GACxB,uBAAuB,GACvB,uBAAuB,GACvB,2BAA2B,CAAC;IAChC,QAAQ,EAAE,OAAO,GAAG,SAAS,GAAG,aAAa,CAAC;IAC9C,OAAO,EAAE,MAAM,CAAC;IAChB;oEACgE;IAChE,QAAQ,EAAE,gBAAgB,CAAC;IAC3B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;CACd;AAKD;;;;;;;;GAQG;AACH,wBAAgB,mBAAmB,CACjC,QAAQ,EAAE,gBAAgB,EAC1B,UAAU,EAAE,gBAAgB,GAAG,SAAS,EACxC,QAAQ,EAAE,SAAS,mBAAmB,EAAE;AACxC,0EAA0E;AAC1E,iBAAiB,EAAE,OAAO,GACzB,sBAAsB,EAAE,CAmE1B;AAED;;kDAEkD;AAClD,wBAAgB,6BAA6B,CAC3C,UAAU,EAAE,gBAAgB,EAC5B,IAAI,EAAE,YAAY,GACjB,sBAAsB,CAexB"}