@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.
- package/dist/alias-resolver.d.ts +45 -0
- package/dist/alias-resolver.d.ts.map +1 -1
- package/dist/alias-resolver.js +33 -0
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/analyzer.js +106 -5
- package/dist/builtins.d.ts.map +1 -1
- package/dist/builtins.js +3 -0
- package/dist/extends-resolution.d.ts +19 -2
- package/dist/extends-resolution.d.ts.map +1 -1
- package/dist/extends-resolution.js +25 -1
- package/dist/index.d.ts +5 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/kernel-globals.d.ts +9 -1
- package/dist/kernel-globals.d.ts.map +1 -1
- package/dist/kernel-globals.js +24 -1
- package/dist/resolve-ref-sentinels.d.ts +13 -1
- package/dist/resolve-ref-sentinels.d.ts.map +1 -1
- package/dist/resolve-ref-sentinels.js +56 -5
- package/dist/validate-cel-context.d.ts.map +1 -1
- package/dist/validate-cel-context.js +17 -1
- package/dist/validate-observed-state.d.ts +98 -0
- package/dist/validate-observed-state.d.ts.map +1 -0
- package/dist/validate-observed-state.js +304 -0
- package/dist/validate-references.d.ts.map +1 -1
- package/dist/validate-references.js +8 -2
- package/package.json +2 -2
- package/src/alias-resolver.ts +58 -0
- package/src/analyzer.ts +126 -4
- package/src/builtins.ts +3 -0
- package/src/extends-resolution.ts +37 -3
- package/src/index.ts +13 -0
- package/src/kernel-globals.ts +20 -0
- package/src/resolve-ref-sentinels.ts +68 -4
- package/src/validate-cel-context.ts +20 -1
- package/src/validate-observed-state.ts +354 -0
- package/src/validate-references.ts +8 -2
package/src/analyzer.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ResourceDefinition, ResourceManifest } from "@telorun/sdk";
|
|
2
|
-
import { canonicalTypeSchemaId } from "@telorun/sdk";
|
|
2
|
+
import { canonicalTypeSchemaId, OBSERVED_STATE_KEY } from "@telorun/sdk";
|
|
3
3
|
import type { Environment } from "@marcbachmann/cel-js";
|
|
4
4
|
import { defaultRegistry, isRefSentinel, isTaggedSentinel } from "@telorun/templating";
|
|
5
5
|
import { AliasResolver, scopeResolverForModule } from "./alias-resolver.js";
|
|
@@ -14,6 +14,13 @@ import { DefinitionRegistry } from "./definition-registry.js";
|
|
|
14
14
|
import { effectiveAuthorSchema } from "./extends-resolution.js";
|
|
15
15
|
import { buildDependencyGraph, formatCycle } from "./dependency-graph.js";
|
|
16
16
|
import { buildKernelGlobalsSchema, mergeKernelGlobalsIntoContext } from "./kernel-globals.js";
|
|
17
|
+
import {
|
|
18
|
+
buildObservedStateIndex,
|
|
19
|
+
buildObservedStateResourcesSchema,
|
|
20
|
+
collectRunReachableNames,
|
|
21
|
+
observedStateRead,
|
|
22
|
+
validateObservedStateDeclarations,
|
|
23
|
+
} from "./validate-observed-state.js";
|
|
17
24
|
import { computeSuggestKind } from "./kind-suggest.js";
|
|
18
25
|
import { visitManifest } from "./manifest-visitor.js";
|
|
19
26
|
import { isModuleKind } from "./module-kinds.js";
|
|
@@ -33,6 +40,7 @@ import {
|
|
|
33
40
|
import { collectValueSchemaIssues } from "./validate-value-schema.js";
|
|
34
41
|
import { DiagnosticSeverity, type AnalysisDiagnostic, type AnalysisOptions } from "./types.js";
|
|
35
42
|
import {
|
|
43
|
+
extractAccessChains,
|
|
36
44
|
extractCelRegionScopes,
|
|
37
45
|
extractContextsFromSchema,
|
|
38
46
|
getManifestItem,
|
|
@@ -717,6 +725,16 @@ function errorContextForPath(
|
|
|
717
725
|
return best?.schema;
|
|
718
726
|
}
|
|
719
727
|
|
|
728
|
+
/** Member-access chains in a CEL expression, or none when it doesn't parse.
|
|
729
|
+
* Best-effort: a syntax error is reported by the engine pass, not here. */
|
|
730
|
+
function celAccessChains(env: Environment, expr: string): string[][] {
|
|
731
|
+
try {
|
|
732
|
+
return extractAccessChains(env.parse(expr).ast);
|
|
733
|
+
} catch {
|
|
734
|
+
return [];
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
|
|
720
738
|
const CEL_PURE_RE = /^\s*\$\{\{[^}]*\}\}\s*$/;
|
|
721
739
|
const CEL_EXPR_RE = /\$\{\{\s*([^}]+?)\s*\}\}/;
|
|
722
740
|
|
|
@@ -1135,7 +1153,7 @@ export class StaticAnalyzer {
|
|
|
1135
1153
|
// {kind, name} objects so downstream phases (validation, dependency graph,
|
|
1136
1154
|
// kernel controllers) see a uniform shape. Runs after normalize so both
|
|
1137
1155
|
// original and inline-extracted manifests have their sentinels resolved.
|
|
1138
|
-
resolveRefSentinels(allManifests, aliases, aliasesByModule);
|
|
1156
|
+
resolveRefSentinels(allManifests, aliases, aliasesByModule, [], defs);
|
|
1139
1157
|
|
|
1140
1158
|
// Phase 2.6: register each named `Telo.Type` resource's schema under its
|
|
1141
1159
|
// canonical module-scoped id (`telo://<module>/<name>`), validate
|
|
@@ -1280,9 +1298,38 @@ export class StaticAnalyzer {
|
|
|
1280
1298
|
}
|
|
1281
1299
|
}
|
|
1282
1300
|
|
|
1301
|
+
// What each resource reports while running (`status:`), and which resources
|
|
1302
|
+
// some slot can actually start. Both feed the observed-state checks below.
|
|
1303
|
+
// A RESOURCE's kind is written in the module that declares it and is never
|
|
1304
|
+
// canonicalized (unlike a definition's `extends`, normalized at registration
|
|
1305
|
+
// above), so an exported instance's `kind: Self.X` only resolves in its own
|
|
1306
|
+
// library's scope.
|
|
1307
|
+
const moduleScopes = { aliasesByModule, rootModules };
|
|
1308
|
+
|
|
1309
|
+
const observedState = buildObservedStateIndex(allManifests, defs, aliases, moduleScopes);
|
|
1310
|
+
const reportsObservedState = [...observedState.values()].some((r) => r.status);
|
|
1311
|
+
const runReachable = reportsObservedState
|
|
1312
|
+
? collectRunReachableNames(allManifests, defs, aliases)
|
|
1313
|
+
: new Set<string>();
|
|
1314
|
+
|
|
1283
1315
|
// Build typed kernel globals schema so x-telo-context chain validation
|
|
1284
1316
|
// recognises variables, secrets, resources, env automatically
|
|
1285
|
-
const kernelGlobals = buildKernelGlobalsSchema(allManifests);
|
|
1317
|
+
const kernelGlobals = buildKernelGlobalsSchema(allManifests, observedState);
|
|
1318
|
+
|
|
1319
|
+
// Fallback context for CEL in a slot with no `x-telo-context` annotation:
|
|
1320
|
+
// everything stays open except the typed `.status` nodes, so unknown-field
|
|
1321
|
+
// checking reaches observed state everywhere without newly rejecting any
|
|
1322
|
+
// read that passes today.
|
|
1323
|
+
const observedStateContext: Record<string, any> | null =
|
|
1324
|
+
reportsObservedState
|
|
1325
|
+
? {
|
|
1326
|
+
type: "object",
|
|
1327
|
+
additionalProperties: true,
|
|
1328
|
+
properties: {
|
|
1329
|
+
resources: buildObservedStateResourcesSchema(observedState, true),
|
|
1330
|
+
},
|
|
1331
|
+
}
|
|
1332
|
+
: null;
|
|
1286
1333
|
|
|
1287
1334
|
// The module doc (Application/Library) carries the Application-only `ports`
|
|
1288
1335
|
// namespace; threaded into per-resource CEL typing so `${{ ports.X }}`
|
|
@@ -1600,6 +1647,9 @@ export class StaticAnalyzer {
|
|
|
1600
1647
|
// `x-telo-step-context` / `x-telo-error-context` scopes. A `!cel` outside
|
|
1601
1648
|
// every region is read as a literal — the runtime never evaluates it.
|
|
1602
1649
|
let celEvalPaths: string[] = [];
|
|
1650
|
+
// The compile half alone: a field that resolves at startup, where observed
|
|
1651
|
+
// state cannot exist yet.
|
|
1652
|
+
let celCompilePaths: string[] = [];
|
|
1603
1653
|
let celRegionScopes: string[] = [];
|
|
1604
1654
|
let celRuleApplies = false;
|
|
1605
1655
|
|
|
@@ -1639,9 +1689,14 @@ export class StaticAnalyzer {
|
|
|
1639
1689
|
? buildEvalPaths(capabilityDef.schema as Record<string, any>)
|
|
1640
1690
|
: { compile: [], runtime: [] };
|
|
1641
1691
|
celEvalPaths = [...own.compile, ...own.runtime, ...parent.compile, ...parent.runtime];
|
|
1692
|
+
// A `Telo.Provider`'s fields are implicitly compile-eval — the
|
|
1693
|
+
// capability abstract carries the root annotation — so its reads are
|
|
1694
|
+
// covered here without the provider restating anything.
|
|
1695
|
+
celCompilePaths = [...own.compile, ...parent.compile];
|
|
1642
1696
|
celRegionScopes = extractCelRegionScopes(ownSchema);
|
|
1643
1697
|
} else {
|
|
1644
1698
|
celEvalPaths = [];
|
|
1699
|
+
celCompilePaths = [];
|
|
1645
1700
|
celRegionScopes = [];
|
|
1646
1701
|
}
|
|
1647
1702
|
},
|
|
@@ -1674,6 +1729,44 @@ export class StaticAnalyzer {
|
|
|
1674
1729
|
return;
|
|
1675
1730
|
}
|
|
1676
1731
|
|
|
1732
|
+
// Observed state exists only while the application runs, so a path
|
|
1733
|
+
// through `.status` is illegal in a field that resolves at startup —
|
|
1734
|
+
// and a resource nothing can start reports nothing, ever. Both are
|
|
1735
|
+
// decided from the expression and the manifest alone.
|
|
1736
|
+
if (reportsObservedState && engineName === "cel" && expr.includes(OBSERVED_STATE_KEY)) {
|
|
1737
|
+
for (const chain of celAccessChains(this.celEnv, expr)) {
|
|
1738
|
+
const read = observedStateRead(chain);
|
|
1739
|
+
if (!read) continue;
|
|
1740
|
+
// An import's exported instance is indexed under `<Alias>.<name>`,
|
|
1741
|
+
// the two-level shape it publishes under, so a cross-module read
|
|
1742
|
+
// is checked exactly like a local one.
|
|
1743
|
+
const reported = observedState.get(
|
|
1744
|
+
read.alias ? `${read.alias}.${read.name}` : read.name,
|
|
1745
|
+
);
|
|
1746
|
+
|
|
1747
|
+
if (celRuleApplies && evalPathsCover(celCompilePaths, path)) {
|
|
1748
|
+
diagnostics.push({
|
|
1749
|
+
severity: DiagnosticSeverity.Error,
|
|
1750
|
+
code: "OBSERVED_STATE_IN_STARTUP_FIELD",
|
|
1751
|
+
source: SOURCE,
|
|
1752
|
+
message: `${m.kind}/${resource.name}: '${path}' is resolved once at startup, so '${chain.join(".")}' does not exist yet — '${read.name}' reports it only while the application is running. Read reported values where the call happens: a step's inputs:, a request's url, a route handler, or a returns: expression.`,
|
|
1753
|
+
data: { resource, filePath, path },
|
|
1754
|
+
});
|
|
1755
|
+
continue;
|
|
1756
|
+
}
|
|
1757
|
+
|
|
1758
|
+
if (reported && !runReachable.has(read.name)) {
|
|
1759
|
+
diagnostics.push({
|
|
1760
|
+
severity: DiagnosticSeverity.Error,
|
|
1761
|
+
code: "OBSERVED_STATE_NEVER_RUN",
|
|
1762
|
+
source: SOURCE,
|
|
1763
|
+
message: `${m.kind}/${resource.name}: '${read.name}' reports '${read.field ?? OBSERVED_STATE_KEY}' only while it is running, and nothing starts it. Add '!ref ${read.name}' to a targets: list, or invoke it from a step.`,
|
|
1764
|
+
data: { resource, filePath, path },
|
|
1765
|
+
});
|
|
1766
|
+
}
|
|
1767
|
+
}
|
|
1768
|
+
}
|
|
1769
|
+
|
|
1677
1770
|
let matchedContext: Record<string, any> | undefined =
|
|
1678
1771
|
e.contextSchema ?? celInvocationContext;
|
|
1679
1772
|
|
|
@@ -1723,6 +1816,12 @@ export class StaticAnalyzer {
|
|
|
1723
1816
|
allManifests: allManifests as Record<string, any>[],
|
|
1724
1817
|
});
|
|
1725
1818
|
effectiveContext = mergeKernelGlobalsIntoContext(resolvedContext, kernelGlobals);
|
|
1819
|
+
} else if (observedStateContext) {
|
|
1820
|
+
// No `x-telo-context` matched, so nothing was chain-validated here
|
|
1821
|
+
// before. Validate the observed-state segment alone rather than
|
|
1822
|
+
// merging the kernel globals, whose closed `variables` / `ports`
|
|
1823
|
+
// nodes would newly reject reads that pass today.
|
|
1824
|
+
effectiveContext = observedStateContext;
|
|
1726
1825
|
}
|
|
1727
1826
|
|
|
1728
1827
|
const engine = defaultRegistry().get(engineName);
|
|
@@ -1792,6 +1891,23 @@ export class StaticAnalyzer {
|
|
|
1792
1891
|
// kind-instead-of-instance ref there is caught statically, not at runtime.
|
|
1793
1892
|
diagnostics.push(...validateStepInvokeReferences(allManifests, defs, aliases));
|
|
1794
1893
|
|
|
1894
|
+
// `required:` inside a `status:` block — reported here rather than by the
|
|
1895
|
+
// AJV shape, which could only say "must NOT be valid" without naming the
|
|
1896
|
+
// rule or the fix.
|
|
1897
|
+
for (const issue of validateObservedStateDeclarations(allManifests)) {
|
|
1898
|
+
diagnostics.push({
|
|
1899
|
+
severity: DiagnosticSeverity.Error,
|
|
1900
|
+
code: "OBSERVED_STATE_REQUIRED_FORBIDDEN",
|
|
1901
|
+
source: SOURCE,
|
|
1902
|
+
message: issue.message,
|
|
1903
|
+
data: {
|
|
1904
|
+
resource: { kind: issue.kind, name: issue.name },
|
|
1905
|
+
filePath: issue.filePath,
|
|
1906
|
+
path: "status.required",
|
|
1907
|
+
},
|
|
1908
|
+
});
|
|
1909
|
+
}
|
|
1910
|
+
|
|
1795
1911
|
// Validate `extends` fields and flag legacy `capability: <UserAbstract>` overload.
|
|
1796
1912
|
diagnostics.push(...validateExtends(allManifests, defs, aliases));
|
|
1797
1913
|
|
|
@@ -1841,7 +1957,13 @@ export class StaticAnalyzer {
|
|
|
1841
1957
|
// Resolve !ref sentinels after normalize so both the original and
|
|
1842
1958
|
// inline-extracted manifests get their refs canonicalized to
|
|
1843
1959
|
// {kind, name} for the kernel that consumes this output.
|
|
1844
|
-
resolveRefSentinels(
|
|
1960
|
+
resolveRefSentinels(
|
|
1961
|
+
normalized,
|
|
1962
|
+
ctx.aliases,
|
|
1963
|
+
ctx.aliasesByModule,
|
|
1964
|
+
crossModuleTargets ?? [],
|
|
1965
|
+
ctx.definitions!,
|
|
1966
|
+
);
|
|
1845
1967
|
// Canonicalize import-scoped schema `$ref`s (`telo://Self|Alias/Type`) so the
|
|
1846
1968
|
// kernel that executes this output compiles inputs/outputs against the same
|
|
1847
1969
|
// ids the type controllers register their schemas under.
|
package/src/builtins.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ResourceDefinition } from "@telorun/sdk";
|
|
2
|
+
import { OBSERVED_STATE_SCHEMA } from "./validate-observed-state.js";
|
|
2
3
|
|
|
3
4
|
/** Descriptive provenance a module declares about itself, shared by
|
|
4
5
|
* `Telo.Application` and `Telo.Library`.
|
|
@@ -186,6 +187,7 @@ export const KERNEL_BUILTINS: ResourceDefinition[] = [
|
|
|
186
187
|
},
|
|
187
188
|
capability: { type: "string" },
|
|
188
189
|
schema: { type: "object", additionalProperties: true },
|
|
190
|
+
status: OBSERVED_STATE_SCHEMA,
|
|
189
191
|
},
|
|
190
192
|
required: ["metadata"],
|
|
191
193
|
// Telo.Abstract is an extension point by design — it must accept forward-compatible
|
|
@@ -214,6 +216,7 @@ export const KERNEL_BUILTINS: ResourceDefinition[] = [
|
|
|
214
216
|
type: "object",
|
|
215
217
|
additionalProperties: true,
|
|
216
218
|
properties: {
|
|
219
|
+
status: OBSERVED_STATE_SCHEMA,
|
|
217
220
|
resources: {
|
|
218
221
|
type: "array",
|
|
219
222
|
items: {
|
|
@@ -2,8 +2,17 @@ import type { ResourceDefinition } from "@telorun/sdk";
|
|
|
2
2
|
import { mergeTypeSchemas } from "@telorun/sdk";
|
|
3
3
|
|
|
4
4
|
/** Resolves a kind string (canonical or alias form, depending on the caller's
|
|
5
|
-
* registry) to its `Telo.Definition` / `Telo.Abstract`, or undefined.
|
|
6
|
-
|
|
5
|
+
* registry) to its `Telo.Definition` / `Telo.Abstract`, or undefined.
|
|
6
|
+
*
|
|
7
|
+
* `from` is the definition the kind was read off. An `extends` alias belongs to
|
|
8
|
+
* the file that DECLARES the definition, not to whoever is reading it, so a
|
|
9
|
+
* resolver that walks an inheritance chain across module boundaries must
|
|
10
|
+
* re-scope at each hop — `from.metadata.module` is what it scopes to. Resolvers
|
|
11
|
+
* that operate in a single scope ignore the parameter. */
|
|
12
|
+
export type DefResolver = (
|
|
13
|
+
kind: string,
|
|
14
|
+
from?: ResourceDefinition,
|
|
15
|
+
) => ResourceDefinition | undefined;
|
|
7
16
|
|
|
8
17
|
/** The template-body / controller fields a definition may carry. Kept local
|
|
9
18
|
* because `ResourceDefinition` intentionally types only the stable surface;
|
|
@@ -19,6 +28,7 @@ interface DefinitionBody {
|
|
|
19
28
|
resources?: unknown[];
|
|
20
29
|
base?: Record<string, unknown>;
|
|
21
30
|
schema?: Record<string, any>;
|
|
31
|
+
status?: Record<string, any>;
|
|
22
32
|
}
|
|
23
33
|
|
|
24
34
|
const body = (def: ResourceDefinition | undefined): DefinitionBody =>
|
|
@@ -32,7 +42,7 @@ export function resolveParent(
|
|
|
32
42
|
): ResourceDefinition | undefined {
|
|
33
43
|
const ext = body(def).extends;
|
|
34
44
|
if (typeof ext !== "string" || ext.length === 0) return undefined;
|
|
35
|
-
return resolve(ext);
|
|
45
|
+
return resolve(ext, def);
|
|
36
46
|
}
|
|
37
47
|
|
|
38
48
|
/** The `extends` ancestor chain, nearest-first, excluding `def` itself.
|
|
@@ -122,3 +132,27 @@ export function effectiveAuthorSchema(
|
|
|
122
132
|
const parentSchema = effectiveAuthorSchema(parent, resolve);
|
|
123
133
|
return mergeTypeSchemas([parentSchema, own]) as Record<string, any>;
|
|
124
134
|
}
|
|
135
|
+
|
|
136
|
+
/** The observed state a kind reports (`status:`), folded through `extends`:
|
|
137
|
+
* - with `base:` present → the **parent's** effective status unchanged; the
|
|
138
|
+
* child delegates to the parent's controller and *is* a parent instance, so
|
|
139
|
+
* it publishes exactly what the parent publishes.
|
|
140
|
+
* - without `base:` but with `extends` → `merge(parent-effective, own)`, so a
|
|
141
|
+
* contract can mandate what its implementations report and an implementation
|
|
142
|
+
* can add to it.
|
|
143
|
+
* - no `extends` → the own block unchanged.
|
|
144
|
+
* Undefined when nothing in the chain declares one — the signal that the kind
|
|
145
|
+
* has not opted into typed `.status` reads. */
|
|
146
|
+
export function effectiveStatusSchema(
|
|
147
|
+
def: ResourceDefinition | undefined,
|
|
148
|
+
resolve: DefResolver,
|
|
149
|
+
): Record<string, any> | undefined {
|
|
150
|
+
const own = body(def).status;
|
|
151
|
+
const parent = resolveParent(def, resolve);
|
|
152
|
+
if (!parent) return own;
|
|
153
|
+
const parentStatus = effectiveStatusSchema(parent, resolve);
|
|
154
|
+
if (body(def).base) return parentStatus;
|
|
155
|
+
if (!parentStatus) return own;
|
|
156
|
+
if (!own) return parentStatus;
|
|
157
|
+
return mergeTypeSchemas([parentStatus, own]) as Record<string, any>;
|
|
158
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -24,10 +24,23 @@ export {
|
|
|
24
24
|
type ReExportSpec,
|
|
25
25
|
} from "./flatten-for-analyzer.js";
|
|
26
26
|
export { buildEvalPaths, evalPathCovers } from "./eval-paths.js";
|
|
27
|
+
export {
|
|
28
|
+
applyObservedStateNode,
|
|
29
|
+
buildObservedStateIndex,
|
|
30
|
+
buildObservedStateResourcesSchema,
|
|
31
|
+
collectRunReachableNames,
|
|
32
|
+
observedStateRead,
|
|
33
|
+
validateObservedStateDeclarations,
|
|
34
|
+
OBSERVED_STATE_SCHEMA,
|
|
35
|
+
} from "./validate-observed-state.js";
|
|
36
|
+
export type { AnalyzedResource, ObservedStateRead } from "./validate-observed-state.js";
|
|
37
|
+
export { moduleScopedDefResolver, scopeResolverForModule } from "./alias-resolver.js";
|
|
38
|
+
export type { ModuleScopes } from "./alias-resolver.js";
|
|
27
39
|
export {
|
|
28
40
|
ancestorChain,
|
|
29
41
|
controllerBearingAncestor,
|
|
30
42
|
effectiveAuthorSchema,
|
|
43
|
+
effectiveStatusSchema,
|
|
31
44
|
hasOwnControllerOrTemplate,
|
|
32
45
|
inheritedCapability,
|
|
33
46
|
isInheritedDelegation,
|
package/src/kernel-globals.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ResourceManifest } from "@telorun/sdk";
|
|
2
2
|
import { residualEntrySchemaMap } from "./residual-schema.js";
|
|
3
|
+
import { applyObservedStateNode } from "./validate-observed-state.js";
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Kernel global names available in every CEL evaluation context at runtime.
|
|
@@ -36,6 +37,11 @@ const SYSTEM_KINDS = new Set([
|
|
|
36
37
|
*/
|
|
37
38
|
export function buildKernelGlobalsSchema(
|
|
38
39
|
manifests: ResourceManifest[],
|
|
40
|
+
/** Every resource a CEL read can name, including scope-declared ones (see
|
|
41
|
+
* `buildObservedStateIndex`). Kinds that declare a `status:` get a typed,
|
|
42
|
+
* closed `status` node; every other resource node stays open, so no flat read
|
|
43
|
+
* that passes today can start failing. */
|
|
44
|
+
resources?: ReadonlyMap<string, { kind: string; status?: Record<string, any> }>,
|
|
39
45
|
): Record<string, any> {
|
|
40
46
|
const moduleManifest =
|
|
41
47
|
(manifests.find((m) => m.kind === "Telo.Application") as
|
|
@@ -55,6 +61,20 @@ export function buildKernelGlobalsSchema(
|
|
|
55
61
|
resourceProps[name] = { type: "object", additionalProperties: true };
|
|
56
62
|
}
|
|
57
63
|
}
|
|
64
|
+
// Scope-declared resources (a `Run.Sequence`'s `with:`) publish like any other
|
|
65
|
+
// now, so their names resolve too — inside the scope's regions, which is where
|
|
66
|
+
// the only expressions that can name them live.
|
|
67
|
+
for (const [key, entry] of resources ?? []) {
|
|
68
|
+
if (key.includes(".")) continue;
|
|
69
|
+
resourceProps[key] ??= { type: "object", additionalProperties: true };
|
|
70
|
+
if (entry.status) applyObservedStateNode(resourceProps, key, entry.status);
|
|
71
|
+
}
|
|
72
|
+
// Imports' exported instances publish two levels deep (`resources.<Alias>.<name>`);
|
|
73
|
+
// the alias node stays open so its other keys keep resolving.
|
|
74
|
+
for (const [key, entry] of resources ?? []) {
|
|
75
|
+
if (!key.includes(".") || !entry.status) continue;
|
|
76
|
+
applyObservedStateNode(resourceProps, key, entry.status);
|
|
77
|
+
}
|
|
58
78
|
|
|
59
79
|
return {
|
|
60
80
|
type: "object",
|
|
@@ -1,8 +1,22 @@
|
|
|
1
1
|
import type { ResourceManifest } from "@telorun/sdk";
|
|
2
2
|
import { isRefSentinel, isTaggedSentinel } from "@telorun/templating";
|
|
3
3
|
import type { AliasResolver } from "./alias-resolver.js";
|
|
4
|
+
import {
|
|
5
|
+
isScopeEntry,
|
|
6
|
+
resolveFieldEntries,
|
|
7
|
+
type ReferenceFieldMap,
|
|
8
|
+
} from "./reference-field-map.js";
|
|
4
9
|
import { REF_RESOLUTION_SKIP_KINDS as SYSTEM_KINDS } from "./system-kinds.js";
|
|
5
10
|
|
|
11
|
+
/** The slice of the definition registry this pass needs: a kind's field map, from
|
|
12
|
+
* which the `x-telo-scope` slots are read. */
|
|
13
|
+
export interface ScopeFieldMapSource {
|
|
14
|
+
getFieldMapForKind(
|
|
15
|
+
kind: string,
|
|
16
|
+
aliases?: { resolveKind(k: string): string | undefined },
|
|
17
|
+
): ReferenceFieldMap | undefined;
|
|
18
|
+
}
|
|
19
|
+
|
|
6
20
|
/** Resolved ref shape written in place of a `!ref` sentinel. `alias` is set only for
|
|
7
21
|
* cross-module references (resolved into an imported library's exported instance). */
|
|
8
22
|
type ResolvedRef = { kind: string; name: string; alias?: string };
|
|
@@ -52,6 +66,10 @@ export function resolveRefSentinels(
|
|
|
52
66
|
// pass — which loads the entry module only — can still resolve `!ref Alias.name` against
|
|
53
67
|
// imported libraries' exported instances.
|
|
54
68
|
crossModuleTargets: ResourceManifest[] = [],
|
|
69
|
+
/** Supplies each kind's `x-telo-scope` slots. Without it a scoped name cannot be
|
|
70
|
+
* told from a module-level one, and a shadowed `!ref` resolves to the resource
|
|
71
|
+
* it shadows — so both call sites pass it. */
|
|
72
|
+
defs?: ScopeFieldMapSource,
|
|
55
73
|
): void {
|
|
56
74
|
const moduleOf = (r: ResourceManifest): string | undefined =>
|
|
57
75
|
(r.metadata as { module?: string } | undefined)?.module;
|
|
@@ -109,21 +127,67 @@ export function resolveRefSentinels(
|
|
|
109
127
|
return undefined;
|
|
110
128
|
};
|
|
111
129
|
|
|
130
|
+
/** Names a resource declares in its own execution scopes, read from the kind's
|
|
131
|
+
* `x-telo-scope` slots — the analyzer's single definition of "scope", shared
|
|
132
|
+
* with `manifest-visitor`. Inferring it structurally instead (any array of
|
|
133
|
+
* named inline manifests) would give scope-local shadowing to the first kind
|
|
134
|
+
* that happens to carry such an array without asking for it, and this pass is
|
|
135
|
+
* shared with the kernel, so the guess would be baked into the runtime tree
|
|
136
|
+
* rather than merely reported. */
|
|
137
|
+
const declaredInScopes = (
|
|
138
|
+
resource: ResourceManifest,
|
|
139
|
+
): Map<string, ResourceManifest> | undefined => {
|
|
140
|
+
const fieldMap = defs?.getFieldMapForKind(resource.kind, aliases);
|
|
141
|
+
if (!fieldMap) return undefined;
|
|
142
|
+
let declared: Map<string, ResourceManifest> | undefined;
|
|
143
|
+
for (const [fieldPath, entry] of fieldMap) {
|
|
144
|
+
if (!isScopeEntry(entry)) continue;
|
|
145
|
+
for (const { value } of resolveFieldEntries(resource, fieldPath)) {
|
|
146
|
+
for (const element of Array.isArray(value) ? value : [value]) {
|
|
147
|
+
if (!element || typeof element !== "object" || Array.isArray(element)) continue;
|
|
148
|
+
const manifest = element as ResourceManifest;
|
|
149
|
+
const name = (manifest.metadata as { name?: string } | undefined)?.name;
|
|
150
|
+
if (typeof manifest.kind === "string" && typeof name === "string") {
|
|
151
|
+
(declared ??= new Map()).set(name, manifest);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return declared;
|
|
157
|
+
};
|
|
158
|
+
|
|
112
159
|
// Resolve every `!ref` sentinel in the tree; leave opaque tagged / precompiled
|
|
113
160
|
// nodes (e.g. `!cel`) untouched and don't descend into them.
|
|
114
|
-
|
|
161
|
+
//
|
|
162
|
+
// `scoped` carries the names the enclosing resource declares in its `x-telo-scope`
|
|
163
|
+
// slots, and they SHADOW the module-level ones — the order the runtime resolves
|
|
164
|
+
// in. Baking the module-level kind into a shadowed reference would label traces
|
|
165
|
+
// and `getRefIdentity` with a resource that never runs.
|
|
166
|
+
const walk = (value: unknown, scoped?: Map<string, ResourceManifest>): unknown => {
|
|
115
167
|
if (isRefSentinel(value)) {
|
|
116
|
-
|
|
168
|
+
const source = value.source;
|
|
169
|
+
const bare = source.indexOf(".") === -1;
|
|
170
|
+
const shadow = bare ? scoped?.get(source) : undefined;
|
|
171
|
+
if (shadow) return { kind: shadow.kind as string, name: source };
|
|
172
|
+
return resolveTarget(source) ?? value;
|
|
117
173
|
}
|
|
118
174
|
if (value === null || typeof value !== "object") return value;
|
|
119
175
|
if (isTaggedSentinel(value)) return value;
|
|
120
176
|
if ((value as { __compiled?: unknown }).__compiled) return value;
|
|
121
177
|
if (Array.isArray(value)) {
|
|
122
|
-
for (let i = 0; i < value.length; i++) value[i] = walk(value[i]);
|
|
178
|
+
for (let i = 0; i < value.length; i++) value[i] = walk(value[i], scoped);
|
|
123
179
|
return value;
|
|
124
180
|
}
|
|
125
181
|
const obj = value as Record<string, unknown>;
|
|
126
|
-
|
|
182
|
+
// A nested inline resource may declare scopes of its own (a `Run.Sequence`
|
|
183
|
+
// inside another sequence's `with:`). Collected before descending, so the
|
|
184
|
+
// declarations are visible to every region of the resource that declares
|
|
185
|
+
// them — a sequence's `with:` names resolve in its `targets:` and `steps:`
|
|
186
|
+
// alike, not only inside `with:` itself.
|
|
187
|
+
const declared =
|
|
188
|
+
typeof obj.kind === "string" ? declaredInScopes(obj as ResourceManifest) : undefined;
|
|
189
|
+
const inner = declared ? new Map([...(scoped ?? new Map()), ...declared]) : scoped;
|
|
190
|
+
for (const key of Object.keys(obj)) obj[key] = walk(obj[key], inner);
|
|
127
191
|
return value;
|
|
128
192
|
};
|
|
129
193
|
|
|
@@ -323,18 +323,37 @@ export function resolveContextAnnotations(
|
|
|
323
323
|
typeof ref.name === "string" &&
|
|
324
324
|
subpath
|
|
325
325
|
) {
|
|
326
|
+
const segments = subpath.split("/");
|
|
326
327
|
const refManifest = allManifests.find(
|
|
327
328
|
(m) => m.kind === ref.kind && (m.metadata as any)?.name === ref.name,
|
|
328
329
|
) as Record<string, any> | undefined;
|
|
329
330
|
if (refManifest) {
|
|
330
331
|
const resolved = resolveTypeFieldToSchema(
|
|
331
|
-
navigatePath(refManifest,
|
|
332
|
+
navigatePath(refManifest, segments) as unknown,
|
|
332
333
|
allManifests,
|
|
333
334
|
);
|
|
334
335
|
if (resolved && typeof resolved === "object") {
|
|
335
336
|
return resolved;
|
|
336
337
|
}
|
|
337
338
|
}
|
|
339
|
+
// The instance declares nothing, so fall back to its KIND's declaration —
|
|
340
|
+
// the same layering `buildStepContextSchema` applies to `steps.<name>.result`,
|
|
341
|
+
// so a kind with one fixed output shape (declared once on its Telo.Definition)
|
|
342
|
+
// types the context, while a kind that exposes the field for per-instance
|
|
343
|
+
// narrowing keeps winning above.
|
|
344
|
+
if (defs) {
|
|
345
|
+
const canonical = aliases?.resolveKind(ref.kind) ?? ref.kind;
|
|
346
|
+
const def = defs.resolve(canonical) as Record<string, unknown> | undefined;
|
|
347
|
+
if (def) {
|
|
348
|
+
const resolved = resolveTypeFieldToSchema(
|
|
349
|
+
navigatePath(def, segments) as unknown,
|
|
350
|
+
allManifests,
|
|
351
|
+
);
|
|
352
|
+
if (resolved && typeof resolved === "object") {
|
|
353
|
+
return resolved;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
338
357
|
}
|
|
339
358
|
// Fallback: open schema (no false errors when outputType is not declared)
|
|
340
359
|
return { ...schema, additionalProperties: true };
|