@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,403 @@
1
+ /**
2
+ * The schema-projection annotations' single reader — the `ref-slot.ts` /
3
+ * `zone-slot.ts` precedent.
4
+ *
5
+ * A kind whose configuration is a COLLECTION OF TYPED ENTRIES can say what that
6
+ * collection means as a JSON Schema object, so a consumer can type the values
7
+ * it will read without the analyzer learning anything about the domain. A SQL
8
+ * table's columns are the first consumer; nothing in either annotation says
9
+ * SQL, column or table.
10
+ *
11
+ * Two halves, because the two facts have different owners:
12
+ *
13
+ * - `x-telo-schema-map`, on the field a projection keys on, gives the schema
14
+ * node each of its values means (`citext → {type: string}`). It sits with the
15
+ * field because that is where the value vocabulary is declared.
16
+ * - `x-telo-schema-projection`, on the KIND DOCUMENT (a sibling of `schema:`,
17
+ * not a keyword inside it), names the entry collection, the keying field, and
18
+ * the fields that MODIFY the mapped node. It sits on the document because it
19
+ * describes the kind's whole declaration rather than one field of it — but
20
+ * `schema:` is where every other `x-telo-*` keyword lives, so the reader
21
+ * accepts it in both positions and `validate-schema-projection.ts` reports the
22
+ * inner one. Silently ignoring a misplaced annotation is the exact failure the
23
+ * strict half exists to prevent: the projection stops typing its consumers and
24
+ * the diagnostic lands on the CONSUMER, blaming the wrong author.
25
+ *
26
+ * It is a declared LOOKUP, never a computed expression. The analyzer
27
+ * type-checks CEL and substitutes placeholders; it never evaluates, and a
28
+ * `base:`-style mapping is evaluated by the kernel at `create()` — too late for
29
+ * `telo check` to type the rows a consumer reads, which is the projection's
30
+ * whole purpose.
31
+ *
32
+ * Distinct from `x-telo-schema-from`, which derives a field's schema from a
33
+ * referenced KIND's definition schema. A projection is DECLARATION-derived: the
34
+ * row shape lives in one instance's own `columns:`, which no definition-level
35
+ * derivation can reach.
36
+ */
37
+
38
+ /** How a kind's entry collection projects to an object schema. */
39
+ export interface SchemaProjection {
40
+ /** JSON Pointer, from the resource root, to the entries. */
41
+ readonly entries: string;
42
+ /** The entry field whose value keys the `x-telo-schema-map` lookup. */
43
+ readonly key: string;
44
+ /** The entry field naming an entry's identity, when entries are an ARRAY.
45
+ * Absent for a keyed map, where the map key is the identity. */
46
+ readonly nameField?: string;
47
+ /** Entry field that widens the mapped node to admit null. */
48
+ readonly nullable?: string;
49
+ /** Entry field that wraps the mapped node in an array. */
50
+ readonly array?: string;
51
+ }
52
+
53
+ export type SchemaMap = Readonly<Record<string, Record<string, unknown>>>;
54
+
55
+ function isObject(value: unknown): value is Record<string, unknown> {
56
+ return !!value && typeof value === "object" && !Array.isArray(value);
57
+ }
58
+
59
+ /** The projection a kind declares, or undefined. Invalid shapes read as absent;
60
+ * `validate-schema-projection.ts` is the half that reports them. */
61
+ export function readSchemaProjection(definition: unknown): SchemaProjection | undefined {
62
+ if (!isObject(definition)) return undefined;
63
+ const raw = rawSchemaProjection(definition);
64
+ if (!isObject(raw)) return undefined;
65
+ const entries = raw.entries;
66
+ const key = raw.key;
67
+ if (typeof entries !== "string" || typeof key !== "string") return undefined;
68
+ return {
69
+ entries,
70
+ key,
71
+ nameField: typeof raw.name === "string" ? raw.name : undefined,
72
+ nullable: typeof raw.nullable === "string" ? raw.nullable : undefined,
73
+ array: typeof raw.array === "string" ? raw.array : undefined,
74
+ };
75
+ }
76
+
77
+ /** The annotation as written, from either position — the document (canonical)
78
+ * or `schema:` (accepted, and reported by the strict half). The document wins:
79
+ * a kind spelling it in both places is describing its own document. */
80
+ export function rawSchemaProjection(definition: unknown): unknown {
81
+ if (!isObject(definition)) return undefined;
82
+ const own = definition["x-telo-schema-projection"];
83
+ if (own !== undefined) return own;
84
+ const schema = definition.schema;
85
+ return isObject(schema) ? schema["x-telo-schema-projection"] : undefined;
86
+ }
87
+
88
+ /** True when the annotation was found inside `schema:` rather than on the
89
+ * document — the misplacement the strict half reports. */
90
+ export function schemaProjectionIsMisplaced(definition: unknown): boolean {
91
+ if (!isObject(definition)) return false;
92
+ if (definition["x-telo-schema-projection"] !== undefined) return false;
93
+ const schema = definition.schema;
94
+ return isObject(schema) && schema["x-telo-schema-projection"] !== undefined;
95
+ }
96
+
97
+ export function readSchemaMap(node: unknown): SchemaMap | undefined {
98
+ if (!isObject(node)) return undefined;
99
+ const raw = node["x-telo-schema-map"];
100
+ if (!isObject(raw)) return undefined;
101
+ const entries = Object.entries(raw).filter(([, value]) => isObject(value));
102
+ if (entries.length === 0) return undefined;
103
+ return Object.fromEntries(entries) as SchemaMap;
104
+ }
105
+
106
+ /** The consumer-side annotation: a JSON Pointer to this resource's ref slot
107
+ * whose target declares the projection. */
108
+ export function readProjectionFrom(node: unknown): string | undefined {
109
+ if (!isObject(node)) return undefined;
110
+ const raw = node["x-telo-schema-projection-from"];
111
+ return typeof raw === "string" ? raw : undefined;
112
+ }
113
+
114
+ function navigate(root: unknown, pointer: string): unknown {
115
+ let current: unknown = root;
116
+ for (const segment of pointer.split("/")) {
117
+ if (segment === "") continue;
118
+ if (!isObject(current)) return undefined;
119
+ current = current[segment];
120
+ }
121
+ return current;
122
+ }
123
+
124
+ /**
125
+ * Find the `x-telo-schema-map` a projection keys on. The map sits on the entry
126
+ * field's schema, which is reached through the collection's own schema — a
127
+ * keyed map's `additionalProperties`, or an array's `items`.
128
+ */
129
+ export function projectionKeyMap(
130
+ kindSchema: unknown,
131
+ projection: SchemaProjection,
132
+ ): SchemaMap | undefined {
133
+ let node: unknown = kindSchema;
134
+ for (const segment of projection.entries.split("/")) {
135
+ if (segment === "") continue;
136
+ if (!isObject(node) || !isObject(node.properties)) return undefined;
137
+ node = node.properties[segment];
138
+ }
139
+ if (!isObject(node)) return undefined;
140
+ const entry = isObject(node.additionalProperties)
141
+ ? node.additionalProperties
142
+ : isObject(node.items)
143
+ ? node.items
144
+ : undefined;
145
+ if (!isObject(entry) || !isObject(entry.properties)) return undefined;
146
+ return readSchemaMap(entry.properties[projection.key]);
147
+ }
148
+
149
+ /**
150
+ * Project one declaration to an object schema.
151
+ *
152
+ * Modifiers are a CLOSED set applied in a FIXED order — `array` wraps, then
153
+ * `nullable` widens. Closed because each changes how the schema is assembled,
154
+ * so a third-party modifier would be a name nothing acts on; ordered because
155
+ * leaving it implicit is how two implementations come to disagree.
156
+ *
157
+ * The projection is deliberately LOSSY. Length, precision, collation and check
158
+ * constraints do not reach it: a consumer needs the type, its nullability and
159
+ * its repetition, and the database enforces the rest. A per-entry schema rich
160
+ * enough to double as a validator would move the domain's semantics into the
161
+ * type layer.
162
+ */
163
+ export function projectEntries(
164
+ manifest: unknown,
165
+ projection: SchemaProjection,
166
+ map: SchemaMap,
167
+ ): Record<string, unknown> | undefined {
168
+ const entries = navigate(manifest, projection.entries);
169
+ if (entries === undefined) return undefined;
170
+
171
+ const pairs: [string, Record<string, unknown>][] = [];
172
+ const consider = (name: string | undefined, entry: unknown): void => {
173
+ if (!isObject(entry) || name === undefined) return;
174
+ const key = entry[projection.key];
175
+ const mapped = typeof key === "string" ? map[key] : undefined;
176
+ // A value with no map entry projects to nothing rather than to `any`: the
177
+ // vocabulary is the kind's own enum, so an unmapped value is a gap in the
178
+ // kind's declaration, not a shape to guess at.
179
+ if (!mapped) return;
180
+ let node: Record<string, unknown> = { ...mapped };
181
+ if (projection.array && entry[projection.array] === true) {
182
+ node = { type: "array", items: node };
183
+ }
184
+ if (projection.nullable && entry[projection.nullable] !== false) {
185
+ node = { anyOf: [node, { type: "null" }] };
186
+ }
187
+ pairs.push([name, node]);
188
+ };
189
+
190
+ if (Array.isArray(entries)) {
191
+ for (const entry of entries) {
192
+ const name = isObject(entry) && projection.nameField
193
+ ? (entry[projection.nameField] as string | undefined)
194
+ : undefined;
195
+ consider(name, entry);
196
+ }
197
+ } else if (isObject(entries)) {
198
+ for (const [name, entry] of Object.entries(entries)) consider(name, entry);
199
+ } else {
200
+ return undefined;
201
+ }
202
+
203
+ return {
204
+ type: "object",
205
+ properties: Object.fromEntries(pairs),
206
+ additionalProperties: false,
207
+ };
208
+ }
209
+
210
+ /** A reference as the analyzer sees it: the internal `{kind, name, alias?}`
211
+ * shape `resolveRefSentinels` rewrites `!ref` to. */
212
+ export interface ProjectionRef {
213
+ readonly name: string;
214
+ readonly kind?: string;
215
+ readonly alias?: string;
216
+ }
217
+
218
+ /** The `{kind, name, alias?}` reference a value holds, or undefined. Exported so
219
+ * a host whose slot may hold EITHER shape can fall back to this reading. */
220
+ export function readProjectionRef(value: unknown): ProjectionRef | undefined {
221
+ if (!isObject(value)) return undefined;
222
+ const name = value.name;
223
+ if (typeof name !== "string") return undefined;
224
+ return {
225
+ name,
226
+ kind: typeof value.kind === "string" ? value.kind : undefined,
227
+ alias: typeof value.alias === "string" ? value.alias : undefined,
228
+ };
229
+ }
230
+
231
+ /** What a reference resolved to. `"ambiguous"` is distinct from `undefined`
232
+ * because the two need different advice: one says disambiguate, the other says
233
+ * the name resolves to nothing. */
234
+ export type ProjectionLookup =
235
+ | { readonly manifest: Record<string, any> }
236
+ | { readonly ambiguous: true }
237
+ | undefined;
238
+
239
+ /**
240
+ * What projecting a consumer's slot needs: resolving a reference to the manifest
241
+ * it names, and the definition that manifest's `kind` names.
242
+ *
243
+ * A RESOLVER rather than a list of manifests, because resolution is scoped and
244
+ * only the host knows the scope: an alias-qualified `!ref Alias.users` names an
245
+ * import's exported instance, and a bare name means the enclosing module's — a
246
+ * distinction a name filter over one flattened list erases, which is how an
247
+ * unambiguous cross-module reference came to read as ambiguous. It is also what
248
+ * lets the kernel supply its own context lookup, so the contract the analyzer
249
+ * types and the contract the kernel enforces are the same schema.
250
+ */
251
+ export interface ProjectionScope {
252
+ /**
253
+ * The declaration the value at a projected slot names.
254
+ *
255
+ * Takes the RAW slot value rather than a parsed reference, because what sits
256
+ * there depends on the host and only the host can read it: the analyzer sees
257
+ * the `{kind, name, alias?}` reference the loader produced, while the kernel
258
+ * binds contracts AFTER Phase-5 injection has replaced that reference with the
259
+ * live instance. Parsing it here would have hardcoded the analyzer's shape and
260
+ * left the kernel unable to resolve anything — which is a contract enforced
261
+ * statically and not at dispatch.
262
+ */
263
+ resolveManifest(value: unknown): ProjectionLookup;
264
+ resolveDefinition(kind: string): Record<string, any> | undefined;
265
+ }
266
+
267
+ /**
268
+ * The resolver for a FLATTENED manifest list — the analyzer's own shape.
269
+ *
270
+ * An alias narrows to the manifests forwarded from that import (stamped
271
+ * `metadata.alias` by flatten), so two libraries each exporting a `users` table
272
+ * stay distinguishable. Only when nothing carries the alias does it fall back to
273
+ * matching by name alone, which is the pre-flatten shape a standalone module
274
+ * analysis has.
275
+ */
276
+ export function manifestListScope(
277
+ manifests: readonly Record<string, any>[],
278
+ resolveDefinition: (kind: string) => Record<string, any> | undefined,
279
+ ): ProjectionScope {
280
+ return {
281
+ resolveDefinition,
282
+ resolveManifest(value) {
283
+ const ref = readProjectionRef(value);
284
+ if (!ref) return undefined;
285
+ const byName = manifests.filter(
286
+ (candidate) =>
287
+ (candidate?.metadata as { name?: unknown } | undefined)?.name === ref.name &&
288
+ (typeof ref.kind !== "string" || candidate.kind === ref.kind),
289
+ );
290
+ const aliased =
291
+ ref.alias && ref.alias !== "Self"
292
+ ? byName.filter(
293
+ (candidate) =>
294
+ (candidate?.metadata as { alias?: unknown } | undefined)?.alias === ref.alias,
295
+ )
296
+ : byName;
297
+ // A name that matches SEVERAL manifests is REFUSED rather than resolved to
298
+ // the first: picking one by flatten order would type the consumer's rows
299
+ // against the wrong declaration — a wrong answer, which is worse than no
300
+ // answer. Reported, so the author is told to disambiguate.
301
+ const matches = aliased.length > 0 ? aliased : byName;
302
+ if (matches.length === 0) return undefined;
303
+ if (matches.length > 1) return { ambiguous: true };
304
+ return { manifest: matches[0]! };
305
+ },
306
+ };
307
+ }
308
+
309
+ export type ProjectionFailure =
310
+ | { readonly reason: "no-ref"; readonly pointer: string }
311
+ | { readonly reason: "unresolved"; readonly pointer: string; readonly name: string }
312
+ | { readonly reason: "ambiguous"; readonly pointer: string; readonly name: string }
313
+ | { readonly reason: "no-projection"; readonly pointer: string; readonly kind: string };
314
+
315
+ function refTarget(
316
+ value: unknown,
317
+ scope: ProjectionScope,
318
+ pointer: string,
319
+ ):
320
+ | { manifest: Record<string, any>; definition: Record<string, any> }
321
+ | ProjectionFailure {
322
+ if (!isObject(value)) return { reason: "no-ref", pointer };
323
+ const name = typeof value.name === "string" ? value.name : "<unnamed>";
324
+ const found = scope.resolveManifest(value);
325
+ if (!found) return { reason: "unresolved", pointer, name };
326
+ if ("ambiguous" in found) return { reason: "ambiguous", pointer, name };
327
+ const manifest = found.manifest;
328
+ if (typeof manifest.kind !== "string") return { reason: "unresolved", pointer, name };
329
+ const definition = scope.resolveDefinition(manifest.kind);
330
+ if (!definition) return { reason: "no-projection", pointer, kind: manifest.kind };
331
+ return { manifest, definition };
332
+ }
333
+
334
+ export function describeProjectionFailure(failure: ProjectionFailure): string {
335
+ switch (failure.reason) {
336
+ case "no-ref":
337
+ return `'${failure.pointer}' does not hold a reference, so there is no declaration to project.`;
338
+ case "unresolved":
339
+ return `'${failure.pointer}' references '${failure.name}', which resolves to no resource.`;
340
+ case "ambiguous":
341
+ return (
342
+ `'${failure.pointer}' references '${failure.name}', which matches more than one resource ` +
343
+ `in scope. Rename one of them so the reference names exactly one declaration.`
344
+ );
345
+ case "no-projection":
346
+ return (
347
+ `'${failure.pointer}' references a resource of kind '${failure.kind}', which declares no ` +
348
+ `'x-telo-schema-projection' — so there is nothing for this slot to be typed from.`
349
+ );
350
+ }
351
+ }
352
+
353
+ /**
354
+ * Replace every `x-telo-schema-projection-from` node with the projection of the
355
+ * declaration it points at.
356
+ *
357
+ * Structural: returns a new schema and never mutates the one handed in. A node
358
+ * that cannot be projected is left exactly as it was — degrading to the slot's
359
+ * own schema rather than to a wrong one — and the reason is pushed to
360
+ * `failures`, because degrading SILENTLY is the failure this whole mechanism
361
+ * exists to move earlier: the consumer's contract quietly reopens and a
362
+ * misspelled field passes `telo check` exactly as it did before.
363
+ */
364
+ export function resolveSchemaProjections(
365
+ schema: unknown,
366
+ manifest: Record<string, any> | undefined,
367
+ scope: ProjectionScope,
368
+ failures?: ProjectionFailure[],
369
+ ): unknown {
370
+ if (Array.isArray(schema)) {
371
+ return schema.map((item) => resolveSchemaProjections(item, manifest, scope, failures));
372
+ }
373
+ if (!isObject(schema)) return schema;
374
+
375
+ const pointer = readProjectionFrom(schema);
376
+ if (pointer && manifest) {
377
+ const target = refTarget(navigate(manifest, pointer), scope, pointer);
378
+ if ("reason" in target) {
379
+ failures?.push(target);
380
+ } else {
381
+ const projection = readSchemaProjection(target.definition);
382
+ const map = projection && projectionKeyMap(target.definition.schema, projection);
383
+ const projected =
384
+ projection && map ? projectEntries(target.manifest, projection, map) : undefined;
385
+ if (projected) {
386
+ const { ["x-telo-schema-projection-from"]: _dropped, ...rest } = schema;
387
+ return { ...rest, ...projected };
388
+ }
389
+ failures?.push({
390
+ reason: "no-projection",
391
+ pointer,
392
+ kind: String(target.manifest.kind ?? "<unknown>"),
393
+ });
394
+ }
395
+ }
396
+
397
+ return Object.fromEntries(
398
+ Object.entries(schema).map(([key, value]) => [
399
+ key,
400
+ key.startsWith("x-telo-") ? value : resolveSchemaProjections(value, manifest, scope, failures),
401
+ ]),
402
+ );
403
+ }
@@ -0,0 +1,64 @@
1
+ /**
2
+ * "Does this array hold steps, and where does a step keep what the analyzer
3
+ * reads?" — one accessor, the `ref-slot.ts` / `zone-slot.ts` precedent.
4
+ *
5
+ * TWO SPELLINGS, one answer. A kind declares a step body by pointing its items
6
+ * at the shared grammar (`$ref: "telo://manifest#/$defs/Step"`), which fragment
7
+ * expansion stamps `x-telo-fragment: Step`; the field names are then constants
8
+ * of that grammar and nothing states them. Before the fragment existed the shape
9
+ * was each kind's own, so the kind had to say where its dispatch ref, output
10
+ * type and pure-value fields lived — `x-telo-step-context`. That annotation
11
+ * stays READ, permanently: published artifacts carry it and no migration entry
12
+ * can synthesize a `$ref` (the patch vocabulary writes scalars), so it has the
13
+ * standing of the legacy `x-telo-ref` string form.
14
+ *
15
+ * The annotation WINS where both are present. A kind that spells its own shape
16
+ * is describing its own manifest, and a stamp says only which grammar the items
17
+ * point at.
18
+ *
19
+ * Browser-safe: no Node built-ins.
20
+ */
21
+
22
+ import { manifestFragmentOf } from "./manifest-schemas.js";
23
+
24
+ /** The name a step-bearing array's items carry once expanded. */
25
+ export const STEP_FRAGMENT = "Step";
26
+
27
+ /** Where a step keeps what the analyzer reads. */
28
+ export interface StepSlot {
29
+ /** Field on a step naming the resource to dispatch. */
30
+ invoke: string;
31
+ /** Field on the INVOKED resource's manifest that narrows its result type.
32
+ * Absent for a composer that allows no per-instance narrowing. */
33
+ outputType?: string;
34
+ /** Field that produces a result without dispatching. Only a grammar with pure
35
+ * steps has one. */
36
+ value?: string;
37
+ }
38
+
39
+ /** The shared grammar's own field names — constants, since the shape is no
40
+ * longer the kind's to choose. */
41
+ const STEP_FRAGMENT_SLOT: StepSlot = { invoke: "invoke", outputType: "outputType", value: "value" };
42
+
43
+ /** The step slot an array property declares, or undefined when it holds no
44
+ * steps. `fieldSchema` is the ARRAY's schema — the stamp sits on its `items`. */
45
+ export function readStepSlot(fieldSchema: unknown): StepSlot | undefined {
46
+ if (!fieldSchema || typeof fieldSchema !== "object") return undefined;
47
+ const declared = (fieldSchema as Record<string, unknown>)["x-telo-step-context"];
48
+ if (declared && typeof declared === "object") {
49
+ const { invoke, outputType, value } = declared as Record<string, unknown>;
50
+ if (typeof invoke !== "string" || invoke.length === 0) return undefined;
51
+ return {
52
+ invoke,
53
+ ...(typeof outputType === "string" ? { outputType } : {}),
54
+ ...(typeof value === "string" ? { value } : {}),
55
+ };
56
+ }
57
+ const items = (fieldSchema as Record<string, unknown>).items;
58
+ return manifestFragmentOf(items) === STEP_FRAGMENT ? STEP_FRAGMENT_SLOT : undefined;
59
+ }
60
+
61
+ /** True when the property holds a step body. */
62
+ export function isStepSlot(fieldSchema: unknown): boolean {
63
+ return readStepSlot(fieldSchema) !== undefined;
64
+ }
@@ -6,4 +6,4 @@
6
6
  // written against, and every kernel in every language reports the same scale.
