@telorun/analyzer 0.56.1 → 0.58.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/analyzer.d.ts +5 -0
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/analyzer.js +232 -94
- package/dist/cel-environment.d.ts.map +1 -1
- package/dist/cel-environment.js +6 -3
- package/dist/definition-registry.d.ts +5 -0
- package/dist/definition-registry.d.ts.map +1 -1
- package/dist/definition-registry.js +9 -0
- package/dist/index.d.ts +14 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -3
- package/dist/invocation-contract.d.ts +21 -13
- package/dist/invocation-contract.d.ts.map +1 -1
- package/dist/invocation-contract.js +43 -46
- package/dist/loaded-types.d.ts +13 -0
- package/dist/loaded-types.d.ts.map +1 -1
- package/dist/manifest-loader.d.ts +8 -1
- package/dist/manifest-loader.d.ts.map +1 -1
- package/dist/manifest-loader.js +33 -6
- package/dist/manifest-visitor.d.ts +4 -0
- package/dist/manifest-visitor.d.ts.map +1 -1
- package/dist/manifest-visitor.js +3 -3
- package/dist/migrations/driver.d.ts +69 -0
- package/dist/migrations/driver.d.ts.map +1 -0
- package/dist/migrations/driver.js +200 -0
- package/dist/migrations/entries/index.d.ts +3 -0
- package/dist/migrations/entries/index.d.ts.map +1 -0
- package/dist/migrations/entries/index.js +9 -0
- package/dist/migrations/entries/normalize-value-types.json +48 -0
- package/dist/migrations/entries/ref-slot-scalar-type.json +20 -0
- package/dist/migrations/entry-data.d.ts +25 -0
- package/dist/migrations/entry-data.d.ts.map +1 -0
- package/dist/migrations/entry-data.js +171 -0
- package/dist/migrations/index.d.ts +22 -0
- package/dist/migrations/index.d.ts.map +1 -0
- package/dist/migrations/index.js +19 -0
- package/dist/migrations/match.d.ts +162 -0
- package/dist/migrations/match.d.ts.map +1 -0
- package/dist/migrations/match.js +295 -0
- package/dist/migrations/patch.d.ts +68 -0
- package/dist/migrations/patch.d.ts.map +1 -0
- package/dist/migrations/patch.js +236 -0
- package/dist/migrations/provenance.d.ts +25 -0
- package/dist/migrations/provenance.d.ts.map +1 -0
- package/dist/migrations/provenance.js +118 -0
- package/dist/migrations/registry.d.ts +35 -0
- package/dist/migrations/registry.d.ts.map +1 -0
- package/dist/migrations/registry.js +35 -0
- package/dist/migrations/report.d.ts +28 -0
- package/dist/migrations/report.d.ts.map +1 -0
- package/dist/migrations/report.js +144 -0
- package/dist/migrations/types.d.ts +115 -0
- package/dist/migrations/types.d.ts.map +1 -0
- package/dist/migrations/types.js +24 -0
- package/dist/migrations/yaml-edit.d.ts +21 -0
- package/dist/migrations/yaml-edit.d.ts.map +1 -0
- package/dist/migrations/yaml-edit.js +245 -0
- package/dist/module-file-claims.d.ts +65 -0
- package/dist/module-file-claims.d.ts.map +1 -0
- package/dist/module-file-claims.js +106 -0
- package/dist/parse-loaded-file.d.ts +8 -0
- package/dist/parse-loaded-file.d.ts.map +1 -1
- package/dist/parse-loaded-file.js +22 -13
- package/dist/resolve-schema-type-refs.d.ts +29 -12
- package/dist/resolve-schema-type-refs.d.ts.map +1 -1
- package/dist/resolve-schema-type-refs.js +89 -16
- package/dist/schema-compat.d.ts +35 -9
- package/dist/schema-compat.d.ts.map +1 -1
- package/dist/schema-compat.js +181 -54
- package/dist/schema-region.d.ts +33 -0
- package/dist/schema-region.d.ts.map +1 -0
- package/dist/schema-region.js +45 -0
- package/dist/types.d.ts +51 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +6 -0
- package/dist/validate-cel-context.d.ts.map +1 -1
- package/dist/validate-cel-context.js +29 -3
- package/dist/validate-include-placement.d.ts +26 -0
- package/dist/validate-include-placement.d.ts.map +1 -0
- package/dist/validate-include-placement.js +67 -0
- package/dist/validate-step-inputs.d.ts +8 -1
- package/dist/validate-step-inputs.d.ts.map +1 -1
- package/dist/validate-step-inputs.js +47 -2
- package/dist/validate-throws-coverage.d.ts.map +1 -1
- package/dist/validate-throws-coverage.js +15 -12
- package/dist/validate-value-type-slots.d.ts +43 -0
- package/dist/validate-value-type-slots.d.ts.map +1 -0
- package/dist/validate-value-type-slots.js +142 -0
- package/dist/value-type-keyword.d.ts +60 -0
- package/dist/value-type-keyword.d.ts.map +1 -0
- package/dist/value-type-keyword.js +119 -0
- package/dist/yaml-source-edit.d.ts +56 -0
- package/dist/yaml-source-edit.d.ts.map +1 -0
- package/dist/yaml-source-edit.js +93 -0
- package/package.json +4 -3
- package/src/analyzer.ts +276 -129
- package/src/cel-environment.ts +6 -3
- package/src/definition-registry.ts +10 -0
- package/src/index.ts +31 -3
- package/src/invocation-contract.ts +43 -48
- package/src/loaded-types.ts +13 -0
- package/src/manifest-loader.ts +37 -6
- package/src/manifest-visitor.ts +11 -3
- package/src/migrations/driver.ts +262 -0
- package/src/migrations/entries/index.ts +10 -0
- package/src/migrations/entries/normalize-value-types.json +48 -0
- package/src/migrations/entries/ref-slot-scalar-type.json +20 -0
- package/src/migrations/entry-data.ts +207 -0
- package/src/migrations/index.ts +22 -0
- package/src/migrations/match.ts +382 -0
- package/src/migrations/patch.ts +265 -0
- package/src/migrations/provenance.ts +167 -0
- package/src/migrations/registry.ts +40 -0
- package/src/migrations/report.ts +169 -0
- package/src/migrations/types.ts +144 -0
- package/src/migrations/yaml-edit.ts +260 -0
- package/src/module-file-claims.ts +168 -0
- package/src/parse-loaded-file.ts +31 -12
- package/src/resolve-schema-type-refs.ts +93 -16
- package/src/schema-compat.ts +218 -62
- package/src/schema-region.ts +46 -0
- package/src/types.ts +54 -0
- package/src/validate-cel-context.ts +36 -3
- package/src/validate-include-placement.ts +70 -0
- package/src/validate-step-inputs.ts +56 -1
- package/src/validate-throws-coverage.ts +16 -11
- package/src/validate-value-type-slots.ts +182 -0
- package/src/value-type-keyword.ts +135 -0
- package/src/yaml-source-edit.ts +106 -0
- package/dist/binary-slot.d.ts +0 -39
- package/dist/binary-slot.d.ts.map +0 -1
- package/dist/binary-slot.js +0 -58
- package/src/binary-slot.ts +0 -71
package/src/analyzer.ts
CHANGED
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
import type { ResourceDefinition, ResourceManifest } from "@telorun/sdk";
|
|
2
2
|
import { canonicalTypeSchemaId, OBSERVED_STATE_KEY } from "@telorun/sdk";
|
|
3
3
|
import type { Environment } from "@marcbachmann/cel-js";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
defaultRegistry,
|
|
6
|
+
isRefSentinel,
|
|
7
|
+
isTaggedSentinel,
|
|
8
|
+
plainChainOf,
|
|
9
|
+
type CelSurface,
|
|
10
|
+
} from "@telorun/templating";
|
|
11
|
+
import type { DiagnosticFix } from "./types.js";
|
|
5
12
|
import {
|
|
6
13
|
AliasResolver,
|
|
7
14
|
moduleScopedDefResolver,
|
|
@@ -50,11 +57,17 @@ import {
|
|
|
50
57
|
validateRefSlotDeclarations,
|
|
51
58
|
type RefSlotIssue,
|
|
52
59
|
} from "./validate-ref-slots.js";
|
|
60
|
+
import {
|
|
61
|
+
validateValueTypeSlots,
|
|
62
|
+
type ValueTypeSlotIssue,
|
|
63
|
+
} from "./validate-value-type-slots.js";
|
|
53
64
|
import { resolveSchemaTypeRefs } from "./resolve-schema-type-refs.js";
|
|
54
65
|
import { validateSchemaTypeRefs } from "./validate-schema-type-refs.js";
|
|
55
66
|
import { rewriteSyntheticOrigins } from "./rewrite-synthetic-origins.js";
|
|
56
67
|
import {
|
|
57
68
|
celTypeSatisfiesJsonSchema,
|
|
69
|
+
checkSchemaCompatibility,
|
|
70
|
+
navigateSchemaToExprPath,
|
|
58
71
|
substituteCelFields,
|
|
59
72
|
validateAgainstSchema,
|
|
60
73
|
type SchemaIssue,
|
|
@@ -84,6 +97,7 @@ import {
|
|
|
84
97
|
import { validateExtends } from "./validate-extends.js";
|
|
85
98
|
import { validateLogging } from "./validate-logging.js";
|
|
86
99
|
import { validateModuleArtifact } from "./validate-module-artifact.js";
|
|
100
|
+
import { validateIncludePlacement } from "./validate-include-placement.js";
|
|
87
101
|
import { validateModuleMetadata } from "./validate-module-metadata.js";
|
|
88
102
|
import { validateBaseMapping } from "./validate-base-mapping.js";
|
|
89
103
|
import { validateInvocationContract } from "./validate-invocation-contract.js";
|
|
@@ -906,89 +920,62 @@ function celAccessChains(env: Environment, expr: string): string[][] {
|
|
|
906
920
|
const CEL_PURE_RE = /^\s*\$\{\{[^}]*\}\}\s*$/;
|
|
907
921
|
const CEL_EXPR_RE = /\$\{\{\s*([^}]+?)\s*\}\}/;
|
|
908
922
|
|
|
909
|
-
/**
|
|
910
|
-
*
|
|
911
|
-
|
|
923
|
+
/** Restore the delimiters an engine's fix was computed without, so the
|
|
924
|
+
* replacement is the whole scalar rather than a bare expression that would be
|
|
925
|
+
* read back as literal text. A tagged scalar carries no wrapper and passes
|
|
926
|
+
* through untouched. */
|
|
927
|
+
function rewrapFix(
|
|
928
|
+
fix: DiagnosticFix | undefined,
|
|
929
|
+
wrapper: CelSurface["wrapper"],
|
|
930
|
+
): DiagnosticFix | undefined {
|
|
931
|
+
if (!fix || !wrapper) return fix;
|
|
932
|
+
return { replacement: wrapper.prefix + fix.replacement + wrapper.suffix };
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
/** A pure-CEL leaf and the schema of the field it sits in. */
|
|
936
|
+
export interface CelValueSlot {
|
|
937
|
+
readonly path: string;
|
|
938
|
+
readonly schema: Record<string, any>;
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
/** Recursively walk `data`+`schema` together, collecting every pure CEL leaf
|
|
942
|
+
* with the schema of the field holding it.
|
|
943
|
+
*
|
|
944
|
+
* Type *checking* is deliberately not done here. It belongs to the templating
|
|
945
|
+
* engine, which owns the expression's syntax and now runs it once against the
|
|
946
|
+
* environment typed for that path; checking again here would mean two verdicts
|
|
947
|
+
* from two environments about one expression — which is exactly how an opaque
|
|
948
|
+
* "no matching overload" used to survive next to the diagnostic that explained
|
|
949
|
+
* it. What this walk supplies is the half the engine cannot know: the declared
|
|
950
|
+
* type of the slot the value flows into. The comparison happens once both are
|
|
951
|
+
* in hand (`reportCelReturnMismatches`). */
|
|
952
|
+
function collectCelValueSlots(
|
|
912
953
|
data: unknown,
|
|
913
954
|
schema: Record<string, any>,
|
|
914
955
|
path: string,
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
): SchemaIssue[] {
|
|
921
|
-
const issues: SchemaIssue[] = [];
|
|
922
|
-
|
|
923
|
-
// A pure CEL value type-checks the same regardless of surface form: a
|
|
924
|
-
// `${{ … }}` string and a `!cel`-tagged sentinel must behave identically.
|
|
956
|
+
): CelValueSlot[] {
|
|
957
|
+
const slots: CelValueSlot[] = [];
|
|
958
|
+
|
|
959
|
+
// A pure CEL value behaves the same regardless of surface form: a
|
|
960
|
+
// `${{ … }}` string and a `!cel`-tagged sentinel are the same expression.
|
|
925
961
|
let celExpr: string | undefined;
|
|
926
962
|
if (isTaggedSentinel(data)) {
|
|
927
963
|
// Non-CEL engines (e.g. `!literal`) are analyzed by their own engine pass.
|
|
928
|
-
if (data.engine !== "cel") return
|
|
964
|
+
if (data.engine !== "cel") return slots;
|
|
929
965
|
celExpr = data.source;
|
|
930
966
|
} else if (typeof data === "string" && CEL_PURE_RE.test(data)) {
|
|
931
967
|
celExpr = data.match(CEL_EXPR_RE)?.[1]?.trim();
|
|
932
968
|
}
|
|
933
969
|
|
|
934
970
|
if (celExpr !== undefined) {
|
|
935
|
-
{
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
// Merge x-telo-context variables for this path if applicable
|
|
939
|
-
let typedEnv = baseTypedEnv;
|
|
940
|
-
if (definition.schema) {
|
|
941
|
-
for (const ctx of extractContextsFromSchema(definition.schema)) {
|
|
942
|
-
if (!pathMatchesScope(path, ctx.scope)) continue;
|
|
943
|
-
typedEnv = buildTypedCelEnvironment(rootEnv, manifest, ctx.schema, rootModuleManifest);
|
|
944
|
-
break;
|
|
945
|
-
}
|
|
946
|
-
}
|
|
947
|
-
|
|
948
|
-
let checkResult: ReturnType<typeof typedEnv.check> | undefined;
|
|
949
|
-
try {
|
|
950
|
-
checkResult = typedEnv.check(expr);
|
|
951
|
-
} catch {
|
|
952
|
-
/* degrade gracefully */
|
|
953
|
-
}
|
|
954
|
-
|
|
955
|
-
if (checkResult?.valid === false && checkResult.error) {
|
|
956
|
-
// env.check() rejected the expression itself — e.g. wrong method, wrong
|
|
957
|
-
// argument types, wrong operator overload. Surface the first line of the
|
|
958
|
-
// error message; the tail is a source-code caret diagram we don't need.
|
|
959
|
-
const message = String((checkResult.error as { message?: string }).message ?? checkResult.error)
|
|
960
|
-
.split("\n")[0]
|
|
961
|
-
.trim();
|
|
962
|
-
issues.push({ message: `CEL type error: ${message}`, path });
|
|
963
|
-
} else if (checkResult?.valid && checkResult.type && schema) {
|
|
964
|
-
const celType = checkResult.type.split("<")[0]!;
|
|
965
|
-
if (!celTypeSatisfiesJsonSchema(celType, schema)) {
|
|
966
|
-
const expected = schema["x-telo-type"] ?? schema.type ?? "unknown";
|
|
967
|
-
issues.push({
|
|
968
|
-
message: `CEL returns '${checkResult.type}' but field expects '${expected}'`,
|
|
969
|
-
path,
|
|
970
|
-
});
|
|
971
|
-
}
|
|
972
|
-
}
|
|
973
|
-
}
|
|
974
|
-
return issues;
|
|
971
|
+
if (schema) slots.push({ path, schema });
|
|
972
|
+
return slots;
|
|
975
973
|
}
|
|
976
974
|
|
|
977
975
|
if (Array.isArray(data)) {
|
|
978
976
|
const itemSchema = (schema.items ?? {}) as Record<string, any>;
|
|
979
977
|
for (let i = 0; i < data.length; i++) {
|
|
980
|
-
|
|
981
|
-
...collectCelTypeIssues(
|
|
982
|
-
data[i],
|
|
983
|
-
itemSchema,
|
|
984
|
-
`${path}[${i}]`,
|
|
985
|
-
definition,
|
|
986
|
-
manifest,
|
|
987
|
-
baseTypedEnv,
|
|
988
|
-
rootEnv,
|
|
989
|
-
rootModuleManifest,
|
|
990
|
-
),
|
|
991
|
-
);
|
|
978
|
+
slots.push(...collectCelValueSlots(data[i], itemSchema, `${path}[${i}]`));
|
|
992
979
|
}
|
|
993
980
|
} else if (data !== null && typeof data === "object") {
|
|
994
981
|
const props = (schema.properties ?? {}) as Record<string, any>;
|
|
@@ -997,22 +984,17 @@ function collectCelTypeIssues(
|
|
|
997
984
|
? (schema.additionalProperties as Record<string, any>)
|
|
998
985
|
: {};
|
|
999
986
|
for (const [k, v] of Object.entries(data as Record<string, unknown>)) {
|
|
1000
|
-
|
|
1001
|
-
...
|
|
987
|
+
slots.push(
|
|
988
|
+
...collectCelValueSlots(
|
|
1002
989
|
v,
|
|
1003
990
|
(props[k] ?? mapValueSchema) as Record<string, any>,
|
|
1004
991
|
path ? `${path}.${k}` : k,
|
|
1005
|
-
definition,
|
|
1006
|
-
manifest,
|
|
1007
|
-
baseTypedEnv,
|
|
1008
|
-
rootEnv,
|
|
1009
|
-
rootModuleManifest,
|
|
1010
992
|
),
|
|
1011
993
|
);
|
|
1012
994
|
}
|
|
1013
995
|
}
|
|
1014
996
|
|
|
1015
|
-
return
|
|
997
|
+
return slots;
|
|
1016
998
|
}
|
|
1017
999
|
|
|
1018
1000
|
export interface StaticAnalyzerOptions {
|
|
@@ -1226,6 +1208,18 @@ export class StaticAnalyzer {
|
|
|
1226
1208
|
const refConstraintIssues: RefConstraintIssue[] = [];
|
|
1227
1209
|
const refSlotIssues: RefSlotIssue[] = [];
|
|
1228
1210
|
const zoneSlotIssues: ZoneSlotIssue[] = [];
|
|
1211
|
+
// `x-telo-type` is checked on EVERY manifest, not only on definition docs: a
|
|
1212
|
+
// schema fragment is written wherever a kind declares a schema-valued field,
|
|
1213
|
+
// so an inline `inputType:` on an ordinary resource carries one just as a
|
|
1214
|
+
// definition's `schema:` does. Same scoping as every other schema issue —
|
|
1215
|
+
// the entry's own modules, since a dependency is not the consumer's to fix.
|
|
1216
|
+
const valueTypeSlotIssues: ValueTypeSlotIssue[] = [];
|
|
1217
|
+
for (const m of manifests) {
|
|
1218
|
+
const declaringModule = (m.metadata as { module?: string } | undefined)?.module;
|
|
1219
|
+
if (!declaringModule || rootModules.has(declaringModule)) {
|
|
1220
|
+
valueTypeSlotIssues.push(...validateValueTypeSlots(m as unknown as ResourceManifest));
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1229
1223
|
for (const m of manifests) {
|
|
1230
1224
|
if (m.kind !== "Telo.Definition" && m.kind !== "Telo.Abstract") continue;
|
|
1231
1225
|
const def = m as unknown as ResourceDefinition;
|
|
@@ -1365,6 +1359,26 @@ export class StaticAnalyzer {
|
|
|
1365
1359
|
// accessor split; `readRefSlot` stays lenient so surfaces keep working
|
|
1366
1360
|
// mid-migration, and this reports what leniency would silently absorb.
|
|
1367
1361
|
for (const issue of refSlotIssues) diagnostics.push(refSlotIssueDiagnostic(issue));
|
|
1362
|
+
// The same split for `x-telo-type`. Its reader returns a slot with no
|
|
1363
|
+
// entry for a name it does not know, which is what an unrecognized brand
|
|
1364
|
+
// used to do SILENTLY — the slot simply lost its identity.
|
|
1365
|
+
for (const issue of valueTypeSlotIssues) {
|
|
1366
|
+
diagnostics.push({
|
|
1367
|
+
severity: DiagnosticSeverity.Error,
|
|
1368
|
+
code: issue.code,
|
|
1369
|
+
source: SOURCE,
|
|
1370
|
+
message: issue.message,
|
|
1371
|
+
data: {
|
|
1372
|
+
resource: {
|
|
1373
|
+
kind: issue.manifest.kind,
|
|
1374
|
+
name: issue.manifest.metadata?.name as string,
|
|
1375
|
+
},
|
|
1376
|
+
filePath: (issue.manifest.metadata as { source?: string } | undefined)?.source,
|
|
1377
|
+
path: issue.path,
|
|
1378
|
+
...(issue.fix ? { fix: issue.fix } : {}),
|
|
1379
|
+
},
|
|
1380
|
+
});
|
|
1381
|
+
}
|
|
1368
1382
|
// Same split for the two zone annotations. Unreadable ones fail in
|
|
1369
1383
|
// OPPOSITE directions — a dropped requirement is silently unenforced, a
|
|
1370
1384
|
// dropped provision invents failures — so neither can be left to
|
|
@@ -1501,6 +1515,9 @@ export class StaticAnalyzer {
|
|
|
1501
1515
|
// these fields, which is precisely why they need a check: a mistyped one
|
|
1502
1516
|
// has no runtime failure mode that would ever surface it.
|
|
1503
1517
|
diagnostics.push(...validateModuleMetadata(allManifests, defs, aliases));
|
|
1518
|
+
// A file embed resolves at resource creation, so one written on a doc that
|
|
1519
|
+
// is never instantiated is read by nothing and would ship silently.
|
|
1520
|
+
diagnostics.push(...validateIncludePlacement(allManifests));
|
|
1504
1521
|
}
|
|
1505
1522
|
resolveSchemaTypeRefs(allManifests, aliases, aliasesByModule);
|
|
1506
1523
|
|
|
@@ -1638,6 +1655,24 @@ export class StaticAnalyzer {
|
|
|
1638
1655
|
allManifests.find((mm) => mm.kind === "Telo.Application") ??
|
|
1639
1656
|
allManifests.find((mm) => mm.kind === "Telo.Library");
|
|
1640
1657
|
|
|
1658
|
+
// Every pure-CEL leaf, with the slot it flows into. Compared against the
|
|
1659
|
+
// type the engine walk resolves, once both halves exist — see
|
|
1660
|
+
// `collectCelValueSlots`.
|
|
1661
|
+
const celReturnSlots: (CelValueSlot & {
|
|
1662
|
+
manifest: ResourceManifest;
|
|
1663
|
+
resource: { kind: string; name: string };
|
|
1664
|
+
filePath?: string;
|
|
1665
|
+
})[] = [];
|
|
1666
|
+
const celTypeByPath = new Map<ResourceManifest, Map<string, string>>();
|
|
1667
|
+
// The schema an expression RESOLVES TO, beside the CEL type it carries. Both
|
|
1668
|
+
// are needed and neither replaces the other: the CEL type answers "does this
|
|
1669
|
+
// fit the slot at all", the schema answers "do their type arguments agree",
|
|
1670
|
+
// which cel-js cannot express because it types by constructor identity.
|
|
1671
|
+
const celSourceSchemaByPath = new Map<ResourceManifest, Map<string, Record<string, any>>>();
|
|
1672
|
+
// Context-free typed environments, one per manifest. Reused across every
|
|
1673
|
+
// expression in it — see the build site for why a matched context opts out.
|
|
1674
|
+
const typedEnvByManifest = new Map<ResourceManifest, Environment>();
|
|
1675
|
+
|
|
1641
1676
|
// Validate each non-definition, non-system resource
|
|
1642
1677
|
for (const m of allManifests) {
|
|
1643
1678
|
const filePath = (m.metadata as { source?: string } | undefined)?.source;
|
|
@@ -1722,7 +1757,15 @@ export class StaticAnalyzer {
|
|
|
1722
1757
|
code: "UNDEFINED_KIND",
|
|
1723
1758
|
source: SOURCE,
|
|
1724
1759
|
message: `No Telo.Definition found for kind '${m.kind}'.${hint}`,
|
|
1725
|
-
|
|
1760
|
+
// `suggestedKind` is kept beside the generic `fix` because it names
|
|
1761
|
+
// what the replacement IS; the fix is how to apply it.
|
|
1762
|
+
data: {
|
|
1763
|
+
resource,
|
|
1764
|
+
filePath,
|
|
1765
|
+
path: "kind",
|
|
1766
|
+
suggestedKind,
|
|
1767
|
+
...(suggestedKind ? { fix: { replacement: suggestedKind } } : {}),
|
|
1768
|
+
},
|
|
1726
1769
|
});
|
|
1727
1770
|
continue;
|
|
1728
1771
|
}
|
|
@@ -1749,37 +1792,12 @@ export class StaticAnalyzer {
|
|
|
1749
1792
|
},
|
|
1750
1793
|
}
|
|
1751
1794
|
: authorSchema;
|
|
1752
|
-
// Phase 1: CEL
|
|
1753
|
-
//
|
|
1754
|
-
//
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
// importer is undefined here and variables/secrets fall back to a permissive `map`
|
|
1759
|
-
// (no false positives) while resources/env stay rejected.
|
|
1760
|
-
const importerModule =
|
|
1761
|
-
m.kind === "Telo.Import"
|
|
1762
|
-
? allManifests.find(
|
|
1763
|
-
(mm) =>
|
|
1764
|
-
(mm.kind === "Telo.Application" || mm.kind === "Telo.Library") &&
|
|
1765
|
-
(mm.metadata as { name?: string } | undefined)?.name ===
|
|
1766
|
-
(m.metadata as { module?: string } | undefined)?.module,
|
|
1767
|
-
)
|
|
1768
|
-
: undefined;
|
|
1769
|
-
const baseTypedEnv =
|
|
1770
|
-
m.kind === "Telo.Import"
|
|
1771
|
-
? buildImportInputCelEnvironment(this.celEnv, importerModule)
|
|
1772
|
-
: buildTypedCelEnvironment(this.celEnv, m, undefined, moduleManifest);
|
|
1773
|
-
const celIssues = collectCelTypeIssues(
|
|
1774
|
-
m,
|
|
1775
|
-
schema,
|
|
1776
|
-
"",
|
|
1777
|
-
definition,
|
|
1778
|
-
m,
|
|
1779
|
-
baseTypedEnv,
|
|
1780
|
-
this.celEnv,
|
|
1781
|
-
moduleManifest,
|
|
1782
|
-
);
|
|
1795
|
+
// Phase 1: collect the pure-CEL leaves and the schema of the slot each
|
|
1796
|
+
// flows into. The expression's own type is resolved later, by the
|
|
1797
|
+
// engine walk that owns type-checking; this half only knows the target.
|
|
1798
|
+
for (const slot of collectCelValueSlots(m, schema, "")) {
|
|
1799
|
+
celReturnSlots.push({ manifest: m, resource, filePath, ...slot });
|
|
1800
|
+
}
|
|
1783
1801
|
// Phase 2+3: AJV on substituted data — CEL fields replaced with typed placeholders
|
|
1784
1802
|
const ajvIssues = validateAgainstSchema(substituteCelFields(m, schema), schema);
|
|
1785
1803
|
// Phase 4: value slots that must satisfy a type declared elsewhere on
|
|
@@ -1791,7 +1809,7 @@ export class StaticAnalyzer {
|
|
|
1791
1809
|
schema,
|
|
1792
1810
|
allManifests as Record<string, any>[],
|
|
1793
1811
|
);
|
|
1794
|
-
const issues = [...
|
|
1812
|
+
const issues = [...ajvIssues, ...valueSchemaIssues];
|
|
1795
1813
|
for (const issue of issues) {
|
|
1796
1814
|
diagnostics.push({
|
|
1797
1815
|
severity: DiagnosticSeverity.Error,
|
|
@@ -1988,12 +2006,15 @@ export class StaticAnalyzer {
|
|
|
1988
2006
|
defs,
|
|
1989
2007
|
aliases,
|
|
1990
2008
|
{ aliasesByModule, rootModules },
|
|
2009
|
+
celStepContextSchema,
|
|
1991
2010
|
)) {
|
|
1992
2011
|
diagnostics.push({
|
|
1993
2012
|
severity: DiagnosticSeverity.Error,
|
|
1994
|
-
code: "CONTRACT_INPUTS_MISMATCH",
|
|
2013
|
+
code: issue.code ?? "CONTRACT_INPUTS_MISMATCH",
|
|
1995
2014
|
source: SOURCE,
|
|
1996
|
-
message:
|
|
2015
|
+
message: issue.code
|
|
2016
|
+
? `${m.kind}/${stepName}: inputs at '${issue.path}' flow into ${issue.targetLabel} with disagreeing type arguments: ${issue.message}`
|
|
2017
|
+
: `${m.kind}/${stepName}: inputs at '${issue.path}' do not satisfy ${issue.targetLabel}'s declared inputType: ${issue.message}`,
|
|
1997
2018
|
data: {
|
|
1998
2019
|
resource: { kind: m.kind, name: stepName ?? "" },
|
|
1999
2020
|
filePath: stepFile,
|
|
@@ -2244,42 +2265,128 @@ export class StaticAnalyzer {
|
|
|
2244
2265
|
});
|
|
2245
2266
|
return;
|
|
2246
2267
|
}
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2268
|
+
// The engine type-checks, so it gets the environment typed for THIS
|
|
2269
|
+
// path — not the bare base one. A `Telo.Import`'s variables/secrets
|
|
2270
|
+
// are a config-only contract evaluated in the IMPORTING module's
|
|
2271
|
+
// scope, so they type from the owning module doc and drop
|
|
2272
|
+
// `resources`/`env`, making a reference to either an error.
|
|
2273
|
+
//
|
|
2274
|
+
// Cached per manifest when no `x-telo-context` applied, which is most
|
|
2275
|
+
// expressions: the environment then depends only on the manifest, so
|
|
2276
|
+
// rebuilding it per expression is pure waste — a clone plus a
|
|
2277
|
+
// re-registration of every variable, on every keystroke in the IDE.
|
|
2278
|
+
// A matched context makes the environment path-specific (its schema is
|
|
2279
|
+
// resolved against the enclosing array item), so those still build
|
|
2280
|
+
// fresh rather than risk one item's types leaking into another's.
|
|
2281
|
+
const cached = effectiveContext === null ? typedEnvByManifest.get(m) : undefined;
|
|
2282
|
+
const typedEnv =
|
|
2283
|
+
cached ??
|
|
2284
|
+
(m.kind === "Telo.Import"
|
|
2285
|
+
? buildImportInputCelEnvironment(
|
|
2286
|
+
this.celEnv,
|
|
2287
|
+
allManifests.find(
|
|
2288
|
+
(mm) =>
|
|
2289
|
+
(mm.kind === "Telo.Application" || mm.kind === "Telo.Library") &&
|
|
2290
|
+
(mm.metadata as { name?: string } | undefined)?.name ===
|
|
2291
|
+
(m.metadata as { module?: string } | undefined)?.module,
|
|
2292
|
+
),
|
|
2293
|
+
)
|
|
2294
|
+
: buildTypedCelEnvironment(
|
|
2295
|
+
this.celEnv,
|
|
2296
|
+
m,
|
|
2297
|
+
effectiveContext ?? undefined,
|
|
2298
|
+
moduleManifest,
|
|
2299
|
+
));
|
|
2300
|
+
if (effectiveContext === null && !cached) typedEnvByManifest.set(m, typedEnv);
|
|
2301
|
+
|
|
2302
|
+
const result = engine.analyze(expr, { celEnv: typedEnv, contextSchema: effectiveContext });
|
|
2303
|
+
|
|
2304
|
+
if (result.type !== undefined) {
|
|
2305
|
+
let byPath = celTypeByPath.get(m);
|
|
2306
|
+
if (!byPath) celTypeByPath.set(m, (byPath = new Map()));
|
|
2307
|
+
byPath.set(path, result.type);
|
|
2308
|
+
}
|
|
2309
|
+
|
|
2310
|
+
// The producer half of the type-argument check. A CEL type is a bare
|
|
2311
|
+
// name — cel-js types by constructor identity, so a byte stream and a
|
|
2312
|
+
// string stream are one CEL type and always will be — so the argument
|
|
2313
|
+
// comparison is a parallel pass over the SCHEMAS the analyzer already
|
|
2314
|
+
// walks. This is where the producer's schema is in hand: navigating
|
|
2315
|
+
// the expression's chain into its context schema is exactly "the
|
|
2316
|
+
// producer's outputType at the expression's tail".
|
|
2317
|
+
//
|
|
2318
|
+
// Only a PLAIN CHAIN is navigated — `steps.encode.result.output`, the
|
|
2319
|
+
// shape a wiring site actually takes. An expression that computes
|
|
2320
|
+
// rather than names has no schema to read off the context, so it
|
|
2321
|
+
// records nothing and no argument check fires: silence where the
|
|
2322
|
+
// analyzer knows least is the conservative direction, and the same one
|
|
2323
|
+
// `x-telo-step-context`'s pure-`value` typing takes.
|
|
2324
|
+
const chain = effectiveContext ? plainChainOf(`\${{${expr}}}`) : undefined;
|
|
2325
|
+
if (chain) {
|
|
2326
|
+
const produced = navigateSchemaToExprPath(effectiveContext!, chain);
|
|
2327
|
+
if (produced) {
|
|
2328
|
+
let byPath = celSourceSchemaByPath.get(m);
|
|
2329
|
+
if (!byPath) celSourceSchemaByPath.set(m, (byPath = new Map()));
|
|
2330
|
+
byPath.set(path, produced);
|
|
2331
|
+
}
|
|
2332
|
+
}
|
|
2333
|
+
|
|
2334
|
+
// A non-deterministic call in a compile-eval field is baked once at
|
|
2335
|
+
// load: `nowIso()` there freezes at boot. Sometimes that is the
|
|
2336
|
+
// intent (a boot timestamp, a run id), so it warns rather than
|
|
2337
|
+
// blocking. The engine reports which calls re-evaluate; the eval mode
|
|
2338
|
+
// is manifest policy and stays here.
|
|
2339
|
+
if (celRuleApplies && evalPathsCover(celCompilePaths, path)) {
|
|
2340
|
+
const volatile = [
|
|
2341
|
+
...new Set(result.calls.filter((c) => c.deterministic === false).map((c) => c.name)),
|
|
2342
|
+
].sort();
|
|
2343
|
+
if (volatile.length > 0) {
|
|
2250
2344
|
diagnostics.push({
|
|
2251
|
-
severity: DiagnosticSeverity.
|
|
2252
|
-
code: "
|
|
2345
|
+
severity: DiagnosticSeverity.Warning,
|
|
2346
|
+
code: "CEL_NONDETERMINISTIC_IN_COMPILE_FIELD",
|
|
2253
2347
|
source: SOURCE,
|
|
2254
|
-
message:
|
|
2348
|
+
message: `${m.kind}/${resource.name}: '${path}' is evaluated once at startup, so ${volatile.map((n) => `\`${n}()\``).join(", ")} ${volatile.length === 1 ? "is" : "are"} baked in at load and never re-evaluated. Move the expression to a field evaluated per call (x-telo-eval: runtime) if it should change over time.`,
|
|
2255
2349
|
data: { resource, filePath, path },
|
|
2256
2350
|
});
|
|
2257
|
-
}
|
|
2351
|
+
}
|
|
2352
|
+
}
|
|
2353
|
+
|
|
2354
|
+
for (const f of result.diagnostics) {
|
|
2355
|
+
// A repair is applicable only when the analyzed expression covers
|
|
2356
|
+
// the whole scalar. For one `${{ }}` among literal text, replacing
|
|
2357
|
+
// the node would drop the text around it, so the correction stays
|
|
2358
|
+
// in the message and no fix is stamped.
|
|
2359
|
+
const fix = e.surface.whole ? rewrapFix(f.fix, e.surface.wrapper) : undefined;
|
|
2360
|
+
const data = { resource, filePath, path, ...(fix ? { fix } : {}) };
|
|
2361
|
+
if (f.code === "CEL_SYNTAX_ERROR") {
|
|
2258
2362
|
diagnostics.push({
|
|
2259
2363
|
severity: DiagnosticSeverity.Error,
|
|
2260
|
-
code: "
|
|
2364
|
+
code: "CEL_SYNTAX_ERROR",
|
|
2261
2365
|
source: SOURCE,
|
|
2262
|
-
message:
|
|
2263
|
-
data
|
|
2366
|
+
message: `CEL syntax error at ${path}: ${f.message}`,
|
|
2367
|
+
data,
|
|
2264
2368
|
});
|
|
2265
|
-
} else if (f.code ===
|
|
2369
|
+
} else if (f.code === undefined) {
|
|
2370
|
+
// No code from a future engine — pass the message through, tagged
|
|
2371
|
+
// with a generic ENGINE_DIAGNOSTIC code so downstream filters can
|
|
2372
|
+
// still bucket it.
|
|
2266
2373
|
diagnostics.push({
|
|
2267
2374
|
severity: DiagnosticSeverity.Error,
|
|
2268
|
-
code: "
|
|
2375
|
+
code: "ENGINE_DIAGNOSTIC",
|
|
2269
2376
|
source: SOURCE,
|
|
2270
|
-
message: `${m.kind}/${resource.name}:
|
|
2271
|
-
data
|
|
2377
|
+
message: `${m.kind}/${resource.name}: !${engineName} at '${path}': ${f.message}`,
|
|
2378
|
+
data,
|
|
2272
2379
|
});
|
|
2273
2380
|
} else {
|
|
2274
|
-
//
|
|
2275
|
-
//
|
|
2276
|
-
//
|
|
2381
|
+
// Named by ENGINE, not hardcoded to CEL: the seam exists so a
|
|
2382
|
+
// second engine can produce coded findings, and labelling them
|
|
2383
|
+
// `CEL` would misattribute the first one that does.
|
|
2277
2384
|
diagnostics.push({
|
|
2278
2385
|
severity: DiagnosticSeverity.Error,
|
|
2279
|
-
code: f.code
|
|
2386
|
+
code: f.code,
|
|
2280
2387
|
source: SOURCE,
|
|
2281
2388
|
message: `${m.kind}/${resource.name}: !${engineName} at '${path}': ${f.message}`,
|
|
2282
|
-
data
|
|
2389
|
+
data,
|
|
2283
2390
|
});
|
|
2284
2391
|
}
|
|
2285
2392
|
}
|
|
@@ -2288,6 +2395,46 @@ export class StaticAnalyzer {
|
|
|
2288
2395
|
{ aliases },
|
|
2289
2396
|
);
|
|
2290
2397
|
|
|
2398
|
+
// The two halves of "does this expression fit the slot it flows into" meet
|
|
2399
|
+
// here: the engine resolved the expression's type during the walk above,
|
|
2400
|
+
// and the schema walk recorded the slot. An expression that failed to check
|
|
2401
|
+
// recorded no type and is already reported by its own diagnostic.
|
|
2402
|
+
for (const slot of celReturnSlots) {
|
|
2403
|
+
const type = celTypeByPath.get(slot.manifest)?.get(slot.path);
|
|
2404
|
+
if (type === undefined) continue;
|
|
2405
|
+
if (!celTypeSatisfiesJsonSchema(type.split("<")[0]!, slot.schema)) {
|
|
2406
|
+
const expected = slot.schema["x-telo-type"] ?? slot.schema.type ?? "unknown";
|
|
2407
|
+
diagnostics.push({
|
|
2408
|
+
severity: DiagnosticSeverity.Error,
|
|
2409
|
+
code: "CEL_TYPE_ERROR",
|
|
2410
|
+
source: SOURCE,
|
|
2411
|
+
message: `${slot.resource.kind}/${slot.resource.name}: CEL at '${slot.path}' returns '${type}' but the field expects '${expected}'.`,
|
|
2412
|
+
data: { resource: slot.resource, filePath: slot.filePath, path: slot.path },
|
|
2413
|
+
});
|
|
2414
|
+
continue;
|
|
2415
|
+
}
|
|
2416
|
+
// The type fits; do its ARGUMENTS agree? Covariant and gradual — an
|
|
2417
|
+
// omitted argument is *any* in both directions, so an unmigrated producer
|
|
2418
|
+
// or consumer is never reported, and only a definite conflict is.
|
|
2419
|
+
const produced = celSourceSchemaByPath.get(slot.manifest)?.get(slot.path);
|
|
2420
|
+
if (!produced) continue;
|
|
2421
|
+
const { compatible, issues } = checkSchemaCompatibility(
|
|
2422
|
+
produced,
|
|
2423
|
+
slot.schema,
|
|
2424
|
+
(ref: string) => defs.schemaForId(ref),
|
|
2425
|
+
);
|
|
2426
|
+
if (compatible) continue;
|
|
2427
|
+
diagnostics.push({
|
|
2428
|
+
severity: DiagnosticSeverity.Error,
|
|
2429
|
+
code: "CEL_TYPE_ARGUMENT_MISMATCH",
|
|
2430
|
+
source: SOURCE,
|
|
2431
|
+
message:
|
|
2432
|
+
`${slot.resource.kind}/${slot.resource.name}: CEL at '${slot.path}' produces a value ` +
|
|
2433
|
+
`whose type arguments disagree with the field's: ${issues.join("; ")}.`,
|
|
2434
|
+
data: { resource: slot.resource, filePath: slot.filePath, path: slot.path },
|
|
2435
|
+
});
|
|
2436
|
+
}
|
|
2437
|
+
|
|
2291
2438
|
// Validate resource references (Phase 3)
|
|
2292
2439
|
diagnostics.push(
|
|
2293
2440
|
...validateReferences(allManifests, { aliases, definitions: defs, aliasesByModule }),
|
package/src/cel-environment.ts
CHANGED
|
@@ -3,10 +3,13 @@ import type { ResourceManifest } from "@telorun/sdk";
|
|
|
3
3
|
import { jsonSchemaToCelType, VALUE_BRAND_BASE } from "./schema-compat.js";
|
|
4
4
|
|
|
5
5
|
/** Transport protocol on a `ports` entry → the nominal CEL brand its resolved
|
|
6
|
-
* value carries. Mirrors the `protocol` enum in the Application schema
|
|
6
|
+
* value carries. Mirrors the `protocol` enum in the Application schema, and
|
|
7
|
+
* names the value types by their canonical `Telo.`-qualified names — the same
|
|
8
|
+
* spelling an author writes at `x-telo-type`, so a branded port and a branded
|
|
9
|
+
* field are comparable by name with nothing in between to translate. */
|
|
7
10
|
const PORT_PROTOCOL_BRAND: Record<string, string> = {
|
|
8
|
-
tcp: "TcpPort",
|
|
9
|
-
udp: "UdpPort",
|
|
11
|
+
tcp: "Telo.TcpPort",
|
|
12
|
+
udp: "Telo.UdpPort",
|
|
10
13
|
};
|
|
11
14
|
|
|
12
15
|
export { buildCelEnvironment } from "@telorun/templating";
|
|
@@ -127,6 +127,16 @@ export class DefinitionRegistry {
|
|
|
127
127
|
return this.registeredSchemaIds.has(id) || this.ajv.getSchema(id) !== undefined;
|
|
128
128
|
}
|
|
129
129
|
|
|
130
|
+
/** The schema registered under `id`, for a structural comparison that must see
|
|
131
|
+
* THROUGH a named shape. Declaring a shape once and referencing it is the
|
|
132
|
+
* sanctioned way to reuse one, so a comparator that cannot follow the
|
|
133
|
+
* reference judges two opaque nodes and learns nothing. */
|
|
134
|
+
schemaForId(id: string): Record<string, any> | undefined {
|
|
135
|
+
const compiled = this.ajv.getSchema(id);
|
|
136
|
+
const schema = compiled?.schema;
|
|
137
|
+
return schema && typeof schema === "object" ? (schema as Record<string, any>) : undefined;
|
|
138
|
+
}
|
|
139
|
+
|
|
130
140
|
/** Validates data against a schema using this registry's AJV instance, which has all
|
|
131
141
|
* registered definition schemas loaded — enabling cross-module $ref resolution.
|
|
132
142
|
* A compile failure returns `[]` here; it is surfaced loudly (once, on the
|