@telorun/analyzer 0.60.0 → 0.62.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.map +1 -1
- package/dist/analyzer.js +130 -9
- package/dist/artifact-layer-index.d.ts +13 -5
- package/dist/artifact-layer-index.d.ts.map +1 -1
- package/dist/artifact-layer-index.js +37 -13
- package/dist/artifact-selector.d.ts +10 -3
- package/dist/artifact-selector.d.ts.map +1 -1
- package/dist/artifact-selector.js +10 -1
- package/dist/builtins.d.ts.map +1 -1
- package/dist/builtins.js +97 -13
- 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 +14 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -2
- 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/module-file-claims.d.ts +7 -0
- package/dist/module-file-claims.d.ts.map +1 -1
- package/dist/module-file-claims.js +22 -2
- package/dist/module-library.d.ts +97 -0
- package/dist/module-library.d.ts.map +1 -0
- package/dist/module-library.js +162 -0
- 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-module-artifact.d.ts.map +1 -1
- package/dist/validate-module-artifact.js +53 -1
- 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/artifact-layer-index.ts +47 -14
- package/src/artifact-selector.ts +15 -4
- package/src/builtins.ts +102 -13
- 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 +44 -1
- package/src/manifest-schemas.ts +223 -4
- package/src/module-file-claims.ts +32 -2
- package/src/module-library.ts +208 -0
- 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-module-artifact.ts +56 -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(
|
|
@@ -29,8 +29,10 @@
|
|
|
29
29
|
*/
|
|
30
30
|
|
|
31
31
|
import {
|
|
32
|
+
LAYER_ROLES,
|
|
32
33
|
isLayerRole,
|
|
33
34
|
normalizeSelector,
|
|
35
|
+
roleCarriesSelector,
|
|
34
36
|
selectorKey,
|
|
35
37
|
selectorMatches,
|
|
36
38
|
type ArtifactSelector,
|
|
@@ -46,7 +48,7 @@ const CONTENT_DIGEST = /^sha256-[A-Za-z0-9_-]{43}$/;
|
|
|
46
48
|
|
|
47
49
|
export interface ArtifactLayer {
|
|
48
50
|
role: LayerRole;
|
|
49
|
-
/** Present on `controller`
|
|
51
|
+
/** Present on the code-bearing roles (`controller`, `library`) only. */
|
|
50
52
|
selector?: ArtifactSelector;
|
|
51
53
|
/** OCI blob digest — addresses the layer and verifies the transfer. */
|
|
52
54
|
blob: string;
|
|
@@ -97,25 +99,35 @@ export function parseLayerIndex(value: unknown, describe = "layers"): ArtifactLa
|
|
|
97
99
|
throw new LayerIndexError(`${where}: expected an object.`);
|
|
98
100
|
}
|
|
99
101
|
const entry = raw as Record<string, unknown>;
|
|
100
|
-
if (
|
|
102
|
+
if (typeof entry.role !== "string" || entry.role === "") {
|
|
101
103
|
throw new LayerIndexError(
|
|
102
|
-
`${where}: role must be one of
|
|
103
|
-
|
|
104
|
+
`${where}: role is required and must be one of ${LAYER_ROLES.map((r) => `'${r}'`).join(", ")}; ` +
|
|
105
|
+
`got ${entry.role === undefined ? "nothing" : `'${String(entry.role)}'`}.`,
|
|
104
106
|
);
|
|
105
107
|
}
|
|
108
|
+
// A role this runtime does not know is SKIPPED, never rejected. Roles are
|
|
109
|
+
// added over time, and a runtime that cannot name one cannot need it — while
|
|
110
|
+
// throwing would make the whole manifest unreadable, so a module gaining a
|
|
111
|
+
// layer for a newer runtime would stop loading on an older one entirely
|
|
112
|
+
// rather than merely lacking that layer. The error stays for a structurally
|
|
113
|
+
// invalid entry, which is a malformed index rather than a newer one.
|
|
114
|
+
if (!isLayerRole(entry.role)) return;
|
|
106
115
|
const role = entry.role;
|
|
107
116
|
|
|
108
117
|
let selector: ArtifactSelector | undefined;
|
|
109
|
-
if (role
|
|
118
|
+
if (roleCarriesSelector(role)) {
|
|
110
119
|
if (entry.selector === undefined) {
|
|
111
|
-
throw new LayerIndexError(`${where}: a
|
|
120
|
+
throw new LayerIndexError(`${where}: a ${role} layer must declare a selector.`);
|
|
112
121
|
}
|
|
113
122
|
selector = normalizeSelector(entry.selector, where);
|
|
114
|
-
|
|
123
|
+
// Scoped by role: a module's `js` controller layer and its `js` library
|
|
124
|
+
// layer are different layers with the same selector, and only a collision
|
|
125
|
+
// *within* one role means two layers claim one address.
|
|
126
|
+
const key = `${role}\0${selectorKey(selector)}`;
|
|
115
127
|
if (seenSelectors.has(key)) {
|
|
116
128
|
throw new LayerIndexError(
|
|
117
|
-
`${where}: a second
|
|
118
|
-
`Each selector addresses exactly one layer.`,
|
|
129
|
+
`${where}: a second ${role} layer claims the selector ${selectorKey(selector)}. ` +
|
|
130
|
+
`Each selector addresses exactly one layer of a role.`,
|
|
119
131
|
);
|
|
120
132
|
}
|
|
121
133
|
seenSelectors.add(key);
|
|
@@ -145,18 +157,39 @@ export function parseLayerIndex(value: unknown, describe = "layers"): ArtifactLa
|
|
|
145
157
|
/** The singleton layer for a role, or undefined when the artifact has none. */
|
|
146
158
|
export function singletonLayer(
|
|
147
159
|
layers: readonly ArtifactLayer[],
|
|
148
|
-
role: Exclude<LayerRole, "controller">,
|
|
160
|
+
role: Exclude<LayerRole, "controller" | "library">,
|
|
149
161
|
): ArtifactLayer | undefined {
|
|
150
162
|
return layers.find((l) => l.role === role);
|
|
151
163
|
}
|
|
152
164
|
|
|
153
|
-
/**
|
|
154
|
-
*
|
|
155
|
-
|
|
165
|
+
/** The layer of one code role carrying exactly `selector`, or undefined.
|
|
166
|
+
*
|
|
167
|
+
* By exact key rather than by re-matching a host: the candidate being resolved
|
|
168
|
+
* already *is* one selector, and it is by construction the key of the layer
|
|
169
|
+
* that carries it. */
|
|
170
|
+
export function codeLayerFor(
|
|
171
|
+
layers: readonly ArtifactLayer[],
|
|
172
|
+
role: Extract<LayerRole, "controller" | "library">,
|
|
173
|
+
selector: ArtifactSelector,
|
|
174
|
+
): ArtifactLayer | undefined {
|
|
175
|
+
const key = selectorKey(selector);
|
|
176
|
+
return layers.find(
|
|
177
|
+
(l) => l.role === role && l.selector !== undefined && selectorKey(l.selector) === key,
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** Every code layer — controller and library alike — matching `target`, in
|
|
182
|
+
* declaration order. Used by `telo install` to warm a cache for one platform:
|
|
183
|
+
* a library layer is as much a prerequisite of an offline run as the controller
|
|
184
|
+
* layer that imports it. */
|
|
185
|
+
export function matchCodeLayers(
|
|
156
186
|
layers: readonly ArtifactLayer[],
|
|
157
187
|
target: PlatformTarget,
|
|
158
188
|
): ArtifactLayer[] {
|
|
159
189
|
return layers.filter(
|
|
160
|
-
(l) =>
|
|
190
|
+
(l) =>
|
|
191
|
+
(l.role === "controller" || l.role === "library") &&
|
|
192
|
+
l.selector !== undefined &&
|
|
193
|
+
selectorMatches(l.selector, target),
|
|
161
194
|
);
|
|
162
195
|
}
|
package/src/artifact-selector.ts
CHANGED
|
@@ -25,16 +25,27 @@
|
|
|
25
25
|
* published into OCI descriptors.
|
|
26
26
|
*/
|
|
27
27
|
|
|
28
|
-
/** The role a layer plays in a module artifact. `controller`
|
|
29
|
-
* selector; `assets` and `common` are singletons and carry none. */
|
|
30
|
-
export type LayerRole = "controller" | "assets" | "common";
|
|
28
|
+
/** The role a layer plays in a module artifact. `controller` and `library` layers
|
|
29
|
+
* carry a selector; `assets` and `common` are singletons and carry none. */
|
|
30
|
+
export type LayerRole = "controller" | "library" | "assets" | "common";
|
|
31
31
|
|
|
32
|
-
export const LAYER_ROLES: readonly LayerRole[] = ["controller", "assets", "common"];
|
|
32
|
+
export const LAYER_ROLES: readonly LayerRole[] = ["controller", "library", "assets", "common"];
|
|
33
33
|
|
|
34
34
|
export function isLayerRole(value: unknown): value is LayerRole {
|
|
35
35
|
return typeof value === "string" && (LAYER_ROLES as readonly string[]).includes(value);
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
/** The roles that hold executable code, and are therefore per format rather than
|
|
39
|
+
* singletons. A `library` layer is per selector for the same reason a
|
|
40
|
+
* `controller` layer is: a module's JS entry point and its future Rust one are
|
|
41
|
+
* different files, and a consumer resolves the one its own runtime can import.
|
|
42
|
+
* A singleton would be wrong the moment a second runtime ships. */
|
|
43
|
+
export const CODE_LAYER_ROLES: readonly LayerRole[] = ["controller", "library"];
|
|
44
|
+
|
|
45
|
+
export function roleCarriesSelector(role: LayerRole): boolean {
|
|
46
|
+
return (CODE_LAYER_ROLES as readonly string[]).includes(role);
|
|
47
|
+
}
|
|
48
|
+
|
|
38
49
|
/** The platform axes, in canonical order. Not a closed vocabulary of *values* —
|
|
39
50
|
* new architectures appear without a Telo release — only of axis names. */
|
|
40
51
|
export const PLATFORM_AXES = ["os", "arch", "libc"] as const;
|
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
|
|
@@ -33,6 +82,33 @@ const ASSETS_FILES_SCHEMA = {
|
|
|
33
82
|
items: { type: "string" },
|
|
34
83
|
};
|
|
35
84
|
|
|
85
|
+
/** `exports.code` — the entry point a sibling module's controller bundle resolves
|
|
86
|
+
* this library's bare specifier to, one per format.
|
|
87
|
+
*
|
|
88
|
+
* Data rather than a package URL: `controllers:` needs a PURL because it can name
|
|
89
|
+
* an ecosystem fetch (`pkg:npm`, `pkg:cargo`), while this always names a file the
|
|
90
|
+
* module already ships, so the type/namespace segments would be constant noise —
|
|
91
|
+
* and a query string is one opaque box to the visual editor. `format` plus the
|
|
92
|
+
* platform axes build the same `ArtifactSelector` a controller candidate does.
|
|
93
|
+
* Semantics and diagnostics live in `analyzer/nodejs/src/module-library.ts`. */
|
|
94
|
+
const LIBRARY_CANDIDATES_SCHEMA = {
|
|
95
|
+
type: "array",
|
|
96
|
+
items: {
|
|
97
|
+
type: "object",
|
|
98
|
+
required: ["specifier", "format", "path"],
|
|
99
|
+
properties: {
|
|
100
|
+
specifier: { type: "string" },
|
|
101
|
+
format: { type: "string" },
|
|
102
|
+
path: { type: "string" },
|
|
103
|
+
source: { type: "string" },
|
|
104
|
+
os: { type: "string" },
|
|
105
|
+
arch: { type: "string" },
|
|
106
|
+
libc: { type: "string" },
|
|
107
|
+
},
|
|
108
|
+
additionalProperties: false,
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
|
|
36
112
|
/** The published layer index, written by `telo publish` (never hand-authored).
|
|
37
113
|
* One entry per layer except the manifest layer, which cannot list its own hash
|
|
38
114
|
* inside itself and is pinned by the importer's `#sha256-...` instead. Shape and
|
|
@@ -44,7 +120,7 @@ const LAYER_INDEX_SCHEMA = {
|
|
|
44
120
|
type: "object",
|
|
45
121
|
required: ["role", "blob", "integrity"],
|
|
46
122
|
properties: {
|
|
47
|
-
role: { type: "string", enum: ["controller", "assets", "common"] },
|
|
123
|
+
role: { type: "string", enum: ["controller", "library", "assets", "common"] },
|
|
48
124
|
selector: {
|
|
49
125
|
type: "object",
|
|
50
126
|
required: ["format"],
|
|
@@ -251,13 +327,13 @@ export const KERNEL_BUILTINS: ResourceDefinition[] = [
|
|
|
251
327
|
// kernel registers this controller directly at boot, before any lazy
|
|
252
328
|
// resolution — it states truthfully who provides it.
|
|
253
329
|
controllers: [{ runtime: "kernel", entry: "Telo.JsonSchema" }],
|
|
254
|
-
schema: {
|
|
330
|
+
schema: withSchemaFragments({
|
|
255
331
|
type: "object",
|
|
256
332
|
properties: {
|
|
257
333
|
schema: {
|
|
258
334
|
title: "Schema",
|
|
259
335
|
description: "JSON Schema definition for the declared data type.",
|
|
260
|
-
|
|
336
|
+
$ref: manifestFragmentRef("JsonSchema7"),
|
|
261
337
|
},
|
|
262
338
|
extends: {
|
|
263
339
|
title: "Extends",
|
|
@@ -292,13 +368,13 @@ export const KERNEL_BUILTINS: ResourceDefinition[] = [
|
|
|
292
368
|
},
|
|
293
369
|
required: ["schema"],
|
|
294
370
|
additionalProperties: false,
|
|
295
|
-
},
|
|
371
|
+
}),
|
|
296
372
|
},
|
|
297
373
|
{
|
|
298
374
|
kind: "Telo.Definition",
|
|
299
375
|
metadata: { name: "Abstract", module: "Telo" },
|
|
300
376
|
capability: "Telo.Template",
|
|
301
|
-
schema: {
|
|
377
|
+
schema: withSchemaFragments({
|
|
302
378
|
type: "object",
|
|
303
379
|
properties: {
|
|
304
380
|
kind: { type: "string" },
|
|
@@ -309,15 +385,15 @@ export const KERNEL_BUILTINS: ResourceDefinition[] = [
|
|
|
309
385
|
additionalProperties: true,
|
|
310
386
|
},
|
|
311
387
|
capability: { type: "string" },
|
|
312
|
-
schema:
|
|
313
|
-
status:
|
|
388
|
+
schema: kindSchemaSlot,
|
|
389
|
+
status: observedStateSlot,
|
|
314
390
|
},
|
|
315
391
|
required: ["metadata"],
|
|
316
392
|
// Telo.Abstract is an extension point by design — it must accept forward-compatible
|
|
317
393
|
// fields (e.g. inputType/outputType from the typed-abstracts plan) without requiring
|
|
318
394
|
// the analyzer to enumerate them here.
|
|
319
395
|
additionalProperties: true,
|
|
320
|
-
},
|
|
396
|
+
}),
|
|
321
397
|
},
|
|
322
398
|
{
|
|
323
399
|
kind: "Telo.Definition",
|
|
@@ -335,11 +411,17 @@ export const KERNEL_BUILTINS: ResourceDefinition[] = [
|
|
|
335
411
|
// matching how Run.Sequence steps factor dispatch from data. The dispatch
|
|
336
412
|
// entry-point (`invoke` / `provide` / `run`) determines how `inputs`/`result`
|
|
337
413
|
// are interpreted at runtime. See analyzer/nodejs/plans/template-internal-cel-validation.md.
|
|
338
|
-
schema: {
|
|
414
|
+
schema: withSchemaFragments({
|
|
339
415
|
type: "object",
|
|
340
416
|
additionalProperties: true,
|
|
341
417
|
properties: {
|
|
342
|
-
|
|
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,
|
|
343
425
|
resources: {
|
|
344
426
|
type: "array",
|
|
345
427
|
items: {
|
|
@@ -509,7 +591,7 @@ export const KERNEL_BUILTINS: ResourceDefinition[] = [
|
|
|
509
591
|
},
|
|
510
592
|
},
|
|
511
593
|
},
|
|
512
|
-
},
|
|
594
|
+
}),
|
|
513
595
|
},
|
|
514
596
|
{
|
|
515
597
|
kind: "Telo.Definition",
|
|
@@ -760,6 +842,9 @@ export const KERNEL_BUILTINS: ResourceDefinition[] = [
|
|
|
760
842
|
// CLI flag — so a level derived from the host environment goes through a
|
|
761
843
|
// `variables:` entry read with `!cel`. See kernel/specs/logging.md §12.
|
|
762
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,
|
|
763
848
|
},
|
|
764
849
|
required: ["metadata"],
|
|
765
850
|
additionalProperties: false,
|
|
@@ -846,9 +931,13 @@ export const KERNEL_BUILTINS: ResourceDefinition[] = [
|
|
|
846
931
|
type: "array",
|
|
847
932
|
items: { type: "string", not: { enum: ["variables", "secrets"] } },
|
|
848
933
|
},
|
|
934
|
+
code: LIBRARY_CANDIDATES_SCHEMA,
|
|
849
935
|
},
|
|
850
936
|
additionalProperties: true,
|
|
851
937
|
},
|
|
938
|
+
// The runtime range this module is verified against. See
|
|
939
|
+
// `analyzer/nodejs/src/requires-block.ts`.
|
|
940
|
+
requires: REQUIRES_SCHEMA,
|
|
852
941
|
},
|
|
853
942
|
required: ["metadata"],
|
|
854
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
|
}
|