7
7
 
8
8
  /** The surface generation this analyzer implements. */
9
- export const TELO_SURFACE_VERSION = "0.78.0";
9
+ export const TELO_SURFACE_VERSION = "0.79.0";
@@ -6,6 +6,8 @@ import {
6
6
  parseCanonicalTypeSchemaId,
7
7
  } from "@telorun/sdk";
8
8
  import { KERNEL_BUILTINS } from "./builtins.js";
9
+ import { isStepSlot } from "./step-slot.js";
10
+ import { withRefSlotsAsReadings } from "./ref-slot-reading.js";
9
11
 
10
12
  export interface ContextResolveOpts {
11
13
  /** When provided, used to resolve `x-telo-context-from-root` annotations against the
@@ -432,7 +434,17 @@ export function resolveContextAnnotations(
432
434
  // so the variable is typed by the CONTRACT rather than by the wrapper
433
435
  // around it. A raw JSON Schema resolves to itself, and a plain property
434
436
  // map (a transport scope) resolves to nothing and is used verbatim.
435
- return resolveTypeFieldToSchema(navigated, allManifests ?? []) ?? navigated;
437
+ const resolved = resolveTypeFieldToSchema(navigated, allManifests ?? []) ?? navigated;
438
+ // A ref slot inside it holds a live instance at runtime, which the
439
+ // kernel substitutes with that resource's published reading before
440
+ // evaluating (`celSelfView`). Type it the same way, or `self.<ref>` is
441
+ // the annotation node — a shape nothing can be read off.
442
+ return defs
443
+ ? (withRefSlotsAsReadings(resolved, {
444
+ resolve: (kind) => defs.resolve(kind),
445
+ resolveKind: (kind) => aliases?.resolveKind(kind),
446
+ }) as Record<string, any>)
447
+ : resolved;
436
448
  }
437
449
  }
438
450
  if (defs) {
@@ -568,7 +580,9 @@ export function extractContextsFromSchema(
568
580
  }
569
581
 
570
582
  /** Schema keys that declare a CEL-bearing region: a field carrying any of these
571
- * is evaluated at runtime, so a `!cel` inside it (or a descendant) is live. */
583
+ * is evaluated at runtime, so a `!cel` inside it (or a descendant) is live. A
584
+ * STEP BODY is one too, and says so through the grammar its items point at
585
+ * rather than through a key — {@link isStepSlot} reads either spelling. */
572
586
  const CEL_REGION_KEYS = [
573
587
  "x-telo-context",
574
588
  "x-telo-step-context",
@@ -587,7 +601,7 @@ export function extractCelRegionScopes(schema: Record<string, any>, path = "$"):
587
601
  if (!schema || typeof schema !== "object") return [];
588
602
  const out: string[] = [];
589
603
 
590
- if (CEL_REGION_KEYS.some((k) => schema[k])) out.push(path);
604
+ if (CEL_REGION_KEYS.some((k) => schema[k]) || isStepSlot(schema)) out.push(path);
591
605
 
592
606
  if (schema.properties) {
593
607
  for (const [key, value] of Object.entries(schema.properties as Record<string, any>)) {