@telorun/analyzer 0.45.0 → 0.47.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 (37) hide show
  1. package/dist/alias-resolver.d.ts +45 -0
  2. package/dist/alias-resolver.d.ts.map +1 -1
  3. package/dist/alias-resolver.js +33 -0
  4. package/dist/analyzer.d.ts.map +1 -1
  5. package/dist/analyzer.js +106 -5
  6. package/dist/builtins.d.ts.map +1 -1
  7. package/dist/builtins.js +3 -0
  8. package/dist/extends-resolution.d.ts +19 -2
  9. package/dist/extends-resolution.d.ts.map +1 -1
  10. package/dist/extends-resolution.js +25 -1
  11. package/dist/index.d.ts +5 -1
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/index.js +3 -1
  14. package/dist/kernel-globals.d.ts +9 -1
  15. package/dist/kernel-globals.d.ts.map +1 -1
  16. package/dist/kernel-globals.js +24 -1
  17. package/dist/resolve-ref-sentinels.d.ts +13 -1
  18. package/dist/resolve-ref-sentinels.d.ts.map +1 -1
  19. package/dist/resolve-ref-sentinels.js +56 -5
  20. package/dist/validate-cel-context.d.ts.map +1 -1
  21. package/dist/validate-cel-context.js +17 -1
  22. package/dist/validate-observed-state.d.ts +98 -0
  23. package/dist/validate-observed-state.d.ts.map +1 -0
  24. package/dist/validate-observed-state.js +304 -0
  25. package/dist/validate-references.d.ts.map +1 -1
  26. package/dist/validate-references.js +8 -2
  27. package/package.json +2 -2
  28. package/src/alias-resolver.ts +58 -0
  29. package/src/analyzer.ts +126 -4
  30. package/src/builtins.ts +3 -0
  31. package/src/extends-resolution.ts +37 -3
  32. package/src/index.ts +13 -0
  33. package/src/kernel-globals.ts +20 -0
  34. package/src/resolve-ref-sentinels.ts +68 -4
  35. package/src/validate-cel-context.ts +20 -1
  36. package/src/validate-observed-state.ts +354 -0
  37. package/src/validate-references.ts +8 -2
@@ -1,4 +1,5 @@
1
1
  import { isRefSentinel, isTaggedSentinel } from "@telorun/templating";
2
+ import { isScopeEntry, resolveFieldEntries, } from "./reference-field-map.js";
2
3
  import { REF_RESOLUTION_SKIP_KINDS as SYSTEM_KINDS } from "./system-kinds.js";
3
4
  /**
4
5
  * Rewrites every `!ref <name>` sentinel in each non-system resource's value tree
@@ -41,7 +42,11 @@ export function resolveRefSentinels(resources, aliases, aliasesByModule,
41
42
  // walked as sources). The kernel passes the analyzer-flattened set here so the runtime
42
43
  // pass — which loads the entry module only — can still resolve `!ref Alias.name` against
43
44
  // imported libraries' exported instances.
44
- crossModuleTargets = []) {
45
+ crossModuleTargets = [],
46
+ /** Supplies each kind's `x-telo-scope` slots. Without it a scoped name cannot be
47
+ * told from a module-level one, and a shadowed `!ref` resolves to the resource
48
+ * it shadows — so both call sites pass it. */
49
+ defs) {
45
50
  const moduleOf = (r) => r.metadata?.module;
46
51
  // Forwarded exports are flagged by flattenForAnalyzer (`metadata.forwardedExport`); they're
47
52
  // cross-module resolution targets only — never walked as local ref sources here.
@@ -96,11 +101,50 @@ crossModuleTargets = []) {
96
101
  }
97
102
  return undefined;
98
103
  };
104
+ /** Names a resource declares in its own execution scopes, read from the kind's
105
+ * `x-telo-scope` slots — the analyzer's single definition of "scope", shared
106
+ * with `manifest-visitor`. Inferring it structurally instead (any array of
107
+ * named inline manifests) would give scope-local shadowing to the first kind
108
+ * that happens to carry such an array without asking for it, and this pass is
109
+ * shared with the kernel, so the guess would be baked into the runtime tree
110
+ * rather than merely reported. */
111
+ const declaredInScopes = (resource) => {
112
+ const fieldMap = defs?.getFieldMapForKind(resource.kind, aliases);
113
+ if (!fieldMap)
114
+ return undefined;
115
+ let declared;
116
+ for (const [fieldPath, entry] of fieldMap) {
117
+ if (!isScopeEntry(entry))
118
+ continue;
119
+ for (const { value } of resolveFieldEntries(resource, fieldPath)) {
120
+ for (const element of Array.isArray(value) ? value : [value]) {
121
+ if (!element || typeof element !== "object" || Array.isArray(element))
122
+ continue;
123
+ const manifest = element;
124
+ const name = manifest.metadata?.name;
125
+ if (typeof manifest.kind === "string" && typeof name === "string") {
126
+ (declared ??= new Map()).set(name, manifest);
127
+ }
128
+ }
129
+ }
130
+ }
131
+ return declared;
132
+ };
99
133
  // Resolve every `!ref` sentinel in the tree; leave opaque tagged / precompiled
