@telorun/analyzer 0.21.0 → 0.23.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/README.md +9 -17
- package/dist/alias-resolver.d.ts +7 -0
- package/dist/alias-resolver.d.ts.map +1 -1
- package/dist/alias-resolver.js +14 -0
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/analyzer.js +59 -15
- package/dist/builtins.d.ts.map +1 -1
- package/dist/builtins.js +10 -9
- package/dist/cel-environment.d.ts +8 -0
- package/dist/cel-environment.d.ts.map +1 -1
- package/dist/cel-environment.js +48 -0
- package/dist/flatten-for-analyzer.d.ts +52 -0
- package/dist/flatten-for-analyzer.d.ts.map +1 -1
- package/dist/flatten-for-analyzer.js +192 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/resolve-ref-sentinels.d.ts +22 -7
- package/dist/resolve-ref-sentinels.d.ts.map +1 -1
- package/dist/resolve-ref-sentinels.js +46 -136
- package/dist/schema-compat.d.ts.map +1 -1
- package/dist/schema-compat.js +28 -8
- package/dist/validate-cel-context.d.ts.map +1 -1
- package/dist/validate-cel-context.js +5 -0
- package/dist/validate-reference-forms.d.ts +28 -0
- package/dist/validate-reference-forms.d.ts.map +1 -0
- package/dist/validate-reference-forms.js +91 -0
- package/dist/validate-references.d.ts.map +1 -1
- package/dist/validate-references.js +4 -37
- package/package.json +2 -2
- package/src/alias-resolver.ts +14 -0
- package/src/analyzer.ts +69 -19
- package/src/builtins.ts +10 -9
- package/src/cel-environment.ts +57 -0
- package/src/flatten-for-analyzer.ts +217 -4
- package/src/index.ts +7 -0
- package/src/resolve-ref-sentinels.ts +39 -133
- package/src/schema-compat.ts +27 -8
- package/src/validate-cel-context.ts +5 -0
- package/src/validate-reference-forms.ts +110 -0
- package/src/validate-references.ts +4 -39
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { isTaggedSentinel } from "@telorun/templating";
|
|
2
|
+
import { visitManifest } from "./manifest-visitor.js";
|
|
3
|
+
import { REF_VALIDATION_SKIP_KINDS as SYSTEM_KINDS } from "./system-kinds.js";
|
|
4
|
+
import { DiagnosticSeverity } from "./types.js";
|
|
5
|
+
const SOURCE = "telo-analyzer";
|
|
6
|
+
/**
|
|
7
|
+
* Reference-form validation — the single enforcement point for "a reference is
|
|
8
|
+
* written `!ref <name>` (or `!ref <Alias>.<name>`), nothing else".
|
|
9
|
+
*
|
|
10
|
+
* Runs on the RAW manifest set, BEFORE inline-resource extraction and `!ref`
|
|
11
|
+
* sentinel resolution. That ordering is load-bearing: only at this point is an
|
|
12
|
+
* author-written value still distinguishable from the resolver's own
|
|
13
|
+
* substitution. After normalization both an author's `{kind, name}` and a
|
|
14
|
+
* resolved `!ref` are the same `{kind, name}` object, so no later pass — and no
|
|
15
|
+
* JSON Schema — can tell them apart.
|
|
16
|
+
*
|
|
17
|
+
* At every `x-telo-ref` slot the only accepted value is:
|
|
18
|
+
* - a `!ref` sentinel (or any tagged sentinel — e.g. a `${{ }}` ref passed
|
|
19
|
+
* through a template), or
|
|
20
|
+
* - an inline definition: a plain object with a `kind` and NO `name` (the
|
|
21
|
+
* extractor assigns the name), or
|
|
22
|
+
* - a `${{ }}` CEL expression string (a reference flowed through CEL).
|
|
23
|
+
*
|
|
24
|
+
* Rejected, each with an actionable diagnostic pointing at `!ref`:
|
|
25
|
+
* - the object form `{ kind, name }` (the old reference object), and
|
|
26
|
+
* - a bare string (the old name / dotted-FQN reference).
|
|
27
|
+
*/
|
|
28
|
+
export function validateReferenceForms(resources, registry, aliases, aliasesByModule) {
|
|
29
|
+
if (!aliases)
|
|
30
|
+
return [];
|
|
31
|
+
const diagnostics = [];
|
|
32
|
+
const isForeign = (r) => r.metadata?.forwardedExport === true;
|
|
33
|
+
const localResources = resources.filter((r) => !isForeign(r));
|
|
34
|
+
visitManifest(localResources, registry, {
|
|
35
|
+
onRef: (e) => {
|
|
36
|
+
const value = e.value;
|
|
37
|
+
// `!ref` and `!cel`/`${{ }}` sentinels are the supported shapes.
|
|
38
|
+
if (isTaggedSentinel(value))
|
|
39
|
+
return;
|
|
40
|
+
const r = e.source;
|
|
41
|
+
const resourceLabel = `${r.kind}/${r.metadata.name}`;
|
|
42
|
+
const resourceData = { kind: r.kind, name: r.metadata.name };
|
|
43
|
+
const filePath = r.metadata?.source;
|
|
44
|
+
const path = e.concretePath;
|
|
45
|
+
if (typeof value === "string") {
|
|
46
|
+
// A `${{ }}` reference flowed through CEL is fine; any other bare
|
|
47
|
+
// string at a ref slot is the removed string / dotted-FQN form.
|
|
48
|
+
if (value.includes("${{"))
|
|
49
|
+
return;
|
|
50
|
+
diagnostics.push({
|
|
51
|
+
severity: DiagnosticSeverity.Error,
|
|
52
|
+
code: "INVALID_REFERENCE_FORM",
|
|
53
|
+
source: SOURCE,
|
|
54
|
+
message: `${resourceLabel}: string reference at '${path}' → '${value}' is not supported; write it as '!ref ${refHint(value)}'`,
|
|
55
|
+
data: { resource: resourceData, filePath, path },
|
|
56
|
+
});
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
60
|
+
const obj = value;
|
|
61
|
+
// A plain object is an inline definition unless it names a resource —
|
|
62
|
+
// a `name` makes it the removed `{ kind, name }` reference object.
|
|
63
|
+
if (typeof obj.name === "string" && typeof obj.kind === "string") {
|
|
64
|
+
diagnostics.push({
|
|
65
|
+
severity: DiagnosticSeverity.Error,
|
|
66
|
+
code: "INVALID_REFERENCE_FORM",
|
|
67
|
+
source: SOURCE,
|
|
68
|
+
message: `${resourceLabel}: object reference '{ kind, name }' at '${path}' is not supported; write it as '!ref ${obj.name}'`,
|
|
69
|
+
data: { resource: resourceData, filePath, path },
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
}, {
|
|
75
|
+
aliases,
|
|
76
|
+
aliasesByModule,
|
|
77
|
+
skipKinds: SYSTEM_KINDS,
|
|
78
|
+
expand: true,
|
|
79
|
+
discoverNestedRefs: true,
|
|
80
|
+
});
|
|
81
|
+
return diagnostics;
|
|
82
|
+
}
|
|
83
|
+
/** Best-effort name for the `!ref` suggestion in a string-ref diagnostic: a
|
|
84
|
+
* dotted-FQN (`Http.Api.UsersApi`) keeps its last segment, an alias-qualified
|
|
85
|
+
* name (`Console.writeLine`) is left intact, a bare name passes through. */
|
|
86
|
+
function refHint(value) {
|
|
87
|
+
const dotCount = (value.match(/\./g) ?? []).length;
|
|
88
|
+
if (dotCount >= 2)
|
|
89
|
+
return value.slice(value.lastIndexOf(".") + 1);
|
|
90
|
+
return value;
|
|
91
|
+
}
|
|
@@ -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;AA4C/F;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,kBAAkB,CAChC,SAAS,EAAE,gBAAgB,EAAE,EAC7B,OAAO,EAAE,eAAe,GACvB,kBAAkB,EAAE,
|
|
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;AA4C/F;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,kBAAkB,CAChC,SAAS,EAAE,gBAAgB,EAAE,EAC7B,OAAO,EAAE,eAAe,GACvB,kBAAkB,EAAE,CA6ctB"}
|
|
@@ -272,43 +272,10 @@ export function validateReferences(resources, context) {
|
|
|
272
272
|
}
|
|
273
273
|
return;
|
|
274
274
|
}
|
|
275
|
-
//
|
|
276
|
-
//
|
|
277
|
-
//
|
|
278
|
-
|
|
279
|
-
const lastDot = val.lastIndexOf(".");
|
|
280
|
-
const refName = lastDot > 0 ? val.slice(lastDot + 1) : val;
|
|
281
|
-
const refKindPrefix = lastDot > 0 ? val.slice(0, lastDot) : undefined;
|
|
282
|
-
const target = byName.get(refName) ?? visibleScopeManifests.find((m) => m.metadata?.name === refName);
|
|
283
|
-
if (!target) {
|
|
284
|
-
// Cross-module reference: "Alias.ResourceName" (single dot, bare alias prefix).
|
|
285
|
-
// The resource lives in the imported module's scope and can't be validated here.
|
|
286
|
-
// Multi-dot prefixes like "Alias.Kind.Name" are local resources with qualified
|
|
287
|
-
// kinds — those must be validated.
|
|
288
|
-
if (refKindPrefix && !refKindPrefix.includes(".") && aliases.hasAlias(refKindPrefix)) {
|
|
289
|
-
return;
|
|
290
|
-
}
|
|
291
|
-
diagnostics.push({
|
|
292
|
-
severity: DiagnosticSeverity.Error,
|
|
293
|
-
code: "UNRESOLVED_REFERENCE",
|
|
294
|
-
source: SOURCE,
|
|
295
|
-
message: `${resourceLabel}: reference at '${concretePath}' → resource '${val}' not found`,
|
|
296
|
-
data: { resource: resourceData, filePath, path: concretePath },
|
|
297
|
-
});
|
|
298
|
-
return;
|
|
299
|
-
}
|
|
300
|
-
const kindErrors = checkKind(target.kind, entry, registry, aliases);
|
|
301
|
-
if (kindErrors.length > 0) {
|
|
302
|
-
diagnostics.push({
|
|
303
|
-
severity: DiagnosticSeverity.Error,
|
|
304
|
-
code: "REFERENCE_KIND_MISMATCH",
|
|
305
|
-
source: SOURCE,
|
|
306
|
-
message: `${resourceLabel}: reference at '${concretePath}' → ${kindErrors.join("; ")}`,
|
|
307
|
-
data: { resource: resourceData, filePath, path: concretePath },
|
|
308
|
-
});
|
|
309
|
-
}
|
|
310
|
-
return;
|
|
311
|
-
}
|
|
275
|
+
// Bare strings are no longer a reference shape — `validateReferenceForms`
|
|
276
|
+
// rejects an author-written string at a ref slot before this pass runs,
|
|
277
|
+
// and a `${{ }}` reference flowed through CEL is resolved/typed
|
|
278
|
+
// elsewhere. Anything still a string here is not a reference to resolve.
|
|
312
279
|
if (typeof val !== "object")
|
|
313
280
|
return;
|
|
314
281
|
const refVal = val;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@telorun/analyzer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.23.0",
|
|
4
4
|
"description": "Telo Analyzer - Static manifest validator for Telo manifests.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"telo",
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
"ajv-formats": "^3.0.1",
|
|
43
43
|
"jsonpath-plus": "^10.3.0",
|
|
44
44
|
"yaml": "^2.8.3",
|
|
45
|
-
"@telorun/templating": "0.
|
|
45
|
+
"@telorun/templating": "0.8.0"
|
|
46
46
|
},
|
|
47
47
|
"devDependencies": {
|
|
48
48
|
"@types/node": "^20.0.0",
|
package/src/alias-resolver.ts
CHANGED
|
@@ -3,6 +3,10 @@
|
|
|
3
3
|
export class AliasResolver {
|
|
4
4
|
private readonly importAliases = new Map<string, string>();
|
|
5
5
|
private readonly importedKinds = new Map<string, Set<string>>();
|
|
6
|
+
/** `${alias}.${suffix}` → canonical `<owningModule>.<Kind>` for kinds an import
|
|
7
|
+
* transitively RE-EXPORTS (`exports.kinds: [Alias.Kind]`), which don't live in the
|
|
8
|
+
* import's own module. Resolved before the normal `<module>.<suffix>` construction. */
|
|
9
|
+
private readonly reExportedKinds = new Map<string, string>();
|
|
6
10
|
|
|
7
11
|
registerImport(alias: string, targetModule: string, exportedKinds: string[]): void {
|
|
8
12
|
this.importAliases.set(alias, targetModule);
|
|
@@ -11,6 +15,12 @@ export class AliasResolver {
|
|
|
11
15
|
}
|
|
12
16
|
}
|
|
13
17
|
|
|
18
|
+
/** Register that `<alias>.<suffix>` re-exports the kind canonically named `canonicalKind`
|
|
19
|
+
* (owned by a module the alias's target imports, possibly several hops away). */
|
|
20
|
+
registerKindReExport(alias: string, suffix: string, canonicalKind: string): void {
|
|
21
|
+
this.reExportedKinds.set(`${alias}.${suffix}`, canonicalKind);
|
|
22
|
+
}
|
|
23
|
+
|
|
14
24
|
/** Real module name an alias points at (e.g. "Console" → "console"), or undefined.
|
|
15
25
|
* Used to resolve an alias-qualified instance reference "Console.writeLine" to the
|
|
16
26
|
* forwarded resource declared in that module. The `exports.resources` gate is enforced
|
|
@@ -29,6 +39,10 @@ export class AliasResolver {
|
|
|
29
39
|
if (dot === -1) return undefined;
|
|
30
40
|
const prefix = kind.slice(0, dot);
|
|
31
41
|
const suffix = kind.slice(dot + 1);
|
|
42
|
+
// Re-export takes precedence: a re-exported kind resolves to its true owning module,
|
|
43
|
+
// not `${prefix-target}.${suffix}` (and bypasses the gate — it's explicitly re-exported).
|
|
44
|
+
const reExported = this.reExportedKinds.get(`${prefix}.${suffix}`);
|
|
45
|
+
if (reExported) return reExported;
|
|
32
46
|
const realModule = this.importAliases.get(prefix);
|
|
33
47
|
if (!realModule) return undefined;
|
|
34
48
|
const allowed = this.importedKinds.get(prefix);
|
package/src/analyzer.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { AliasResolver } from "./alias-resolver.js";
|
|
|
5
5
|
import { AnalysisRegistry } from "./analysis-registry.js";
|
|
6
6
|
import {
|
|
7
7
|
buildCelEnvironment,
|
|
8
|
+
buildImportInputCelEnvironment,
|
|
8
9
|
buildTypedCelEnvironment,
|
|
9
10
|
type CelHandlers,
|
|
10
11
|
} from "./cel-environment.js";
|
|
@@ -36,6 +37,7 @@ import { validateExtends } from "./validate-extends.js";
|
|
|
36
37
|
import { validateNestedInlineResources } from "./validate-nested-inline.js";
|
|
37
38
|
import { validateProviderCoherence } from "./validate-provider-coherence.js";
|
|
38
39
|
import { validateReferences } from "./validate-references.js";
|
|
40
|
+
import { validateReferenceForms } from "./validate-reference-forms.js";
|
|
39
41
|
import { validateUnusedDeclarations } from "./validate-unused-declarations.js";
|
|
40
42
|
import { validateThrowsCoverage } from "./validate-throws-coverage.js";
|
|
41
43
|
|
|
@@ -705,16 +707,22 @@ export class StaticAnalyzer {
|
|
|
705
707
|
if (resolvedModuleName) {
|
|
706
708
|
defs.registerModuleIdentity(resolvedNamespace ?? null, resolvedModuleName);
|
|
707
709
|
}
|
|
710
|
+
// `metadata.reExportedKinds` (stamped by flattenForAnalyzer / the editor projection)
|
|
711
|
+
// maps an exported suffix to the true owning module's canonical kind for kinds this
|
|
712
|
+
// import transitively re-exports (`exports.kinds: [Alias.Kind]`).
|
|
713
|
+
const reExportedKinds = ((m.metadata as any)?.reExportedKinds ?? {}) as Record<
|
|
714
|
+
string,
|
|
715
|
+
string
|
|
716
|
+
>;
|
|
708
717
|
// Alias registration is scoped: consumer imports vs. imported-library imports.
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
libResolver.registerImport(alias, targetModule, exportedKinds);
|
|
718
|
+
const resolver =
|
|
719
|
+
!ownModule || rootModules.has(ownModule)
|
|
720
|
+
? aliases
|
|
721
|
+
: (aliasesByModule.get(ownModule) ??
|
|
722
|
+
aliasesByModule.set(ownModule, new AliasResolver()).get(ownModule)!);
|
|
723
|
+
resolver.registerImport(alias, targetModule, exportedKinds);
|
|
724
|
+
for (const [suffix, canonical] of Object.entries(reExportedKinds)) {
|
|
725
|
+
resolver.registerKindReExport(alias, suffix, canonical);
|
|
718
726
|
}
|
|
719
727
|
}
|
|
720
728
|
}
|
|
@@ -774,6 +782,15 @@ export class StaticAnalyzer {
|
|
|
774
782
|
defs.register(normalized);
|
|
775
783
|
}
|
|
776
784
|
|
|
785
|
+
// Reference-form validation — enforce `!ref` as the only reference shape.
|
|
786
|
+
// Runs on the RAW manifests, BEFORE inline extraction and sentinel
|
|
787
|
+
// resolution, while an author-written `{kind, name}` is still
|
|
788
|
+
// distinguishable from the resolver's own substitution (after Phase 2/2.5
|
|
789
|
+
// they are the same object).
|
|
790
|
+
if (!options?.skipValidation) {
|
|
791
|
+
diagnostics.push(...validateReferenceForms(manifests, defs, aliases, aliasesByModule));
|
|
792
|
+
}
|
|
793
|
+
|
|
777
794
|
// Phase 2: extract inline resources from x-telo-ref slots into first-class manifests
|
|
778
795
|
const allManifests = normalizeInlineResources(manifests, defs, aliases, aliasesByModule);
|
|
779
796
|
|
|
@@ -781,7 +798,7 @@ export class StaticAnalyzer {
|
|
|
781
798
|
// {kind, name} objects so downstream phases (validation, dependency graph,
|
|
782
799
|
// kernel controllers) see a uniform shape. Runs after normalize so both
|
|
783
800
|
// original and inline-extracted manifests have their sentinels resolved.
|
|
784
|
-
resolveRefSentinels(allManifests,
|
|
801
|
+
resolveRefSentinels(allManifests, aliases, aliasesByModule);
|
|
785
802
|
|
|
786
803
|
// Trusted-input fast path: when the caller has already attested that
|
|
787
804
|
// this exact manifest set passes analysis (e.g. via the kernel's
|
|
@@ -854,6 +871,26 @@ export class StaticAnalyzer {
|
|
|
854
871
|
}
|
|
855
872
|
}
|
|
856
873
|
}
|
|
874
|
+
// `exports.resources` entries are plain names: `Db` (local) or `Alias.Name` (re-export),
|
|
875
|
+
// mirroring `exports.kinds`. The `!ref` tag is not accepted here — a `!ref` parses to a
|
|
876
|
+
// sentinel object that the schema's CEL/ref exemption would silently pass, so reject any
|
|
877
|
+
// non-string entry with an actionable message instead.
|
|
878
|
+
const exportsResources = (m as Record<string, any>).exports?.resources;
|
|
879
|
+
if (Array.isArray(exportsResources)) {
|
|
880
|
+
for (let i = 0; i < exportsResources.length; i++) {
|
|
881
|
+
if (typeof exportsResources[i] === "string") continue;
|
|
882
|
+
diagnostics.push({
|
|
883
|
+
severity: DiagnosticSeverity.Error,
|
|
884
|
+
code: "INVALID_EXPORT",
|
|
885
|
+
source: SOURCE,
|
|
886
|
+
message:
|
|
887
|
+
`Telo.Library exports.resources[${i}]: write the exported name as a plain string — ` +
|
|
888
|
+
`'Name' to export a local instance, or 'Alias.Name' to re-export an imported one. ` +
|
|
889
|
+
`The '!ref' tag is not allowed in exports.resources.`,
|
|
890
|
+
data: { resource, filePath, path: `exports.resources.${i}` },
|
|
891
|
+
});
|
|
892
|
+
}
|
|
893
|
+
}
|
|
857
894
|
}
|
|
858
895
|
|
|
859
896
|
// Build typed kernel globals schema so x-telo-context chain validation
|
|
@@ -933,8 +970,27 @@ export class StaticAnalyzer {
|
|
|
933
970
|
},
|
|
934
971
|
}
|
|
935
972
|
: definition.schema;
|
|
936
|
-
// Phase 1: CEL type checking — walk data+schema together, check env.check() return types
|
|
937
|
-
|
|
973
|
+
// Phase 1: CEL type checking — walk data+schema together, check env.check() return types.
|
|
974
|
+
// A Telo.Import's variables/secrets are a config-only contract evaluated against the
|
|
975
|
+
// IMPORTING module's scope, so type them from the owning module doc (matched by
|
|
976
|
+
// `metadata.module`) and drop `resources`/`env` so referencing them is an error. A
|
|
977
|
+
// library's own internal import is validated against that library in the library's
|
|
978
|
+
// standalone analysis; in this flattened app pass the library doc is absent, so the
|
|
979
|
+
// importer is undefined here and variables/secrets fall back to a permissive `map`
|
|
980
|
+
// (no false positives) while resources/env stay rejected.
|
|
981
|
+
const importerModule =
|
|
982
|
+
m.kind === "Telo.Import"
|
|
983
|
+
? allManifests.find(
|
|
984
|
+
(mm) =>
|
|
985
|
+
(mm.kind === "Telo.Application" || mm.kind === "Telo.Library") &&
|
|
986
|
+
(mm.metadata as { name?: string } | undefined)?.name ===
|
|
987
|
+
(m.metadata as { module?: string } | undefined)?.module,
|
|
988
|
+
)
|
|
989
|
+
: undefined;
|
|
990
|
+
const baseTypedEnv =
|
|
991
|
+
m.kind === "Telo.Import"
|
|
992
|
+
? buildImportInputCelEnvironment(this.celEnv, importerModule)
|
|
993
|
+
: buildTypedCelEnvironment(this.celEnv, m, undefined, moduleManifest);
|
|
938
994
|
const celIssues = collectCelTypeIssues(
|
|
939
995
|
m,
|
|
940
996
|
schema,
|
|
@@ -1286,13 +1342,7 @@ export class StaticAnalyzer {
|
|
|
1286
1342
|
// Resolve !ref sentinels after normalize so both the original and
|
|
1287
1343
|
// inline-extracted manifests get their refs canonicalized to
|
|
1288
1344
|
// {kind, name} for the kernel that consumes this output.
|
|
1289
|
-
resolveRefSentinels(
|
|
1290
|
-
normalized,
|
|
1291
|
-
ctx.definitions!,
|
|
1292
|
-
ctx.aliases,
|
|
1293
|
-
ctx.aliasesByModule,
|
|
1294
|
-
crossModuleTargets ?? [],
|
|
1295
|
-
);
|
|
1345
|
+
resolveRefSentinels(normalized, ctx.aliases, ctx.aliasesByModule, crossModuleTargets ?? []);
|
|
1296
1346
|
return normalized;
|
|
1297
1347
|
}
|
|
1298
1348
|
|
package/src/builtins.ts
CHANGED
|
@@ -236,7 +236,7 @@ export const KERNEL_BUILTINS: ResourceDefinition[] = [
|
|
|
236
236
|
},
|
|
237
237
|
// Gated reference: run() a Runnable/Service only when the
|
|
238
238
|
// `when` CEL guard holds. Discriminated by the `ref` key. `ref`
|
|
239
|
-
// is a
|
|
239
|
+
// is a `!ref` that resolves to the `{ kind, name }` shape below.
|
|
240
240
|
{
|
|
241
241
|
type: "object",
|
|
242
242
|
required: ["ref"],
|
|
@@ -264,12 +264,11 @@ export const KERNEL_BUILTINS: ResourceDefinition[] = [
|
|
|
264
264
|
// with an optional `name` (for steps.<name>.result plumbing),
|
|
265
265
|
// `when` guard, and `inputs`. Discriminated by the `invoke` key.
|
|
266
266
|
// Control flow (if/while/switch/try) is not available here —
|
|
267
|
-
// reach for Run.Sequence. `invoke` is ref-only
|
|
268
|
-
// to
|
|
269
|
-
//
|
|
270
|
-
//
|
|
271
|
-
//
|
|
272
|
-
// Run.Sequence invoke steps.
|
|
267
|
+
// reach for Run.Sequence. `invoke` is ref-only: a `!ref` that
|
|
268
|
+
// resolves to the `{ kind, name }` shape below. Requiring `name`
|
|
269
|
+
// rejects an inline `{ kind }` definition (no name) at analysis
|
|
270
|
+
// instead of failing at boot with an undefined resource name. The
|
|
271
|
+
// Invocable/Runnable kind set mirrors Run.Sequence invoke steps.
|
|
273
272
|
{
|
|
274
273
|
type: "object",
|
|
275
274
|
required: ["invoke"],
|
|
@@ -456,8 +455,10 @@ export const KERNEL_BUILTINS: ResourceDefinition[] = [
|
|
|
456
455
|
type: "object",
|
|
457
456
|
properties: {
|
|
458
457
|
kinds: { type: "array", items: { type: "string" } },
|
|
459
|
-
//
|
|
460
|
-
//
|
|
458
|
+
// An entry is a bare name (`Db`, a locally-owned export) or a dotted `Alias.Name`
|
|
459
|
+
// (re-export of the instance reached via this library's import aliased `Alias`,
|
|
460
|
+
// under the name `Name`) — mirroring `exports.kinds`. `variables` / `secrets` are
|
|
461
|
+
// reserved on the resources.<Alias> value-flow surface, so they may not be exported.
|
|
461
462
|
resources: {
|
|
462
463
|
type: "array",
|
|
463
464
|
items: { type: "string", not: { enum: ["variables", "secrets"] } },
|
package/src/cel-environment.ts
CHANGED
|
@@ -102,3 +102,60 @@ export function buildTypedCelEnvironment(
|
|
|
102
102
|
return baseEnv.clone();
|
|
103
103
|
}
|
|
104
104
|
}
|
|
105
|
+
|
|
106
|
+
/** Register a `variables`/`secrets` namespace typed from a module doc's schema map
|
|
107
|
+
* (`{ name: <schema>, … }`), falling back to dyn `map` when absent or untyped. */
|
|
108
|
+
function registerConfigNamespace(
|
|
109
|
+
env: Environment,
|
|
110
|
+
block: unknown,
|
|
111
|
+
name: "variables" | "secrets",
|
|
112
|
+
): void {
|
|
113
|
+
if (block !== null && typeof block === "object" && !Array.isArray(block)) {
|
|
114
|
+
const entries = Object.entries(block as Record<string, unknown>).filter(
|
|
115
|
+
([, v]) => v !== null && typeof v === "object" && !Array.isArray(v),
|
|
116
|
+
);
|
|
117
|
+
if (entries.length > 0) {
|
|
118
|
+
const schema: Record<string, string> = {};
|
|
119
|
+
for (const [k, v] of entries) schema[k] = jsonSchemaToCelType(v as Record<string, any>);
|
|
120
|
+
(env as any).registerVariable({ name, schema });
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
env.registerVariable(name, "map");
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** CEL environment for the `variables:`/`secrets:` expressions on a `Telo.Import`.
|
|
128
|
+
*
|
|
129
|
+
* Import inputs are a config-only contract: their expressions are evaluated
|
|
130
|
+
* against the IMPORTING module's `variables`/`secrets`, never the import's own
|
|
131
|
+
* values map (the bug) nor the imported child's. `resources`, `env`, and `ports`
|
|
132
|
+
* are registered as empty typed objects, so referencing them is a "No such key"
|
|
133
|
+
* error that steers authors to a typed `variables` entry. */
|
|
134
|
+
export function buildImportInputCelEnvironment(
|
|
135
|
+
baseEnv: Environment,
|
|
136
|
+
moduleManifest: ResourceManifest | undefined,
|
|
137
|
+
): Environment {
|
|
138
|
+
const env = baseEnv.clone();
|
|
139
|
+
for (const brand of Object.keys(VALUE_BRAND_BASE)) {
|
|
140
|
+
(env as any).registerType(brand, { fields: {} });
|
|
141
|
+
}
|
|
142
|
+
const mod = moduleManifest as Record<string, unknown> | undefined;
|
|
143
|
+
// Typing variables/secrets from the importer's schema can fail on a malformed
|
|
144
|
+
// schema; degrade those to permissive `map` if so — but never lose the
|
|
145
|
+
// resources/env/ports rejection registered below (the catch is scoped so a
|
|
146
|
+
// typing failure can't silently re-open the config-only contract).
|
|
147
|
+
try {
|
|
148
|
+
registerConfigNamespace(env, mod?.variables, "variables");
|
|
149
|
+
registerConfigNamespace(env, mod?.secrets, "secrets");
|
|
150
|
+
} catch {
|
|
151
|
+
env.registerVariable("variables", "map");
|
|
152
|
+
env.registerVariable("secrets", "map");
|
|
153
|
+
}
|
|
154
|
+
// Override the base env's dyn `resources`/`env`/`ports` with empty typed objects
|
|
155
|
+
// so any access (`resources.X`, `env.X`) is a "No such key" error — these
|
|
156
|
+
// surfaces are not part of the config-only import contract.
|
|
157
|
+
for (const name of ["resources", "env", "ports"]) {
|
|
158
|
+
(env as any).registerVariable({ name, schema: {} });
|
|
159
|
+
}
|
|
160
|
+
return env;
|
|
161
|
+
}
|