@telorun/analyzer 0.61.0 → 0.62.1
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.map +1 -1
- package/dist/analyzer.js +130 -9
- package/dist/builtins.d.ts.map +1 -1
- package/dist/builtins.js +69 -12
- package/dist/cel-bindings.d.ts +0 -6
- package/dist/cel-bindings.d.ts.map +1 -1
- package/dist/cel-bindings.js +3 -28
- package/dist/definition-registry.d.ts +17 -0
- package/dist/definition-registry.d.ts.map +1 -1
- package/dist/definition-registry.js +31 -2
- package/dist/identifier-name.d.ts +114 -0
- package/dist/identifier-name.d.ts.map +1 -0
- package/dist/identifier-name.js +183 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -0
- package/dist/manifest-schemas.d.ts +81 -0
- package/dist/manifest-schemas.d.ts.map +1 -1
- package/dist/manifest-schemas.js +208 -6
- package/dist/release/payload-digest.d.ts +7 -3
- package/dist/release/payload-digest.d.ts.map +1 -1
- package/dist/release/payload-digest.js +7 -3
- package/dist/requires-block.d.ts +125 -0
- package/dist/requires-block.d.ts.map +1 -0
- package/dist/requires-block.js +182 -0
- package/dist/schema-keywords.d.ts +68 -0
- package/dist/schema-keywords.d.ts.map +1 -0
- package/dist/schema-keywords.js +324 -0
- package/dist/schema-region.d.ts +12 -1
- package/dist/schema-region.d.ts.map +1 -1
- package/dist/schema-region.js +12 -1
- package/dist/telo-version.d.ts +3 -0
- package/dist/telo-version.d.ts.map +1 -0
- package/dist/telo-version.js +8 -0
- package/dist/types.d.ts +31 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/validate-identifier-names.d.ts +31 -0
- package/dist/validate-identifier-names.d.ts.map +1 -0
- package/dist/validate-identifier-names.js +144 -0
- package/dist/validate-observed-state.d.ts +9 -2
- package/dist/validate-observed-state.d.ts.map +1 -1
- package/dist/validate-observed-state.js +9 -2
- package/dist/validate-references.d.ts.map +1 -1
- package/dist/validate-references.js +5 -26
- package/dist/validate-requires.d.ts +49 -0
- package/dist/validate-requires.d.ts.map +1 -0
- package/dist/validate-requires.js +99 -0
- package/dist/value-type-keyword.d.ts +1 -1
- package/dist/value-type-keyword.d.ts.map +1 -1
- package/dist/value-type-keyword.js +1 -0
- package/dist/version-range.d.ts +88 -0
- package/dist/version-range.d.ts.map +1 -0
- package/dist/version-range.js +173 -0
- package/package.json +2 -2
- package/src/analyzer.ts +146 -10
- package/src/builtins.ts +73 -12
- package/src/cel-bindings.ts +3 -28
- package/src/definition-registry.ts +30 -2
- package/src/identifier-name.ts +228 -0
- package/src/index.ts +34 -0
- package/src/manifest-schemas.ts +223 -4
- package/src/release/payload-digest.ts +7 -3
- package/src/requires-block.ts +253 -0
- package/src/schema-keywords.ts +359 -0
- package/src/schema-region.ts +12 -1
- package/src/telo-version.ts +9 -0
- package/src/types.ts +32 -0
- package/src/validate-identifier-names.ts +173 -0
- package/src/validate-observed-state.ts +9 -2
- package/src/validate-references.ts +5 -26
- package/src/validate-requires.ts +129 -0
- package/src/value-type-keyword.ts +1 -0
- package/src/version-range.ts +238 -0
package/src/analyzer.ts
CHANGED
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
plainChainOf,
|
|
9
9
|
type CelSurface,
|
|
10
10
|
} from "@telorun/templating";
|
|
11
|
-
import type { DiagnosticFix } from "./types.js";
|
|
11
|
+
import type { DiagnosticData, DiagnosticFix } from "./types.js";
|
|
12
12
|
import {
|
|
13
13
|
AliasResolver,
|
|
14
14
|
moduleScopedDefResolver,
|
|
@@ -89,17 +89,19 @@ import {
|
|
|
89
89
|
BINDINGS_ANNOTATION,
|
|
90
90
|
bindingContextProperties,
|
|
91
91
|
bindingPathChain,
|
|
92
|
-
CEL_RESERVED_WORDS,
|
|
93
92
|
findBindingSites,
|
|
94
93
|
resolveBindingOrder,
|
|
95
94
|
schemaAtChain,
|
|
96
95
|
type BindingSites,
|
|
97
96
|
} from "./cel-bindings.js";
|
|
97
|
+
import { CEL_RESERVED_WORDS, checkName } from "./identifier-name.js";
|
|
98
|
+
import { validateIdentifierNames } from "./validate-identifier-names.js";
|
|
98
99
|
import { validateExtends } from "./validate-extends.js";
|
|
99
100
|
import { validateLogging } from "./validate-logging.js";
|
|
100
101
|
import { validateModuleArtifact } from "./validate-module-artifact.js";
|
|
101
102
|
import { validateIncludePlacement } from "./validate-include-placement.js";
|
|
102
103
|
import { validateModuleMetadata } from "./validate-module-metadata.js";
|
|
104
|
+
import { validateRequires } from "./validate-requires.js";
|
|
103
105
|
import { validateBaseMapping } from "./validate-base-mapping.js";
|
|
104
106
|
import { validateInvocationContract } from "./validate-invocation-contract.js";
|
|
105
107
|
import { collectStepInputIssues } from "./validate-step-inputs.js";
|
|
@@ -1029,6 +1031,86 @@ export interface StaticAnalyzerOptions {
|
|
|
1029
1031
|
celHandlers?: CelHandlers;
|
|
1030
1032
|
}
|
|
1031
1033
|
|
|
1034
|
+
/**
|
|
1035
|
+
* Files belonging to a module this runtime declared itself unable to read.
|
|
1036
|
+
*
|
|
1037
|
+
* Attribution is by FILE rather than by resource identity, the same choice
|
|
1038
|
+
* `remapMigratedPaths` makes and for the same reason: a diagnostic carries at
|
|
1039
|
+
* most two routing facts and routinely only one, so indexing by resource would
|
|
1040
|
+
* leave every diagnostic without `data.resource` unreachable.
|
|
1041
|
+
*
|
|
1042
|
+
* **A module NAME is not a graph-unique key.** Names are module-scoped, so two
|
|
1043
|
+
* libraries may both be called `Store` — CLAUDE.md names this exact hazard for
|
|
1044
|
+
* migration provenance ("two libraries declaring a Store would share one
|
|
1045
|
+
* bucket"), where the answer is to narrow or not remap at all. Same answer here:
|
|
1046
|
+
* the gated doc's own `metadata.source` is always suppressed, and a name is used
|
|
1047
|
+
* to reach its `include:` partials only when that name identifies exactly ONE
|
|
1048
|
+
* module doc in the set. Where it does not, the partials keep their diagnostics
|
|
1049
|
+
* rather than risk silencing an unrelated library's — a stray extra diagnostic is
|
|
1050
|
+
* a far cheaper failure than a hidden one.
|
|
1051
|
+
*
|
|
1052
|
+
* A module doc with no `source` contributes nothing — suppressing on a guess
|
|
1053
|
+
* would hide diagnostics belonging to files nobody named.
|
|
1054
|
+
*/
|
|
1055
|
+
function filesOfUnreadableModules(
|
|
1056
|
+
manifests: ResourceManifest[],
|
|
1057
|
+
requiresDiagnostics: AnalysisDiagnostic[],
|
|
1058
|
+
): ReadonlySet<string> {
|
|
1059
|
+
const files = new Set<string>();
|
|
1060
|
+
const gatedNames = new Set<string>();
|
|
1061
|
+
|
|
1062
|
+
for (const d of requiresDiagnostics) {
|
|
1063
|
+
if (d.code !== "MODULE_REQUIRES_NEWER_RUNTIME") continue;
|
|
1064
|
+
const data = d.data as DiagnosticData | undefined;
|
|
1065
|
+
// The gated document itself, addressed by the file it was declared in.
|
|
1066
|
+
if (typeof data?.filePath === "string" && data.filePath) files.add(data.filePath);
|
|
1067
|
+
const name = data?.resource?.name;
|
|
1068
|
+
if (typeof name === "string") gatedNames.add(name);
|
|
1069
|
+
}
|
|
1070
|
+
if (files.size === 0 && gatedNames.size === 0) return files;
|
|
1071
|
+
|
|
1072
|
+
// How many module docs answer to each gated name — the ambiguity test.
|
|
1073
|
+
const docsPerName = new Map<string, number>();
|
|
1074
|
+
for (const m of manifests) {
|
|
1075
|
+
if (m.kind !== "Telo.Application" && m.kind !== "Telo.Library") continue;
|
|
1076
|
+
const name = (m.metadata as { name?: string } | undefined)?.name;
|
|
1077
|
+
if (typeof name === "string" && gatedNames.has(name)) {
|
|
1078
|
+
docsPerName.set(name, (docsPerName.get(name) ?? 0) + 1);
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
for (const m of manifests) {
|
|
1083
|
+
const metadata = (m.metadata ?? {}) as Record<string, unknown>;
|
|
1084
|
+
const owner = metadata.module;
|
|
1085
|
+
if (typeof owner !== "string" || !gatedNames.has(owner)) continue;
|
|
1086
|
+
if (docsPerName.get(owner) !== 1) continue; // ambiguous — do not guess
|
|
1087
|
+
if (typeof metadata.source === "string" && metadata.source) files.add(metadata.source);
|
|
1088
|
+
}
|
|
1089
|
+
return files;
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
/**
|
|
1093
|
+
* Drop every diagnostic anchored in a file whose module this runtime cannot
|
|
1094
|
+
* read, except the gate diagnostic itself.
|
|
1095
|
+
*
|
|
1096
|
+
* A filter rather than a guard on each validator: threading "skip this module"
|
|
1097
|
+
* through thirty validators would make each one responsible for a rule none of
|
|
1098
|
+
* them owns, and a validator added later would silently opt out of it. A
|
|
1099
|
+
* diagnostic with no `filePath` is KEPT — suppression must never be the default
|
|
1100
|
+
* for something it cannot attribute.
|
|
1101
|
+
*/
|
|
1102
|
+
function suppressUnreadableModuleDiagnostics(
|
|
1103
|
+
diagnostics: AnalysisDiagnostic[],
|
|
1104
|
+
unreadableFiles: ReadonlySet<string>,
|
|
1105
|
+
): AnalysisDiagnostic[] {
|
|
1106
|
+
if (unreadableFiles.size === 0) return diagnostics;
|
|
1107
|
+
return diagnostics.filter((d) => {
|
|
1108
|
+
if (d.code === "MODULE_REQUIRES_NEWER_RUNTIME") return true;
|
|
1109
|
+
const filePath = (d.data as DiagnosticData | undefined)?.filePath;
|
|
1110
|
+
return typeof filePath !== "string" || !unreadableFiles.has(filePath);
|
|
1111
|
+
});
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1032
1114
|
export class StaticAnalyzer {
|
|
1033
1115
|
private readonly celEnv: Environment;
|
|
1034
1116
|
|
|
@@ -1527,6 +1609,25 @@ export class StaticAnalyzer {
|
|
|
1527
1609
|
});
|
|
1528
1610
|
}
|
|
1529
1611
|
}
|
|
1612
|
+
// Declared runtime requirements, FIRST among the validators and suppressing
|
|
1613
|
+
// the rest for any module this runtime cannot read. A module that adopted
|
|
1614
|
+
// newer syntax also produces the vocabulary errors that syntax causes here —
|
|
1615
|
+
// an unknown `use` token, an object where a zone annotation expects a
|
|
1616
|
+
// pointer, an `additionalProperties` violation against a kernel-owned
|
|
1617
|
+
// schema — every one of which is true and blames the module's author for a
|
|
1618
|
+
// version skew. Reporting them beside the gate would bury the one message
|
|
1619
|
+
// that names the actual cause and the actual fix.
|
|
1620
|
+
const requiresDiagnostics = validateRequires(allManifests as unknown as ResourceManifest[], {
|
|
1621
|
+
teloVersion: options?.teloVersion,
|
|
1622
|
+
hostVersions: options?.hostVersions,
|
|
1623
|
+
entryModules: rootModules,
|
|
1624
|
+
});
|
|
1625
|
+
const unreadableFiles = filesOfUnreadableModules(
|
|
1626
|
+
allManifests as unknown as ResourceManifest[],
|
|
1627
|
+
requiresDiagnostics,
|
|
1628
|
+
);
|
|
1629
|
+
diagnostics.push(...requiresDiagnostics);
|
|
1630
|
+
|
|
1530
1631
|
if (!options?.skipValidation) {
|
|
1531
1632
|
diagnostics.push(
|
|
1532
1633
|
...validateSchemaTypeRefs(allManifests, defs, aliases, aliasesByModule, rootModules),
|
|
@@ -1543,6 +1644,19 @@ export class StaticAnalyzer {
|
|
|
1543
1644
|
// these fields, which is precisely why they need a check: a mistyped one
|
|
1544
1645
|
// has no runtime failure mode that would ever surface it.
|
|
1545
1646
|
diagnostics.push(...validateModuleMetadata(allManifests, defs, aliases));
|
|
1647
|
+
// Every author-written name. Telo has no lexer, so a name's shape is
|
|
1648
|
+
// unchecked where it is declared and its consequences land at whichever
|
|
1649
|
+
// CEL site reads it — for a hyphen, sometimes as silent arithmetic. Takes
|
|
1650
|
+
// the call graph for step names rather than re-walking the step arrays.
|
|
1651
|
+
diagnostics.push(
|
|
1652
|
+
...validateIdentifierNames(
|
|
1653
|
+
allManifests as unknown as ResourceManifest[],
|
|
1654
|
+
defs,
|
|
1655
|
+
aliases,
|
|
1656
|
+
rootModules,
|
|
1657
|
+
getCallGraph(),
|
|
1658
|
+
),
|
|
1659
|
+
);
|
|
1546
1660
|
// A file embed resolves at resource creation, so one written on a doc that
|
|
1547
1661
|
// is never instantiated is read by nothing and would ship silently.
|
|
1548
1662
|
diagnostics.push(...validateIncludePlacement(allManifests));
|
|
@@ -1556,7 +1670,7 @@ export class StaticAnalyzer {
|
|
|
1556
1670
|
// normalisation have already run above; that's all downstream
|
|
1557
1671
|
// consumers (prepare, init loop) require.
|
|
1558
1672
|
if (options?.skipValidation) {
|
|
1559
|
-
return diagnostics;
|
|
1673
|
+
return suppressUnreadableModuleDiagnostics(diagnostics, unreadableFiles);
|
|
1560
1674
|
}
|
|
1561
1675
|
|
|
1562
1676
|
// Build a name→manifest map for looking up referenced resources
|
|
@@ -2109,14 +2223,33 @@ export class StaticAnalyzer {
|
|
|
2109
2223
|
|
|
2110
2224
|
for (const name of Object.keys(declared)) {
|
|
2111
2225
|
const shadows = inScope.has(name);
|
|
2112
|
-
if (
|
|
2226
|
+
if (shadows || keywords.has(name)) {
|
|
2227
|
+
diagnostics.push({
|
|
2228
|
+
severity: DiagnosticSeverity.Error,
|
|
2229
|
+
code: "BINDING_NAME_RESERVED",
|
|
2230
|
+
source: SOURCE,
|
|
2231
|
+
message: shadows
|
|
2232
|
+
? `${m.kind}/${bindingsName}: binding '${name}' shadows a variable already in scope here (${[...inScope].sort().join(", ")}). Rename the binding — a scope variable always wins, so this one would never be read.`
|
|
2233
|
+
: `${m.kind}/${bindingsName}: binding '${name}' is a CEL keyword, so no expression can read it as a reference. Rename the binding.`,
|
|
2234
|
+
data: {
|
|
2235
|
+
resource: resourceRef,
|
|
2236
|
+
filePath: bindingsFile,
|
|
2237
|
+
path: `${celBindingSites.field}.${name}`,
|
|
2238
|
+
},
|
|
2239
|
+
});
|
|
2240
|
+
continue;
|
|
2241
|
+
}
|
|
2242
|
+
// A binding is read by bare name, so it lives in the same
|
|
2243
|
+
// identifier space as a resource or step name and breaks the
|
|
2244
|
+
// same way. The keyword tier is unreachable here — the check
|
|
2245
|
+
// above owns it, and can also say what is being shadowed.
|
|
2246
|
+
const violation = checkName(name, "value", "binding name");
|
|
2247
|
+
if (!violation) continue;
|
|
2113
2248
|
diagnostics.push({
|
|
2114
|
-
severity:
|
|
2115
|
-
code:
|
|
2249
|
+
severity: violation.severity,
|
|
2250
|
+
code: violation.code,
|
|
2116
2251
|
source: SOURCE,
|
|
2117
|
-
message:
|
|
2118
|
-
? `${m.kind}/${bindingsName}: binding '${name}' shadows a variable already in scope here (${[...inScope].sort().join(", ")}). Rename the binding — a scope variable always wins, so this one would never be read.`
|
|
2119
|
-
: `${m.kind}/${bindingsName}: binding '${name}' is a CEL keyword, so no expression can read it as a reference. Rename the binding.`,
|
|
2252
|
+
message: `${m.kind}/${bindingsName}: ${violation.message}`,
|
|
2120
2253
|
data: {
|
|
2121
2254
|
resource: resourceRef,
|
|
2122
2255
|
filePath: bindingsFile,
|
|
@@ -2514,7 +2647,10 @@ export class StaticAnalyzer {
|
|
|
2514
2647
|
|
|
2515
2648
|
// Reroute diagnostics on synthetic (inline-extracted) resources back to
|
|
2516
2649
|
// the chain root so position-index lookups land on the parent doc.
|
|
2517
|
-
return rewriteSyntheticOrigins(
|
|
2650
|
+
return rewriteSyntheticOrigins(
|
|
2651
|
+
suppressUnreadableModuleDiagnostics(diagnostics, unreadableFiles),
|
|
2652
|
+
allManifests,
|
|
2653
|
+
);
|
|
2518
2654
|
}
|
|
2519
2655
|
|
|
2520
2656
|
analyzeErrors(
|
package/src/builtins.ts
CHANGED
|
@@ -1,6 +1,29 @@
|
|
|
1
|
-
import { manifestFragment } from "./manifest-schemas.js";
|
|
1
|
+
import { manifestFragment, manifestFragmentRef, withSchemaFragments } from "./manifest-schemas.js";
|
|
2
|
+
|
|
3
|
+
/** A slot holding author-written JSON Schema. Localized and hoisted by
|
|
4
|
+
* {@link withSchemaFragments} on the enclosing schema, which is what makes the
|
|
5
|
+
* `#/$defs` pointer resolve inside whatever AJV compiles.
|
|
6
|
+
*
|
|
7
|
+
* `KindSchema` and `JsonSchema7` share a body; the name is the discriminator
|
|
8
|
+
* the IDE reads off the `x-telo-fragment` stamp to decide whether the
|
|
9
|
+
* `x-telo-*` vocabulary belongs here. A kind's own `schema:` is where it does;
|
|
10
|
+
* a `status:` block or an `inputType:` describes plain data, where it does not. */
|
|
11
|
+
const kindSchemaSlot = {
|
|
12
|
+
title: "Schema",
|
|
13
|
+
description: "Configuration this kind accepts, as JSON Schema plus `x-telo-*` annotations.",
|
|
14
|
+
$ref: manifestFragmentRef("KindSchema"),
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/** Observed state a kind reports while running, as a data schema. `required:` is
|
|
18
|
+
* rejected separately by `validateObservedStateDeclarations`, which can say why
|
|
19
|
+
* and what to write instead. */
|
|
20
|
+
const observedStateSlot = {
|
|
21
|
+
title: "Observed state",
|
|
22
|
+
description:
|
|
23
|
+
"What a resource of this kind reports while running, published at `resources.<name>.status.<field>`.",
|
|
24
|
+
$ref: manifestFragmentRef("JsonSchema7"),
|
|
25
|
+
};
|
|
2
26
|
import type { ResourceDefinition } from "@telorun/sdk";
|
|
3
|
-
import { OBSERVED_STATE_SCHEMA } from "./validate-observed-state.js";
|
|
4
27
|
|
|
5
28
|
/** Descriptive provenance a module declares about itself, shared by
|
|
6
29
|
* `Telo.Application` and `Telo.Library`.
|
|
@@ -24,6 +47,32 @@ const PROVENANCE_METADATA = {
|
|
|
24
47
|
documentation: { type: "string" },
|
|
25
48
|
};
|
|
26
49
|
|
|
50
|
+
/** The declared runtime requirements block, shared by `Telo.Application` and
|
|
51
|
+
* `Telo.Library` — the two module kinds, whose schemas are otherwise
|
|
52
|
+
* independent and would drift.
|
|
53
|
+
*
|
|
54
|
+
* **Deliberately says only "these are objects", and nothing about the values.**
|
|
55
|
+
* The grammar belongs to `requires-block.ts`, which is the single reader, and
|
|
56
|
+
* every rule that matters — `^` and `~` refused, a bare version refused, bounds
|
|
57
|
+
* that must not exclude each other, an upper bound that must name a version that
|
|
58
|
+
* exists — needs a parse and a comparison, not a schema. Adding `type: "string"`
|
|
59
|
+
* here bought nothing and cost a duplicate: `telo: 80` then produced BOTH a
|
|
60
|
+
* `SCHEMA_VIOLATION` and a `REQUIRES_INVALID` for one node, one of them phrased
|
|
61
|
+
* by a layer that does not know what the value is for.
|
|
62
|
+
*
|
|
63
|
+
* Left open at both tiers for the same reason. An unrecognized axis is reported
|
|
64
|
+
* by the reader with the vocabulary it knows — a far better message than AJV's —
|
|
65
|
+
* and, critically, is SUPPRESSED while the `telo` requirement is itself unmet,
|
|
66
|
+
* since an older runtime not knowing a newer axis is a consequence of the
|
|
67
|
+
* version skew rather than a second defect. AJV cannot express that ordering. */
|
|
68
|
+
const REQUIRES_SCHEMA = {
|
|
69
|
+
type: "object",
|
|
70
|
+
properties: {
|
|
71
|
+
host: { type: "object" },
|
|
72
|
+
},
|
|
73
|
+
additionalProperties: true,
|
|
74
|
+
};
|
|
75
|
+
|
|
27
76
|
/** Author-declared subset of `files:` that ships in the artifact's lazily
|
|
28
77
|
* materialized `assets` layer. Optional: an unclaimed file joins the `common`
|
|
29
78
|
* layer, which is pulled alongside any controller layer, so omitting this costs
|
|
@@ -278,13 +327,13 @@ export const KERNEL_BUILTINS: ResourceDefinition[] = [
|
|
|
278
327
|
// kernel registers this controller directly at boot, before any lazy
|
|
279
328
|
// resolution — it states truthfully who provides it.
|
|
280
329
|
controllers: [{ runtime: "kernel", entry: "Telo.JsonSchema" }],
|
|
281
|
-
schema: {
|
|
330
|
+
schema: withSchemaFragments({
|
|
282
331
|
type: "object",
|
|
283
332
|
properties: {
|
|
284
333
|
schema: {
|
|
285
334
|
title: "Schema",
|
|
286
335
|
description: "JSON Schema definition for the declared data type.",
|
|
287
|
-
|
|
336
|
+
$ref: manifestFragmentRef("JsonSchema7"),
|
|
288
337
|
},
|
|
289
338
|
extends: {
|
|
290
339
|
title: "Extends",
|
|
@@ -319,13 +368,13 @@ export const KERNEL_BUILTINS: ResourceDefinition[] = [
|
|
|
319
368
|
},
|
|
320
369
|
required: ["schema"],
|
|
321
370
|
additionalProperties: false,
|
|
322
|
-
},
|
|
371
|
+
}),
|
|
323
372
|
},
|
|
324
373
|
{
|
|
325
374
|
kind: "Telo.Definition",
|
|
326
375
|
metadata: { name: "Abstract", module: "Telo" },
|
|
327
376
|
capability: "Telo.Template",
|
|
328
|
-
schema: {
|
|
377
|
+
schema: withSchemaFragments({
|
|
329
378
|
type: "object",
|
|
330
379
|
properties: {
|
|
331
380
|
kind: { type: "string" },
|
|
@@ -336,15 +385,15 @@ export const KERNEL_BUILTINS: ResourceDefinition[] = [
|
|
|
336
385
|
additionalProperties: true,
|
|
337
386
|
},
|
|
338
387
|
capability: { type: "string" },
|
|
339
|
-
schema:
|
|
340
|
-
status:
|
|
388
|
+
schema: kindSchemaSlot,
|
|
389
|
+
status: observedStateSlot,
|
|
341
390
|
},
|
|
342
391
|
required: ["metadata"],
|
|
343
392
|
// Telo.Abstract is an extension point by design — it must accept forward-compatible
|
|
344
393
|
// fields (e.g. inputType/outputType from the typed-abstracts plan) without requiring
|
|
345
394
|
// the analyzer to enumerate them here.
|
|
346
395
|
additionalProperties: true,
|
|
347
|
-
},
|
|
396
|
+
}),
|
|
348
397
|
},
|
|
349
398
|
{
|
|
350
399
|
kind: "Telo.Definition",
|
|
@@ -362,11 +411,17 @@ export const KERNEL_BUILTINS: ResourceDefinition[] = [
|
|
|
362
411
|
// matching how Run.Sequence steps factor dispatch from data. The dispatch
|
|
363
412
|
// entry-point (`invoke` / `provide` / `run`) determines how `inputs`/`result`
|
|
364
413
|
// are interpreted at runtime. See analyzer/nodejs/plans/template-internal-cel-validation.md.
|
|
365
|
-
schema: {
|
|
414
|
+
schema: withSchemaFragments({
|
|
366
415
|
type: "object",
|
|
367
416
|
additionalProperties: true,
|
|
368
417
|
properties: {
|
|
369
|
-
|
|
418
|
+
// The kind's own configuration contract. Declared as a slot for the
|
|
419
|
+
// first time here: it was reachable only as an unnamed extra property,
|
|
420
|
+
// so nothing could say what belonged in it — no completion inside a
|
|
421
|
+
// `schema:` block, and a misspelled keyword surviving to a runtime
|
|
422
|
+
// failure that named a different field.
|
|
423
|
+
schema: kindSchemaSlot,
|
|
424
|
+
status: observedStateSlot,
|
|
370
425
|
resources: {
|
|
371
426
|
type: "array",
|
|
372
427
|
items: {
|
|
@@ -536,7 +591,7 @@ export const KERNEL_BUILTINS: ResourceDefinition[] = [
|
|
|
536
591
|
},
|
|
537
592
|
},
|
|
538
593
|
},
|
|
539
|
-
},
|
|
594
|
+
}),
|
|
540
595
|
},
|
|
541
596
|
{
|
|
542
597
|
kind: "Telo.Definition",
|
|
@@ -787,6 +842,9 @@ export const KERNEL_BUILTINS: ResourceDefinition[] = [
|
|
|
787
842
|
// CLI flag — so a level derived from the host environment goes through a
|
|
788
843
|
// `variables:` entry read with `!cel`. See kernel/specs/logging.md §12.
|
|
789
844
|
logging: ROOT_LOGGING_SCHEMA,
|
|
845
|
+
// The runtime range this module is verified against. See
|
|
846
|
+
// `analyzer/nodejs/src/requires-block.ts`.
|
|
847
|
+
requires: REQUIRES_SCHEMA,
|
|
790
848
|
},
|
|
791
849
|
required: ["metadata"],
|
|
792
850
|
additionalProperties: false,
|
|
@@ -877,6 +935,9 @@ export const KERNEL_BUILTINS: ResourceDefinition[] = [
|
|
|
877
935
|
},
|
|
878
936
|
additionalProperties: true,
|
|
879
937
|
},
|
|
938
|
+
// The runtime range this module is verified against. See
|
|
939
|
+
// `analyzer/nodejs/src/requires-block.ts`.
|
|
940
|
+
requires: REQUIRES_SCHEMA,
|
|
880
941
|
},
|
|
881
942
|
required: ["metadata"],
|
|
882
943
|
additionalProperties: false,
|
package/src/cel-bindings.ts
CHANGED
|
@@ -20,34 +20,9 @@ export interface BindingSites {
|
|
|
20
20
|
scopeNames: Set<string>;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
* which turns silence into a diagnostic.
|
|
27
|
-
*/
|
|
28
|
-
export const CEL_RESERVED_WORDS: readonly string[] = [
|
|
29
|
-
"as",
|
|
30
|
-
"break",
|
|
31
|
-
"const",
|
|
32
|
-
"continue",
|
|
33
|
-
"else",
|
|
34
|
-
"false",
|
|
35
|
-
"for",
|
|
36
|
-
"function",
|
|
37
|
-
"if",
|
|
38
|
-
"import",
|
|
39
|
-
"in",
|
|
40
|
-
"let",
|
|
41
|
-
"loop",
|
|
42
|
-
"namespace",
|
|
43
|
-
"null",
|
|
44
|
-
"package",
|
|
45
|
-
"return",
|
|
46
|
-
"true",
|
|
47
|
-
"var",
|
|
48
|
-
"void",
|
|
49
|
-
"while",
|
|
50
|
-
];
|
|
23
|
+
// CEL keywords live in `identifier-name.ts` — a binding named after one is
|
|
24
|
+
// unreachable for exactly the reason a resource or step named after one is, so
|
|
25
|
+
// the list belongs to the identifier vocabulary rather than to this file.
|
|
51
26
|
|
|
52
27
|
/** Locate a kind's bindings field and the scope names its annotated contexts
|
|
53
28
|
* declare. Returns undefined for a kind that declares no bindings region. */
|
|
@@ -116,11 +116,36 @@ export class DefinitionRegistry {
|
|
|
116
116
|
registerNamedTypeSchema(id: string, schema: Record<string, any>): boolean {
|
|
117
117
|
if (this.definitionSchemaIds.has(id)) return false;
|
|
118
118
|
if (this.registeredSchemaIds.has(id) || this.ajv.getSchema(id)) return true;
|
|
119
|
-
this.
|
|
119
|
+
if (!this.tryAddSchema(schema, id)) return true;
|
|
120
120
|
this.registeredSchemaIds.add(id);
|
|
121
121
|
return true;
|
|
122
122
|
}
|
|
123
123
|
|
|
124
|
+
/**
|
|
125
|
+
* Register a schema, surviving one AJV refuses.
|
|
126
|
+
*
|
|
127
|
+
* `addSchema` META-VALIDATES and THROWS, and a throw here escapes the whole
|
|
128
|
+
* analyze pass: one author schema with `minimum: "3"` in it aborted the run
|
|
129
|
+
* with AJV's own unanchored text and took every other diagnostic in the file
|
|
130
|
+
* down with it — including the anchored one that says exactly which keyword is
|
|
131
|
+
* wrong. Registration is a lookup table for `$ref` resolution, so failing to
|
|
132
|
+
* fill one entry costs a reference that could not have resolved anyway.
|
|
133
|
+
*
|
|
134
|
+
* Nothing is swallowed: an unregisterable schema is invalid, and the two
|
|
135
|
+
* checks that report it both run afterwards and both anchor on the offending
|
|
136
|
+
* line — `SCHEMA_VIOLATION` from the `KindSchema` / `JsonSchema7` fragment the
|
|
137
|
+
* slot points at, and `SCHEMA_COMPILE_ERROR` from {@link schemaCompileError},
|
|
138
|
+
* which wraps `compile` for this same reason.
|
|
139
|
+
*/
|
|
140
|
+
private tryAddSchema(schema: Record<string, any>, id: string): boolean {
|
|
141
|
+
try {
|
|
142
|
+
this.ajv.addSchema(schema, id);
|
|
143
|
+
return true;
|
|
144
|
+
} catch {
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
124
149
|
/** True when a schema is registered under `id` (a canonical `telo://` type id
|
|
125
150
|
* or a definition `$id`). Used to flag schema `$ref`s that resolve to nothing. */
|
|
126
151
|
hasSchemaId(id: string): boolean {
|
|
@@ -187,7 +212,10 @@ export class DefinitionRegistry {
|
|
|
187
212
|
if (this.ajv.getSchema(id)) {
|
|
188
213
|
throw new Error(`Duplicate definition schema $id: "${id}" is already registered`);
|
|
189
214
|
}
|
|
190
|
-
|
|
215
|
+
// A schema AJV refuses is left unregistered rather than aborting the pass —
|
|
216
|
+
// see {@link tryAddSchema}. The id stays claimed either way, so a later
|
|
217
|
+
// named type cannot quietly take a kind's place.
|
|
218
|
+
this.tryAddSchema(schema, id);
|
|
191
219
|
this.registeredSchemaIds.add(id);
|
|
192
220
|
this.definitionSchemaIds.add(id);
|
|
193
221
|
}
|