100
134
  // nodes (e.g. `!cel`) untouched and don't descend into them.
101
- const walk = (value) => {
135
+ //
136
+ // `scoped` carries the names the enclosing resource declares in its `x-telo-scope`
137
+ // slots, and they SHADOW the module-level ones — the order the runtime resolves
138
+ // in. Baking the module-level kind into a shadowed reference would label traces
139
+ // and `getRefIdentity` with a resource that never runs.
140
+ const walk = (value, scoped) => {
102
141
  if (isRefSentinel(value)) {
103
- return resolveTarget(value.source) ?? value;
142
+ const source = value.source;
143
+ const bare = source.indexOf(".") === -1;
144
+ const shadow = bare ? scoped?.get(source) : undefined;
145
+ if (shadow)
146
+ return { kind: shadow.kind, name: source };
147
+ return resolveTarget(source) ?? value;
104
148
  }
105
149
  if (value === null || typeof value !== "object")
106
150
  return value;
@@ -110,12 +154,19 @@ crossModuleTargets = []) {
110
154
  return value;
111
155
  if (Array.isArray(value)) {
112
156
  for (let i = 0; i < value.length; i++)
113
- value[i] = walk(value[i]);
157
+ value[i] = walk(value[i], scoped);
114
158
  return value;
115
159
  }
116
160
  const obj = value;
161
+ // A nested inline resource may declare scopes of its own (a `Run.Sequence`
162
+ // inside another sequence's `with:`). Collected before descending, so the
163
+ // declarations are visible to every region of the resource that declares
164
+ // them — a sequence's `with:` names resolve in its `targets:` and `steps:`
165
+ // alike, not only inside `with:` itself.
166
+ const declared = typeof obj.kind === "string" ? declaredInScopes(obj) : undefined;
167
+ const inner = declared ? new Map([...(scoped ?? new Map()), ...declared]) : scoped;
117
168
  for (const key of Object.keys(obj))
118
- obj[key] = walk(obj[key]);
169
+ obj[key] = walk(obj[key], inner);
119
170
  return value;
120
171
  };
121
172
  for (const r of resources) {
@@ -1 +1 @@
1
- {"version":3,"file":"validate-cel-context.d.ts","sourceRoot":"","sources":["../src/validate-cel-context.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,0BAA0B,EAAE,MAAM,qBAAqB,CAAC;AAGtF,MAAM,WAAW,kBAAkB;IACjC;mEAC+D;IAC/D,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IACnC;;kDAE8C;IAC9C,IAAI,CAAC,EAAE;QACL,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,SAAS,CAAC;KACxD,CAAC;IACF,OAAO,CAAC,EAAE;QACR,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAC;KAC/C,CAAC;IACF,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,CAAC;CACtC;AAED;;;;;GAKG;AACH,wBAAgB,wBAAwB,CACtC,KAAK,EAAE,OAAO,EACd,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,EACnC,QAAQ,GAAE,WAAW,CAAC,MAAM,CAAa,GACxC,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,SAAS,CA0CjC;AAuFD;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAoBzE;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+CG;AACH,wBAAgB,yBAAyB,CACvC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAC3B,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EACjC,IAAI,CAAC,EAAE,kBAAkB,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,GAChD,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAgHrB;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAC7B,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAC5B,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAQrB;AAWD;;;;;;;;;GASG;AACH,wBAAgB,yBAAyB,CACvC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAC3B,IAAI,SAAM,GACT,KAAK,CAAC;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;CAAE,CAAC,CAGvD;AAUD;;;;;;;GAOG;AACH,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,IAAI,SAAM,GAAG,MAAM,EAAE,CAqBxF"}
1
+ {"version":3,"file":"validate-cel-context.d.ts","sourceRoot":"","sources":["../src/validate-cel-context.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,0BAA0B,EAAE,MAAM,qBAAqB,CAAC;AAGtF,MAAM,WAAW,kBAAkB;IACjC;mEAC+D;IAC/D,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IACnC;;kDAE8C;IAC9C,IAAI,CAAC,EAAE;QACL,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,SAAS,CAAC;KACxD,CAAC;IACF,OAAO,CAAC,EAAE;QACR,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAC;KAC/C,CAAC;IACF,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,CAAC;CACtC;AAED;;;;;GAKG;AACH,wBAAgB,wBAAwB,CACtC,KAAK,EAAE,OAAO,EACd,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,EACnC,QAAQ,GAAE,WAAW,CAAC,MAAM,CAAa,GACxC,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,SAAS,CA0CjC;AAuFD;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAoBzE;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+CG;AACH,wBAAgB,yBAAyB,CACvC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAC3B,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EACjC,IAAI,CAAC,EAAE,kBAAkB,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,GAChD,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAmIrB;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAC7B,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAC5B,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAQrB;AAWD;;;;;;;;;GASG;AACH,wBAAgB,yBAAyB,CACvC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAC3B,IAAI,SAAM,GACT,KAAK,CAAC;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;CAAE,CAAC,CAGvD;AAUD;;;;;;;GAOG;AACH,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,IAAI,SAAM,GAAG,MAAM,EAAE,CAqBxF"}
@@ -276,13 +276,29 @@ export function resolveContextAnnotations(schema, manifestItem, opts) {
276
276
  typeof ref.kind === "string" &&
277
277
  typeof ref.name === "string" &&
278
278
  subpath) {
279
+ const segments = subpath.split("/");
279
280
  const refManifest = allManifests.find((m) => m.kind === ref.kind && m.metadata?.name === ref.name);
280
281
  if (refManifest) {
281
- const resolved = resolveTypeFieldToSchema(navigatePath(refManifest, subpath.split("/")), allManifests);
282
+ const resolved = resolveTypeFieldToSchema(navigatePath(refManifest, segments), allManifests);
282
283
  if (resolved && typeof resolved === "object") {
283
284
  return resolved;
284
285
  }
285
286
  }
287
+ // The instance declares nothing, so fall back to its KIND's declaration —
288
+ // the same layering `buildStepContextSchema` applies to `steps.<name>.result`,
289
+ // so a kind with one fixed output shape (declared once on its Telo.Definition)
290
+ // types the context, while a kind that exposes the field for per-instance
291
+ // narrowing keeps winning above.
292
+ if (defs) {
293
+ const canonical = aliases?.resolveKind(ref.kind) ?? ref.kind;
294
+ const def = defs.resolve(canonical);
295
+ if (def) {
296
+ const resolved = resolveTypeFieldToSchema(navigatePath(def, segments), allManifests);
297
+ if (resolved && typeof resolved === "object") {
298
+ return resolved;
299
+ }
300
+ }
301
+ }
286
302
  }
287
303
  // Fallback: open schema (no false errors when outputType is not declared)
288
304
  return { ...schema, additionalProperties: true };
@@ -0,0 +1,98 @@
1
+ import type { ResourceDefinition, ResourceManifest } from "@telorun/sdk";
2
+ import { type ModuleScopes } from "./alias-resolver.js";
3
+ /**
4
+ * The `status:` block's own schema — a plain JSON Schema, structurally. The one
5
+ * normative restriction (`required:` is rejected) is enforced by
6
+ * {@link validateObservedStateDeclarations} rather than here, so the author gets
7
+ * a message naming the rule and the fix instead of AJV's "must NOT be valid".
8
+ *
9
+ * Exported from the analyzer and re-used by the kernel's manifest schemas, so
10
+ * the rule has one definition rather than two kept in sync by hand.
11
+ */
12
+ export declare const OBSERVED_STATE_SCHEMA: {
13
+ type: string;
14
+ additionalProperties: boolean;
15
+ };
16
+ /**
17
+ * `required:` inside a `status:` block. Every declared field is mandatory once
18
+ * the resource has run, so the list would be either redundant or a lie; a
19
+ * genuinely sometimes-absent value is declared with a nullable type, which
20
+ * `CEL_NULLABLE_ACCESS` already guards.
21
+ */
22
+ export declare function validateObservedStateDeclarations(manifests: readonly ResourceManifest[]): Array<{
23
+ kind: string;
24
+ name: string;
25
+ filePath?: string;
26
+ message: string;
27
+ }>;
28
+ /** A CEL access into a resource's observed-state segment. */
29
+ export interface ObservedStateRead {
30
+ /** Import alias, when the read crosses a module boundary
31
+ * (`resources.<Alias>.<name>.status`). */
32
+ alias?: string;
33
+ /** Resource name. */
34
+ name: string;
35
+ /** The field read under `.status`, when the chain names one. */
36
+ field?: string;
37
+ }
38
+ /**
39
+ * Recognise an observed-state read in a member-access chain. Purely syntactic —
40
+ * it inspects the chain, not the topology — so the availability rule it feeds
41
+ * applies to every kind, declared or not.
42
+ *
43
+ * `resources.<name>.status.<field>` and the two-level cross-module form
44
+ * `resources.<Alias>.<name>.status.<field>` are both observed-state reads.
45
+ */
46
+ export declare function observedStateRead(chain: readonly string[]): ObservedStateRead | undefined;
47
+ /**
48
+ * The names of every resource some slot can start: referenced from a ref slot
49
+ * that accepts a `Telo.Runnable` / `Telo.Service`, or named as a step's
50
+ * `invoke:` target. A resource in none of them can never `run()`, so it can
51
+ * never report observed state.
52
+ *
53
+ * Deliberately an over-approximation — a name reachable through any of these
54
+ * routes counts as runnable — because the cost of a false "can never run" is a
55
+ * valid manifest rejected, while the cost of a miss is only that the reader
56
+ * finds out at runtime instead, with a message that names the same fix.
57
+ */
58
+ export declare function collectRunReachableNames(manifests: readonly ResourceManifest[], defs: {
59
+ resolve(kind: string): ResourceDefinition | undefined;
60
+ }, aliases?: {
61
+ resolveKind(kind: string): string | undefined;
62
+ }): Set<string>;
63
+ /** What a resource name resolves to for CEL purposes. `status` is present only
64
+ * when the kind declares one; `scoped` marks a resource declared inside an
65
+ * `x-telo-scope` slot, which resolves only within that scope's regions. */
66
+ export interface AnalyzedResource {
67
+ kind: string;
68
+ status?: Record<string, any>;
69
+ scoped?: boolean;
70
+ }
71
+ /**
72
+ * Index every resource a CEL `resources.…` read can name: the module's own
73
+ * top-level resources, the ones declared inside `x-telo-scope` slots (a
74
+ * `Run.Sequence`'s `with:`), and each import's exported instances — keyed
75
+ * `<Alias>.<name>`, the two-level shape those publish under.
76
+ *
77
+ * Scope slots are found through the declaring kind's schema annotation, not by
78
+ * field name, so any composer with a scope participates.
79
+ */
80
+ export declare function buildObservedStateIndex(manifests: readonly ResourceManifest[], defs: {
81
+ resolve(kind: string): ResourceDefinition | undefined;
82
+ }, aliases?: {
83
+ resolveKind(kind: string): string | undefined;
84
+ moduleForAlias?(alias: string): string | undefined;
85
+ }, scopes?: ModuleScopes): Map<string, AnalyzedResource>;
86
+ /** The `resources` node of a CEL context schema: one entry per resource, each
87
+ * open except for a typed, closed `status` node on kinds that declare one.
88
+ * `open` keeps the map itself permissive, so unknown resource names and every
89
+ * flat field pass exactly as they do today. */
90
+ export declare function buildObservedStateResourcesSchema(index: ReadonlyMap<string, AnalyzedResource>, open: boolean): Record<string, any>;
91
+ /**
92
+ * Write the typed `status` node for one index key into a `resources` property
93
+ * map. A dotted key (`Alias.name`) is an import's exported instance, which
94
+ * publishes two levels deep — the alias node stays open so every other name
95
+ * under it keeps resolving as it does today.
96
+ */
97
+ export declare function applyObservedStateNode(properties: Record<string, any>, key: string, status: Record<string, any>): void;
98
+ //# sourceMappingURL=validate-observed-state.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validate-observed-state.d.ts","sourceRoot":"","sources":["../src/validate-observed-state.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAIzE,OAAO,EAA2B,KAAK,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAyBjF;;;;;;;;GAQG;AACH,eAAO,MAAM,qBAAqB;;;CAGjC,CAAC;AAEF;;;;;GAKG;AACH,wBAAgB,iCAAiC,CAC/C,SAAS,EAAE,SAAS,gBAAgB,EAAE,GACrC,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC,CAmB3E;AAED,6DAA6D;AAC7D,MAAM,WAAW,iBAAiB;IAChC;+CAC2C;IAC3C,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,qBAAqB;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,gEAAgE;IAChE,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,GAAG,iBAAiB,GAAG,SAAS,CAOzF;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,wBAAwB,CACtC,SAAS,EAAE,SAAS,gBAAgB,EAAE,EACtC,IAAI,EAAE;IAAE,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,kBAAkB,GAAG,SAAS,CAAA;CAAE,EAC/D,OAAO,CAAC,EAAE;IAAE,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAA;CAAE,GAC1D,GAAG,CAAC,MAAM,CAAC,CAyBb;AAgDD;;4EAE4E;AAC5E,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IAC7B,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED;;;;;;;;GAQG;AACH,wBAAgB,uBAAuB,CACrC,SAAS,EAAE,SAAS,gBAAgB,EAAE,EACtC,IAAI,EAAE;IAAE,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,kBAAkB,GAAG,SAAS,CAAA;CAAE,EAC/D,OAAO,CAAC,EAAE;IAAE,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAC;IAAC,cAAc,CAAC,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAA;CAAE,EAC/G,MAAM,CAAC,EAAE,YAAY,GACpB,GAAG,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAsC/B;AAyDD;;;gDAGgD;AAChD,wBAAgB,iCAAiC,CAC/C,KAAK,EAAE,WAAW,CAAC,MAAM,EAAE,gBAAgB,CAAC,EAC5C,IAAI,EAAE,OAAO,GACZ,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CASrB;AAED;;;;;GAKG;AACH,wBAAgB,sBAAsB,CACpC,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAC/B,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAC1B,IAAI,CAmBN"}
@@ -0,0 +1,304 @@
1
+ import { OBSERVED_STATE_KEY } from "@telorun/sdk";
2
+ import { effectiveStatusSchema } from "./extends-resolution.js";
3
+ import { parseExportEntry } from "./flatten-for-analyzer.js";
4
+ import { moduleScopedDefResolver } from "./alias-resolver.js";
5
+ import { buildReferenceFieldMap, isRefEntry, isScopeEntry, resolveFieldValues, } from "./reference-field-map.js";
6
+ /** The kernel capabilities whose `run()` the kernel dispatches. A ref slot that
7
+ * accepts one of them is a slot that can start a resource — `targets:` on an
8
+ * Application or a `Run.Sequence`, and a step's `invoke:` (whose schema accepts
9
+ * `Telo.Runnable` alongside `Telo.Invocable`, and which the kernel dispatches
10
+ * through `run()` when the target has no `invoke()`). Keyed on the declared
11
+ * capability, never on a field name or a kind, so any composer that accepts a
12
+ * runnable participates without the analyzer knowing about it. */
13
+ const RUN_DISPATCH_CONTRACTS = new Set(["Telo.Runnable", "Telo.Service"]);
14
+ const SYSTEM_KINDS = new Set([
15
+ "Telo.Definition",
16
+ "Telo.Abstract",
17
+ "Telo.Import",
18
+ "Telo.Application",
19
+ "Telo.Library",
20
+ ]);
21
+ /**
22
+ * The `status:` block's own schema — a plain JSON Schema, structurally. The one
23
+ * normative restriction (`required:` is rejected) is enforced by
24
+ * {@link validateObservedStateDeclarations} rather than here, so the author gets
25
+ * a message naming the rule and the fix instead of AJV's "must NOT be valid".
26
+ *
27
+ * Exported from the analyzer and re-used by the kernel's manifest schemas, so
28
+ * the rule has one definition rather than two kept in sync by hand.
29
+ */
30
+ export const OBSERVED_STATE_SCHEMA = {
31
+ type: "object",
32
+ additionalProperties: true,
33
+ };
34
+ /**
35
+ * `required:` inside a `status:` block. Every declared field is mandatory once
36
+ * the resource has run, so the list would be either redundant or a lie; a
37
+ * genuinely sometimes-absent value is declared with a nullable type, which
38
+ * `CEL_NULLABLE_ACCESS` already guards.
39
+ */
40
+ export function validateObservedStateDeclarations(manifests) {
41
+ const out = [];
42
+ for (const m of manifests) {
43
+ if (m.kind !== "Telo.Definition" && m.kind !== "Telo.Abstract")
44
+ continue;
45
+ const status = m.status;
46
+ if (!status || typeof status !== "object" || !Array.isArray(status.required))
47
+ continue;
48
+ const name = m.metadata?.name ?? "<unnamed>";
49
+ out.push({
50
+ kind: m.kind,
51
+ name,
52
+ filePath: m.metadata?.source,
53
+ message: `${m.kind}/${name}: 'status:' must not declare 'required:' — every field a kind declares ` +
54
+ `it reports is mandatory once the resource has run, so the list is either redundant or a ` +
55
+ `lie. Declare a sometimes-absent field with a nullable type instead ` +
56
+ `(e.g. type: [string, "null"]); CEL_NULLABLE_ACCESS then forces the reader to guard it.`,
57
+ });
58
+ }
59
+ return out;
60
+ }
61
+ /**
62
+ * Recognise an observed-state read in a member-access chain. Purely syntactic —
63
+ * it inspects the chain, not the topology — so the availability rule it feeds
64
+ * applies to every kind, declared or not.
65
+ *
66
+ * `resources.<name>.status.<field>` and the two-level cross-module form
67
+ * `resources.<Alias>.<name>.status.<field>` are both observed-state reads.
68
+ */
69
+ export function observedStateRead(chain) {
70
+ if (chain[0] !== "resources")
71
+ return undefined;
72
+ if (chain[2] === OBSERVED_STATE_KEY)
73
+ return { name: chain[1], field: chain[3] };
74
+ if (chain[3] === OBSERVED_STATE_KEY) {
75
+ return { alias: chain[1], name: chain[2], field: chain[4] };
76
+ }
77
+ return undefined;
78
+ }
79
+ /**
80
+ * The names of every resource some slot can start: referenced from a ref slot
81
+ * that accepts a `Telo.Runnable` / `Telo.Service`, or named as a step's
82
+ * `invoke:` target. A resource in none of them can never `run()`, so it can
83
+ * never report observed state.
84
+ *
85
+ * Deliberately an over-approximation — a name reachable through any of these
86
+ * routes counts as runnable — because the cost of a false "can never run" is a
87
+ * valid manifest rejected, while the cost of a miss is only that the reader
88
+ * finds out at runtime instead, with a message that names the same fix.
89
+ */
90
+ export function collectRunReachableNames(manifests, defs, aliases) {
91
+ const names = new Set();
92
+ const resolve = (kind) => defs.resolve(aliases?.resolveKind(kind) ?? kind) ?? defs.resolve(kind);
93
+ for (const manifest of manifests) {
94
+ const def = resolve(manifest.kind);
95
+ const schema = def?.schema;
96
+ if (!schema)
97
+ continue;
98
+ for (const [path, entry] of buildReferenceFieldMap(schema)) {
99
+ if (!isRefEntry(entry))
100
+ continue;
101
+ if (!entry.refs.some((ref) => RUN_DISPATCH_CONTRACTS.has(ref)))
102
+ continue;
103
+ for (const value of resolveFieldValues(manifest, path))
104
+ collectRefName(value, names);
105
+ }
106
+ // Step arrays nest through `if` / `while` / `switch` / `try`, and the step
107
+ // `invoke:` slot sits behind a local `$ref` the field map does not follow.
108
+ // Match the declared invoke key at any depth instead of re-deriving the
109
+ // nesting rules — over-approximating in the safe direction.
110
+ const invokeKey = stepInvokeKey(schema);
111
+ if (invokeKey)
112
+ collectKeyedRefs(manifest, invokeKey, names);
113
+ }
114
+ return names;
115
+ }
116
+ /** The property name a kind's `x-telo-step-context` declares as its dispatch
117
+ * slot (`invoke`), or undefined when the kind has no step array. */
118
+ function stepInvokeKey(schema) {
119
+ for (const fieldSchema of Object.values((schema.properties ?? {}))) {
120
+ const stepCtx = fieldSchema?.["x-telo-step-context"];
121
+ if (stepCtx?.invoke)
122
+ return stepCtx.invoke;
123
+ }
124
+ return undefined;
125
+ }
126
+ /** Collect ref names at every `key` property anywhere in `node`. */
127
+ function collectKeyedRefs(node, key, out) {
128
+ if (Array.isArray(node)) {
129
+ for (const item of node)
130
+ collectKeyedRefs(item, key, out);
131
+ return;
132
+ }
133
+ if (node === null || typeof node !== "object")
134
+ return;
135
+ for (const [k, value] of Object.entries(node)) {
136
+ if (k === key)
137
+ collectRefName(value, out);
138
+ collectKeyedRefs(value, key, out);
139
+ }
140
+ }
141
+ /** Record the resource name a slot value points at — a resolved `{kind, name}`
142
+ * ref, an unresolved `!ref` sentinel, or a `{ ref }` / `{ invoke }` wrapper. */
143
+ function collectRefName(value, out) {
144
+ if (value === null || typeof value !== "object")
145
+ return;
146
+ if (Array.isArray(value)) {
147
+ for (const item of value)
148
+ collectRefName(item, out);
149
+ return;
150
+ }
151
+ const v = value;
152
+ if (typeof v.name === "string")
153
+ out.add(v.name);
154
+ if (typeof v.source === "string") {
155
+ const dot = v.source.lastIndexOf(".");
156
+ out.add(dot >= 0 ? v.source.slice(dot + 1) : v.source);
157
+ }
158
+ for (const wrapper of ["ref", "invoke"]) {
159
+ if (v[wrapper] !== undefined)
160
+ collectRefName(v[wrapper], out);
161
+ }
162
+ }
163
+ /**
164
+ * Index every resource a CEL `resources.…` read can name: the module's own
165
+ * top-level resources, the ones declared inside `x-telo-scope` slots (a
166
+ * `Run.Sequence`'s `with:`), and each import's exported instances — keyed
167
+ * `<Alias>.<name>`, the two-level shape those publish under.
168
+ *
169
+ * Scope slots are found through the declaring kind's schema annotation, not by
170
+ * field name, so any composer with a scope participates.
171
+ */
172
+ export function buildObservedStateIndex(manifests, defs, aliases, scopes) {
173
+ const out = new Map();
174
+ const resolve = moduleScopedDefResolver(defs, aliases, scopes);
175
+ /** `module` is the resource's DECLARING module: an exported instance is
176
+ * written with that library's aliases (`kind: Self.Listener`), which the
177
+ * consumer's table cannot resolve. */
178
+ const record = (kind, key, scoped, module) => {
179
+ const status = effectiveStatusSchema(resolve.in(kind, module), resolve);
180
+ out.set(key, { kind, ...(status ? { status } : {}), ...(scoped ? { scoped } : {}) });
181
+ };
182
+ for (const manifest of manifests) {
183
+ const kind = manifest.kind;
184
+ const name = manifest.metadata?.name;
185
+ if (!kind || SYSTEM_KINDS.has(kind))
186
+ continue;
187
+ if (name)
188
+ record(kind, name, false, manifest.metadata?.module);
189
+ const schema = resolve(kind)?.schema;
190
+ if (!schema)
191
+ continue;
192
+ for (const [path, entry] of buildReferenceFieldMap(schema)) {
193
+ if (!isScopeEntry(entry))
194
+ continue;
195
+ for (const value of resolveFieldValues(manifest, path)) {
196
+ for (const scopedEntry of Array.isArray(value) ? value : [value]) {
197
+ const scopedKind = scopedEntry?.kind;
198
+ const scopedName = scopedEntry?.metadata?.name;
199
+ if (typeof scopedKind === "string" && typeof scopedName === "string") {
200
+ record(scopedKind, scopedName, true);
201
+ }
202
+ }
203
+ }
204
+ }
205
+ }
206
+ for (const [alias, name, kind, module] of importedExports(manifests, aliases)) {
207
+ record(kind, `${alias}.${name}`, false, module);
208
+ }
209
+ return out;
210
+ }
211
+ /**
212
+ * Every `<alias, exported name, kind>` an import makes readable as
213
+ * `resources.<Alias>.<name>`. The importer's `Telo.Import` docs give the
214
+ * aliases; the exported instances are the ones already stamped
215
+ * `metadata.forwardedExport` by `selectModuleManifestsForAnalysis` — the module
216
+ * doc that declared `exports.resources` is dropped for non-root modules, so the
217
+ * stamp, not the declaration, is what survives into the consumer's manifest
218
+ * list. A module doc is still consulted when one IS present (a single-library
219
+ * analysis, the editor's projection).
220
+ */
221
+ function* importedExports(manifests, aliases) {
222
+ if (!aliases?.moduleForAlias)
223
+ return;
224
+ const declaredByModule = new Map();
225
+ for (const m of manifests) {
226
+ if (m.kind !== "Telo.Library")
227
+ continue;
228
+ const libName = (m.metadata?.name ?? m.metadata?.module);
229
+ const declared = m.exports?.resources;
230
+ if (!libName || !Array.isArray(declared))
231
+ continue;
232
+ declaredByModule.set(libName, new Set(declared
233
+ .filter((e) => typeof e === "string")
234
+ .map((e) => parseExportEntry(e).name)));
235
+ }
236
+ const exportsByModule = new Map();
237
+ for (const m of manifests) {
238
+ const module = m.metadata?.module;
239
+ const name = m.metadata?.name;
240
+ if (!module || !name || SYSTEM_KINDS.has(m.kind))
241
+ continue;
242
+ const forwarded = m.metadata?.forwardedExport;
243
+ if (!forwarded && !declaredByModule.get(module)?.has(name))
244
+ continue;
245
+ let byName = exportsByModule.get(module);
246
+ if (!byName)
247
+ exportsByModule.set(module, (byName = new Map()));
248
+ byName.set(name, m.kind);
249
+ }
250
+ for (const m of manifests) {
251
+ if (m.kind !== "Telo.Import")
252
+ continue;
253
+ const alias = m.metadata?.name;
254
+ if (!alias)
255
+ continue;
256
+ const targetModule = aliases.moduleForAlias(alias);
257
+ const exported = targetModule && exportsByModule.get(targetModule);
258
+ if (!exported)
259
+ continue;
260
+ for (const [name, kind] of exported)
261
+ yield [alias, name, kind, targetModule];
262
+ }
263
+ }
264
+ /** The `resources` node of a CEL context schema: one entry per resource, each
265
+ * open except for a typed, closed `status` node on kinds that declare one.
266
+ * `open` keeps the map itself permissive, so unknown resource names and every
267
+ * flat field pass exactly as they do today. */
268
+ export function buildObservedStateResourcesSchema(index, open) {
269
+ const properties = {};
270
+ for (const [key, { status }] of index) {
271
+ if (!status)
272
+ continue;
273
+ applyObservedStateNode(properties, key, status);
274
+ }
275
+ return open
276
+ ? { type: "object", additionalProperties: true, properties }
277
+ : { type: "object", properties };
278
+ }
279
+ /**
280
+ * Write the typed `status` node for one index key into a `resources` property
281
+ * map. A dotted key (`Alias.name`) is an import's exported instance, which
282
+ * publishes two levels deep — the alias node stays open so every other name
283
+ * under it keeps resolving as it does today.
284
+ */
285
+ export function applyObservedStateNode(properties, key, status) {
286
+ const dot = key.indexOf(".");
287
+ const leaf = {
288
+ type: "object",
289
+ additionalProperties: true,
290
+ properties: { [OBSERVED_STATE_KEY]: { ...status, additionalProperties: false } },
291
+ };
292
+ if (dot < 0) {
293
+ properties[key] = leaf;
294
+ return;
295
+ }
296
+ const alias = key.slice(0, dot);
297
+ const name = key.slice(dot + 1);
298
+ const aliasNode = (properties[alias] ??= {
299
+ type: "object",
300
+ additionalProperties: true,
301
+ properties: {},
302
+ });
303
+ (aliasNode.properties ??= {})[name] = leaf;
304
+ }
@@ -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;AAgD/F;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,kBAAkB,CAChC,SAAS,EAAE,gBAAgB,EAAE,EAC7B,OAAO,EAAE,eAAe,GACvB,kBAAkB,EAAE,CA6dtB"}
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;AAgD/F;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,kBAAkB,CAChC,SAAS,EAAE,gBAAgB,EAAE,EAC7B,OAAO,EAAE,eAAe,GACvB,kBAAkB,EAAE,CAmetB"}
@@ -254,8 +254,14 @@ export function validateReferences(resources, context) {
254
254
  }
255
255
  // Local reference (bare name or explicit `Self.`-qualified).
256
256
  const localName = aliasPrefix === "Self" ? refName.slice(dot + 1) : refName;
257
- const target = byName.get(localName) ??
258
- visibleScopeManifests.find((m) => m.metadata?.name === localName);
257
+ // Scope-local FIRST, enclosing module as the fallback — the order the
258
+ // runtime uses at every name-resolution site (`ScopeContext.getInstance`,
259
+ // `ResourceContext.resolveRef`, and the CEL `resources` layering). Module-first
260
+ // here would validate a shadowed name against the resource the kernel will
261
+ // never bind: a false pass when the outer kind fits and the scoped one does
262
+ // not, a false REFERENCE_KIND_MISMATCH when it is the other way round.
263
+ const target = visibleScopeManifests.find((m) => m.metadata?.name === localName) ??
264
+ byName.get(localName);
259
265
  if (!target) {
260
266
  diagnostics.push({
261
267
  severity: DiagnosticSeverity.Error,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@telorun/analyzer",
3
- "version": "0.45.0",
3
+ "version": "0.47.0",
4
4
  "description": "Telo Analyzer - Static manifest validator for Telo manifests.",
5
5
  "keywords": [
6
6
  "telo",
@@ -48,7 +48,7 @@
48
48
  "@types/node": "^20.0.0",
49
49
  "typescript": "^5.0.0",
50
50
  "vitest": "^2.1.8",
51
- "@telorun/sdk": "0.56.0"
51
+ "@telorun/sdk": "0.59.0"
52
52
  },
53
53
  "peerDependencies": {
54
54
  "@telorun/sdk": "*"
@@ -121,3 +121,61 @@ export function scopeResolverForModule(
121
121
  ? aliasesByModule.get(ownModule)
122
122
  : undefined;
123
123
  }
124
+
125
+ /** Per-declaring-module alias tables plus the set of root (consumer-owned)
126
+ * modules. The shape `StaticAnalyzer.analyze` already threads through its
127
+ * passes, and what {@link moduleScopedDefResolver} needs to re-scope. */
128
+ export interface ModuleScopes {
129
+ aliasesByModule: ReadonlyMap<string, { resolveKind(kind: string): string | undefined }>;
130
+ rootModules: ReadonlySet<string>;
131
+ }
132
+
133
+ /** Minimal view of the definition registry a kind lookup needs. */
134
+ export interface DefinitionLookup {
135
+ resolve(kind: string): unknown;
136
+ }
137
+
138
+ /**
139
+ * Resolve a kind to its definition **in the scope that declared it**.
140
+ *
141
+ * An `extends` alias belongs to the file it was written in, not to whoever is
142
+ * reading it: a consumer that imports only a backend (the sanctioned "one import
143
+ * instead of two") has no alias for the abstract's library, so folding an
144
+ * inheritance chain with the consumer's table silently stops at the first hop
145
+ * and yields an un-merged schema. `from` — the definition the kind was read off —
146
+ * carries `metadata.module`, which is the scope to resolve in; a chain crossing
147
+ * several modules re-scopes at every hop.
148
+ *
149
+ * `resolveIn` takes the module explicitly, for the top-level lookup where there
150
+ * is no `from` yet (an exported instance's `kind: Self.X` is written in the
151
+ * exporting library's scope, which the consumer's table cannot resolve either).
152
+ *
153
+ * The runtime counterpart is `resource-definition-controller`, which resolves
154
+ * against the defining module context and stamps the result; keeping both on the
155
+ * same rule is what stops `telo check` and the kernel from disagreeing about
156
+ * which inherited fields a child kind may set.
157
+ */
158
+ export function moduleScopedDefResolver<T>(
159
+ defs: { resolve(kind: string): T | undefined },
160
+ aliases?: { resolveKind(kind: string): string | undefined },
161
+ scopes?: ModuleScopes,
162
+ ): {
163
+ (kind: string, from?: { metadata?: { module?: string } }): T | undefined;
164
+ in(kind: string, module?: string): T | undefined;
165
+ } {
166
+ const resolveIn = (kind: string, module?: string): T | undefined => {
167
+ const scoped =
168
+ module && scopes && !scopes.rootModules.has(module)
169
+ ? scopes.aliasesByModule.get(module)
170
+ : undefined;
171
+ return (
172
+ (scoped ? defs.resolve(scoped.resolveKind(kind) ?? kind) : undefined) ??
173
+ defs.resolve(aliases?.resolveKind(kind) ?? kind) ??
174
+ defs.resolve(kind)
175
+ );
176
+ };
177
+ const resolver = ((kind: string, from?: { metadata?: { module?: string } }) =>
178
+ resolveIn(kind, from?.metadata?.module)) as ReturnType<typeof moduleScopedDefResolver<T>>;
179
+ resolver.in = resolveIn;
180
+ return resolver;
181
+ }