@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
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The single reader of a module doc's `requires:` block — the version ranges of
|
|
3
|
+
* runtime a module declares itself verified against. The load gate, the CLI's
|
|
4
|
+
* publish preflight and `upgrade`'s candidate filter all recognise the block
|
|
5
|
+
* here and nowhere else, the one-accessor rule `ref-slot.ts` and `zone-slot.ts`
|
|
6
|
+
* established. Browser-safe: no Node built-ins, so the editor reaches the
|
|
7
|
+
* identical rule the kernel does.
|
|
8
|
+
*
|
|
9
|
+
* ```yaml
|
|
10
|
+
* requires:
|
|
11
|
+
* telo: ">=0.80.0"
|
|
12
|
+
* host:
|
|
13
|
+
* node: ">=20.0.0"
|
|
14
|
+
* ```
|
|
15
|
+
*
|
|
16
|
+
* **Two tiers, and the split is not cosmetic.** `telo` names the *manifest
|
|
17
|
+
* surface generation* a runtime implements — one scale shared by every kernel,
|
|
18
|
+
* Node, Rust or Go, independent of each kernel's own release identity — and it
|
|
19
|
+
* is the one axis verified by EXECUTION, by running the CLI at each edge of the
|
|
20
|
+
* declared range. Host axes cannot be edge-verified by any CI; they are asserted
|
|
21
|
+
* by the author and compared against the version the running host reports
|
|
22
|
+
* ({@link HostVersions}). A flat map would imply one semantics for both.
|
|
23
|
+
*
|
|
24
|
+
* Nesting is also what disambiguates the names: `nodejs` and `rust` are already
|
|
25
|
+
* *kernel labels* in this repo (`LABEL_TO_PURL_TYPE`, an `imports:` entry's
|
|
26
|
+
* `runtime:`), so a top-level `node:` reads as the Node kernel rather than the
|
|
27
|
+
* Node.js runtime — and no word escapes that, because the host runtime and the
|
|
28
|
+
* kernel implementation genuinely share a name. Under `host:` position carries
|
|
29
|
+
* the disambiguation and no word has to.
|
|
30
|
+
*
|
|
31
|
+
* **Ordering is normative: `telo` is checked before `host`, and before any
|
|
32
|
+
* unknown-axis complaint.** A module using an axis introduced in telo 0.85 also
|
|
33
|
+
* declares telo `>=0.85`, so an older runtime fails on the telo axis first and
|
|
34
|
+
* never has to decide what an axis it has never heard of means. That is what
|
|
35
|
+
* makes the block safely extensible; consumers get the order from
|
|
36
|
+
* {@link evaluateRequires} rather than re-deriving it.
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
import { parseModuleVersion } from "./module-version-order.js";
|
|
40
|
+
import {
|
|
41
|
+
isUnsatisfiable,
|
|
42
|
+
parseVersionRange,
|
|
43
|
+
rangeAccepts,
|
|
44
|
+
type VersionRange,
|
|
45
|
+
} from "./version-range.js";
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Host axes this analyzer knows.
|
|
49
|
+
*
|
|
50
|
+
* **An axis is in this list only when something checks it.** A declared
|
|
51
|
+
* requirement nothing compares is worse than no requirement at all: it validates,
|
|
52
|
+
* it reads as protection, and it silently protects nobody — the exact failure
|
|
53
|
+
* class this whole mechanism exists to remove, reintroduced inside it. So `rustc`
|
|
54
|
+
* is deliberately absent until the slice that builds controller crates can
|
|
55
|
+
* compare it; adding it there is a one-line change here plus a supplier in
|
|
56
|
+
* {@link HostVersions}, and until then an author writing it is told it is not a
|
|
57
|
+
* known axis rather than quietly reassured.
|
|
58
|
+
*
|
|
59
|
+
* Extending the set is a telo release, which is exactly why a module using a new
|
|
60
|
+
* axis must also raise its `telo` bound — and why `telo` is checked first.
|
|
61
|
+
*/
|
|
62
|
+
export const KNOWN_HOST_AXES = ["node"] as const;
|
|
63
|
+
export type HostAxis = (typeof KNOWN_HOST_AXES)[number];
|
|
64
|
+
|
|
65
|
+
/** Top-level keys of the block. `host` is a container, `telo` a range. */
|
|
66
|
+
const KNOWN_AXES = ["telo", "host"] as const;
|
|
67
|
+
|
|
68
|
+
export interface RequiresBlock {
|
|
69
|
+
/** The surface generation range, when declared. */
|
|
70
|
+
telo?: VersionRange;
|
|
71
|
+
/** Declared host axes, keyed by axis name. Empty when `host:` is absent. */
|
|
72
|
+
host: Partial<Record<HostAxis, VersionRange>>;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface RequiresIssue {
|
|
76
|
+
/** Dotted path within the doc, e.g. `requires.telo` or `requires.host.node`. */
|
|
77
|
+
path: string;
|
|
78
|
+
message: string;
|
|
79
|
+
/** The spelling to use instead, when there is an unambiguous one. */
|
|
80
|
+
hint?: string;
|
|
81
|
+
/** True for an unrecognized axis, which consumers suppress while the `telo`
|
|
82
|
+
* requirement itself is unmet — an older runtime not knowing a newer axis is
|
|
83
|
+
* a consequence of the version skew, not a second defect to report. */
|
|
84
|
+
unknownAxis?: boolean;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export interface ReadRequiresResult {
|
|
88
|
+
/** Present whenever the doc carries a `requires:` key at all, even a malformed
|
|
89
|
+
* one — so a consumer can tell "declared nothing" from "declared badly". */
|
|
90
|
+
declared: boolean;
|
|
91
|
+
block: RequiresBlock;
|
|
92
|
+
issues: RequiresIssue[];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const EMPTY: RequiresBlock = { host: {} };
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Read and parse the block off a module doc. Never throws. A malformed entry
|
|
99
|
+
* yields an issue AND is omitted from the block, so a consumer enforcing the
|
|
100
|
+
* block never silently treats garbage as a satisfied requirement — the issue is
|
|
101
|
+
* what makes the manifest fail, exactly as a malformed zone annotation does.
|
|
102
|
+
*/
|
|
103
|
+
export function readRequires(doc: Record<string, unknown> | undefined): ReadRequiresResult {
|
|
104
|
+
const raw = doc?.requires;
|
|
105
|
+
if (raw === undefined) return { declared: false, block: EMPTY, issues: [] };
|
|
106
|
+
|
|
107
|
+
const issues: RequiresIssue[] = [];
|
|
108
|
+
const block: RequiresBlock = { host: {} };
|
|
109
|
+
|
|
110
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
111
|
+
issues.push({
|
|
112
|
+
path: "requires",
|
|
113
|
+
message: `'requires' must be a mapping of axes, got ${describe(raw)}.`,
|
|
114
|
+
});
|
|
115
|
+
return { declared: true, block, issues };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const entries = raw as Record<string, unknown>;
|
|
119
|
+
|
|
120
|
+
for (const key of Object.keys(entries)) {
|
|
121
|
+
if (!(KNOWN_AXES as readonly string[]).includes(key)) {
|
|
122
|
+
issues.push({
|
|
123
|
+
path: `requires.${key}`,
|
|
124
|
+
message:
|
|
125
|
+
`'requires.${key}' is not a known axis. This runtime knows ` +
|
|
126
|
+
`${KNOWN_AXES.map((a) => `'${a}'`).join(" and ")}; host requirements go under 'host'.`,
|
|
127
|
+
unknownAxis: true,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (entries.telo !== undefined) {
|
|
133
|
+
const range = parseRangeAt(entries.telo, "requires.telo", issues);
|
|
134
|
+
if (range) block.telo = range;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (entries.host !== undefined) {
|
|
138
|
+
const host = entries.host;
|
|
139
|
+
if (host === null || typeof host !== "object" || Array.isArray(host)) {
|
|
140
|
+
issues.push({
|
|
141
|
+
path: "requires.host",
|
|
142
|
+
message: `'requires.host' must be a mapping of host axes, got ${describe(host)}.`,
|
|
143
|
+
});
|
|
144
|
+
} else {
|
|
145
|
+
for (const [axis, value] of Object.entries(host as Record<string, unknown>)) {
|
|
146
|
+
if (!(KNOWN_HOST_AXES as readonly string[]).includes(axis)) {
|
|
147
|
+
issues.push({
|
|
148
|
+
path: `requires.host.${axis}`,
|
|
149
|
+
message:
|
|
150
|
+
`'requires.host.${axis}' is not a known host axis. This runtime knows ` +
|
|
151
|
+
`${KNOWN_HOST_AXES.map((a) => `'${a}'`).join(", ")}.`,
|
|
152
|
+
unknownAxis: true,
|
|
153
|
+
});
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
const range = parseRangeAt(value, `requires.host.${axis}`, issues);
|
|
157
|
+
if (range) block.host[axis as HostAxis] = range;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return { declared: true, block, issues };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function parseRangeAt(
|
|
166
|
+
value: unknown,
|
|
167
|
+
path: string,
|
|
168
|
+
issues: RequiresIssue[],
|
|
169
|
+
): VersionRange | undefined {
|
|
170
|
+
const result = parseVersionRange(value);
|
|
171
|
+
if (!result.ok) {
|
|
172
|
+
issues.push({ path, message: `'${path}': ${result.error.message}.`, hint: result.error.hint });
|
|
173
|
+
return undefined;
|
|
174
|
+
}
|
|
175
|
+
if (isUnsatisfiable(result.range)) {
|
|
176
|
+
issues.push({
|
|
177
|
+
path,
|
|
178
|
+
message: `'${path}': '${result.range.raw}' admits no version — its bounds exclude each other.`,
|
|
179
|
+
});
|
|
180
|
+
return undefined;
|
|
181
|
+
}
|
|
182
|
+
return result.range;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** What a runtime concluded about a module's declared requirements. */
|
|
186
|
+
export type RequiresVerdict =
|
|
187
|
+
| { satisfied: true }
|
|
188
|
+
/** An axis whose declared range excludes the version this runtime reported. */
|
|
189
|
+
| {
|
|
190
|
+
satisfied: false;
|
|
191
|
+
axis: "telo" | HostAxis;
|
|
192
|
+
declared: VersionRange;
|
|
193
|
+
running: string;
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
/** The versions a host can speak for. Absent entries are not checked — the
|
|
197
|
+
* editor has no host to report, and an axis nothing supplies is skipped rather
|
|
198
|
+
* than guessed. Every axis in {@link KNOWN_HOST_AXES} has a supplier; an axis
|
|
199
|
+
* with none does not belong in the vocabulary (see the note there). */
|
|
200
|
+
export interface HostVersions {
|
|
201
|
+
node?: string;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Evaluate a module's declared requirements against the runtime performing the
|
|
206
|
+
* analysis.
|
|
207
|
+
*
|
|
208
|
+
* `telo` is checked FIRST and short-circuits: a module using a host axis
|
|
209
|
+
* introduced in a later telo also declares that telo, so an older runtime must
|
|
210
|
+
* report the version skew rather than a host axis it may not even know. Absent
|
|
211
|
+
* declarations are satisfied — the bootstrap rule, permanent for everything
|
|
212
|
+
* published before the mechanism existed.
|
|
213
|
+
*
|
|
214
|
+
* **A version this cannot PARSE is satisfied, on every axis.** A runtime that
|
|
215
|
+
* cannot name its own version must not start rejecting modules on the strength
|
|
216
|
+
* of a number it could not read — the refusal-to-guess `module-version-order.ts`
|
|
217
|
+
* makes, pointed in the safe direction. The test is a parse, not a shape: `0.76`
|
|
218
|
+
* and `2024.1` look like versions and are not three-part ones, so a cheaper
|
|
219
|
+
* check (a leading digit, say) would fail them CLOSED and gate every module in
|
|
220
|
+
* the graph on a number nothing could compare. `AnalysisOptions.teloVersion` is
|
|
221
|
+
* hand-written by definition, so that is exactly where such a value arrives.
|
|
222
|
+
*/
|
|
223
|
+
export function evaluateRequires(
|
|
224
|
+
block: RequiresBlock,
|
|
225
|
+
running: string | undefined,
|
|
226
|
+
host: HostVersions = {},
|
|
227
|
+
): RequiresVerdict {
|
|
228
|
+
const telo = check("telo", block.telo, running);
|
|
229
|
+
if (telo) return telo;
|
|
230
|
+
for (const axis of KNOWN_HOST_AXES) {
|
|
231
|
+
const verdict = check(axis, block.host[axis], host[axis]);
|
|
232
|
+
if (verdict) return verdict;
|
|
233
|
+
}
|
|
234
|
+
return { satisfied: true };
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/** One axis, or `undefined` when it is satisfied / undeclared / unreportable. */
|
|
238
|
+
function check(
|
|
239
|
+
axis: "telo" | HostAxis,
|
|
240
|
+
declared: VersionRange | undefined,
|
|
241
|
+
running: string | undefined,
|
|
242
|
+
): RequiresVerdict | undefined {
|
|
243
|
+
if (!declared || !running) return undefined;
|
|
244
|
+
if (!parseModuleVersion(running)) return undefined;
|
|
245
|
+
if (rangeAccepts(declared, running)) return undefined;
|
|
246
|
+
return { satisfied: false, axis, declared, running };
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function describe(value: unknown): string {
|
|
250
|
+
if (value === null) return "null";
|
|
251
|
+
if (Array.isArray(value)) return "an array";
|
|
252
|
+
return typeof value;
|
|
253
|
+
}
|
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The JSON Schema vocabulary an author writes inside a manifest, as data.
|
|
3
|
+
*
|
|
4
|
+
* WHY THIS EXISTS. A schema-valued slot used to be declared `type: object` and
|
|
5
|
+
* nothing more, so every surface that reads a kind schema — completion, hover,
|
|
6
|
+
* the editor's field walk, AJV — knew only "some object". Autocompletion was
|
|
7
|
+
* dead from the first key, and a typo (`requred:`, `type: 5`) was carried all
|
|
8
|
+
* the way to a runtime validation failure that named the wrong thing.
|
|
9
|
+
*
|
|
10
|
+
* ONE SOURCE, TWO SURFACES. The maps below are the whole vocabulary; the
|
|
11
|
+
* fragments in `manifest-schemas.ts` are built FROM them, and completion reads
|
|
12
|
+
* them directly. A second hand-written list is exactly how the IDE's suggestions
|
|
13
|
+
* and the validator's rules would drift apart.
|
|
14
|
+
*
|
|
15
|
+
* WHY THE ANNOTATIONS ARE NOT IN THE VALIDATING FRAGMENT. `x-telo-*` keys are
|
|
16
|
+
* offered by completion but deliberately never appear as literal property names
|
|
17
|
+
* in the schema that gets hoisted into a manifest. Several passes walk a
|
|
18
|
+
* definition's schema testing every object for an annotation KEY
|
|
19
|
+
* (`resolveSchemaRefKinds`, `validate-ref-slots`, `validate-zone-slots`), and a
|
|
20
|
+
* `properties` map holding a key spelled `x-telo-ref` reads to them as an
|
|
21
|
+
* annotated node — inventing diagnostics about a slot the author never wrote.
|
|
22
|
+
* So the hoisted body stays open (`additionalProperties: true`, which admits
|
|
23
|
+
* every annotation) and the annotation vocabulary stays here, on the analyzer
|
|
24
|
+
* side of the boundary, where no manifest walk can reach it.
|
|
25
|
+
*
|
|
26
|
+
* Browser-safe: no Node built-ins.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import { X_TELO_TYPE } from "@telorun/sdk";
|
|
30
|
+
import { ANNOTATION_KEYWORDS } from "./value-type-keyword.js";
|
|
31
|
+
|
|
32
|
+
/** A keyword entry: the JSON Schema its VALUE must satisfy, carrying the title
|
|
33
|
+
* and description completion and hover show. */
|
|
34
|
+
export type SchemaKeywords = Record<string, Record<string, unknown>>;
|
|
35
|
+
|
|
36
|
+
/** A JSON type name, as `type:` accepts it — one, or a list (`[string, "null"]`,
|
|
37
|
+
* the spelling `CEL_NULLABLE_ACCESS` guards). */
|
|
38
|
+
const TYPE_NAME = {
|
|
39
|
+
type: "string",
|
|
40
|
+
enum: ["object", "array", "string", "number", "integer", "boolean", "null"],
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* The draft-07 keywords, as a property map for a fragment named `self`.
|
|
45
|
+
*
|
|
46
|
+
* Parameterized by the fragment name because a schema's nested positions hold
|
|
47
|
+
* schemas of the SAME flavour: a property of a kind schema may carry
|
|
48
|
+
* annotations, a property of a data schema may not. Both recurse into
|
|
49
|
+
* themselves, and the document-local pointer is what the hoisting in
|
|
50
|
+
* `expandManifestFragments` makes resolvable.
|
|
51
|
+
*/
|
|
52
|
+
export function jsonSchemaKeywords(self: string): SchemaKeywords {
|
|
53
|
+
const schema = { $ref: `#/$defs/${self}` };
|
|
54
|
+
const schemaList = { type: "array", items: schema };
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
// Shape
|
|
58
|
+
type: {
|
|
59
|
+
title: "Type",
|
|
60
|
+
description: "The JSON type this value must have, or a list of accepted types.",
|
|
61
|
+
anyOf: [TYPE_NAME, { type: "array", items: TYPE_NAME }],
|
|
62
|
+
},
|
|
63
|
+
properties: {
|
|
64
|
+
title: "Properties",
|
|
65
|
+
description: "Schema per named property of an object.",
|
|
66
|
+
type: "object",
|
|
67
|
+
additionalProperties: schema,
|
|
68
|
+
},
|
|
69
|
+
required: {
|
|
70
|
+
title: "Required",
|
|
71
|
+
description: "Property names that must be present.",
|
|
72
|
+
type: "array",
|
|
73
|
+
items: { type: "string" },
|
|
74
|
+
},
|
|
75
|
+
additionalProperties: {
|
|
76
|
+
title: "Additional properties",
|
|
77
|
+
description:
|
|
78
|
+
"`false` rejects any property not named above — which is what makes a typo an error rather than an ignored field.",
|
|
79
|
+
anyOf: [{ type: "boolean" }, schema],
|
|
80
|
+
},
|
|
81
|
+
patternProperties: {
|
|
82
|
+
title: "Pattern properties",
|
|
83
|
+
description: "Schema per regular expression matching a property name.",
|
|
84
|
+
type: "object",
|
|
85
|
+
additionalProperties: schema,
|
|
86
|
+
},
|
|
87
|
+
propertyNames: {
|
|
88
|
+
title: "Property names",
|
|
89
|
+
description: "Schema every property NAME must satisfy.",
|
|
90
|
+
...schema,
|
|
91
|
+
},
|
|
92
|
+
items: {
|
|
93
|
+
title: "Items",
|
|
94
|
+
description: "Schema for each element of an array.",
|
|
95
|
+
anyOf: [schema, schemaList],
|
|
96
|
+
},
|
|
97
|
+
additionalItems: {
|
|
98
|
+
title: "Additional items",
|
|
99
|
+
description: "Schema for elements past a positional `items` list.",
|
|
100
|
+
anyOf: [{ type: "boolean" }, schema],
|
|
101
|
+
},
|
|
102
|
+
contains: {
|
|
103
|
+
title: "Contains",
|
|
104
|
+
description: "At least one element must satisfy this schema.",
|
|
105
|
+
...schema,
|
|
106
|
+
},
|
|
107
|
+
|
|
108
|
+
// Composition
|
|
109
|
+
allOf: { title: "All of", description: "Every branch must match.", ...schemaList },
|
|
110
|
+
anyOf: { title: "Any of", description: "At least one branch must match.", ...schemaList },
|
|
111
|
+
oneOf: {
|
|
112
|
+
title: "One of",
|
|
113
|
+
description:
|
|
114
|
+
"Exactly one branch must match. Prefer `anyOf` when a branch declares a value type — a consumer that does not know the keyword reads the branch as matching everything, and only `anyOf` degrades gracefully.",
|
|
115
|
+
...schemaList,
|
|
116
|
+
},
|
|
117
|
+
not: { title: "Not", description: "The value must NOT match this schema.", ...schema },
|
|
118
|
+
if: { title: "If", description: "Condition selecting `then` / `else`.", ...schema },
|
|
119
|
+
then: { title: "Then", description: "Applied when `if` matches.", ...schema },
|
|
120
|
+
else: { title: "Else", description: "Applied when `if` does not match.", ...schema },
|
|
121
|
+
|
|
122
|
+
// Values
|
|
123
|
+
enum: { title: "Enum", description: "The complete set of accepted values.", type: "array" },
|
|
124
|
+
const: { title: "Const", description: "The single accepted value." },
|
|
125
|
+
default: {
|
|
126
|
+
title: "Default",
|
|
127
|
+
description:
|
|
128
|
+
"Filled in when the value is absent. On an invocation contract this is applied at dispatch, so a caller may omit the field.",
|
|
129
|
+
},
|
|
130
|
+
examples: { title: "Examples", description: "Sample values, for documentation.", type: "array" },
|
|
131
|
+
|
|
132
|
+
// Numbers
|
|
133
|
+
minimum: { title: "Minimum", type: "number" },
|
|
134
|
+
maximum: { title: "Maximum", type: "number" },
|
|
135
|
+
exclusiveMinimum: { title: "Exclusive minimum", type: "number" },
|
|
136
|
+
exclusiveMaximum: { title: "Exclusive maximum", type: "number" },
|
|
137
|
+
multipleOf: { title: "Multiple of", type: "number", exclusiveMinimum: 0 },
|
|
138
|
+
|
|
139
|
+
// Strings
|
|
140
|
+
minLength: { title: "Min length", type: "integer", minimum: 0 },
|
|
141
|
+
maxLength: { title: "Max length", type: "integer", minimum: 0 },
|
|
142
|
+
pattern: { title: "Pattern", description: "Regular expression the string must match.", type: "string" },
|
|
143
|
+
format: {
|
|
144
|
+
title: "Format",
|
|
145
|
+
description: "Named string format (`date-time`, `uri`, `email`, …).",
|
|
146
|
+
type: "string",
|
|
147
|
+
},
|
|
148
|
+
contentMediaType: {
|
|
149
|
+
title: "Content media type",
|
|
150
|
+
description:
|
|
151
|
+
"Media type of the string's content (`application/javascript`). Drives the editor's code-widget language.",
|
|
152
|
+
type: "string",
|
|
153
|
+
},
|
|
154
|
+
contentEncoding: { title: "Content encoding", type: "string" },
|
|
155
|
+
|
|
156
|
+
// Arrays
|
|
157
|
+
minItems: { title: "Min items", type: "integer", minimum: 0 },
|
|
158
|
+
maxItems: { title: "Max items", type: "integer", minimum: 0 },
|
|
159
|
+
uniqueItems: { title: "Unique items", type: "boolean" },
|
|
160
|
+
|
|
161
|
+
// Objects
|
|
162
|
+
minProperties: { title: "Min properties", type: "integer", minimum: 0 },
|
|
163
|
+
maxProperties: { title: "Max properties", type: "integer", minimum: 0 },
|
|
164
|
+
dependencies: {
|
|
165
|
+
title: "Dependencies",
|
|
166
|
+
description: "Per property, the other properties it requires — or a schema to apply.",
|
|
167
|
+
type: "object",
|
|
168
|
+
additionalProperties: { anyOf: [{ type: "array", items: { type: "string" } }, schema] },
|
|
169
|
+
},
|
|
170
|
+
|
|
171
|
+
// Documentation and structure
|
|
172
|
+
title: { title: "Title", description: "Human-readable label for this value.", type: "string" },
|
|
173
|
+
description: {
|
|
174
|
+
title: "Description",
|
|
175
|
+
description: "What this value is, for the author reading the manifest.",
|
|
176
|
+
type: "string",
|
|
177
|
+
},
|
|
178
|
+
deprecated: { title: "Deprecated", type: "boolean" },
|
|
179
|
+
readOnly: { title: "Read only", type: "boolean" },
|
|
180
|
+
writeOnly: { title: "Write only", type: "boolean" },
|
|
181
|
+
$ref: {
|
|
182
|
+
title: "Reference",
|
|
183
|
+
description:
|
|
184
|
+
"Pointer to another schema: `#/$defs/<Name>` for one declared below, `telo:<module>/<Type>` for a named type a module declared.",
|
|
185
|
+
type: "string",
|
|
186
|
+
},
|
|
187
|
+
$defs: {
|
|
188
|
+
title: "Definitions",
|
|
189
|
+
description: "Named sub-schemas, private to this schema, referenced with `#/$defs/<Name>`.",
|
|
190
|
+
type: "object",
|
|
191
|
+
additionalProperties: schema,
|
|
192
|
+
},
|
|
193
|
+
definitions: {
|
|
194
|
+
title: "Definitions (draft-07)",
|
|
195
|
+
description: "Older spelling of `$defs`.",
|
|
196
|
+
type: "object",
|
|
197
|
+
additionalProperties: schema,
|
|
198
|
+
},
|
|
199
|
+
$comment: { title: "Comment", type: "string" },
|
|
200
|
+
$id: { title: "Id", type: "string" },
|
|
201
|
+
$schema: { title: "Schema dialect", type: "string" },
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* The `x-telo-*` annotation vocabulary — completion and hover only.
|
|
207
|
+
*
|
|
208
|
+
* TOTAL over {@link ANNOTATION_KEYWORDS} plus `x-telo-type`, which is what makes
|
|
209
|
+
* it a description of the existing list rather than a second copy of it: adding
|
|
210
|
+
* an annotation without a completion entry is a compile error here, and the
|
|
211
|
+
* first draft of this map — hand-written — had already silently dropped four
|
|
212
|
+
* annotations that live stdlib manifests use.
|
|
213
|
+
*
|
|
214
|
+
* Values are intentionally loose. What an annotation MEANS is checked by the
|
|
215
|
+
* pass that owns it (`validate-ref-slots`, `validate-zone-slots`,
|
|
216
|
+
* `validate-value-type-slots`), which reports in that annotation's own
|
|
217
|
+
* vocabulary; restating the shape here would give one mistake two diagnostics
|
|
218
|
+
* that disagree about what is wrong.
|
|
219
|
+
*
|
|
220
|
+
* A KNOWN BLIND SPOT, and not an oversight: several annotations
|
|
221
|
+
* (`x-telo-context`, `x-telo-error-context`, `x-telo-step-context`) hold JSON
|
|
222
|
+
* Schema themselves, and cannot be typed by the fragment this file feeds. A
|
|
223
|
+
* literal `x-telo-*` key inside a hoisted `properties` map reads to the
|
|
224
|
+
* annotation walkers as an annotated node, so the vocabulary has to stay out of
|
|
225
|
+
* anything that enters a manifest — which leaves an annotation's VALUE with
|
|
226
|
+
* neither validation nor completion. Closing it needs the walkers to distinguish
|
|
227
|
+
* a schema describing an annotation from an annotation, which nothing does yet.
|
|
228
|
+
*/
|
|
229
|
+
export const TELO_SCHEMA_ANNOTATIONS: Record<
|
|
230
|
+
(typeof ANNOTATION_KEYWORDS)[number] | typeof X_TELO_TYPE,
|
|
231
|
+
Record<string, unknown>
|
|
232
|
+
> = {
|
|
233
|
+
"x-telo-eval": {
|
|
234
|
+
title: "Evaluation mode",
|
|
235
|
+
description:
|
|
236
|
+
"When `${{ }}` / `!cel` in this field is evaluated: `compile` at load, `runtime` per invocation. A CEL-bearing field MUST declare one, or the expression is read as a literal.",
|
|
237
|
+
type: "string",
|
|
238
|
+
enum: ["compile", "runtime"],
|
|
239
|
+
},
|
|
240
|
+
"x-telo-ref": {
|
|
241
|
+
title: "Reference slot",
|
|
242
|
+
description:
|
|
243
|
+
"This field holds a `!ref` to a resource — plus what the declaring resource DOES with it (`use`), which is what every topology analysis reads.",
|
|
244
|
+
anyOf: [{ type: "string" }, { type: "object" }],
|
|
245
|
+
},
|
|
246
|
+
"x-telo-type": {
|
|
247
|
+
title: "Value type",
|
|
248
|
+
description:
|
|
249
|
+
"What the value IS beyond JSON's types: `Telo.Bytes`, `Telo.Stream`, `Telo.TcpPort`, … Optionally parameterized (`{ name: Telo.Stream, of: Telo.Bytes }`).",
|
|
250
|
+
anyOf: [{ type: "string" }, { type: "object" }],
|
|
251
|
+
},
|
|
252
|
+
"x-telo-scope": {
|
|
253
|
+
title: "Execution scope",
|
|
254
|
+
description:
|
|
255
|
+
"JSON Pointer to a region whose `!ref`s resolve against this field's inline resources, created on entry and torn down on exit.",
|
|
256
|
+
anyOf: [{ type: "string" }, { type: "array", items: { type: "string" } }],
|
|
257
|
+
},
|
|
258
|
+
"x-telo-context": {
|
|
259
|
+
title: "CEL context",
|
|
260
|
+
description: "The CEL variables in scope inside this field, as a JSON Schema. Analyzer-only.",
|
|
261
|
+
type: "object",
|
|
262
|
+
},
|
|
263
|
+
"x-telo-error-context": {
|
|
264
|
+
title: "Error context",
|
|
265
|
+
description: "Schema of the `error` CEL variable inside this field, at any nesting depth.",
|
|
266
|
+
type: "object",
|
|
267
|
+
},
|
|
268
|
+
"x-telo-step-context": {
|
|
269
|
+
title: "Step context",
|
|
270
|
+
description:
|
|
271
|
+
"On a step array: how to build typed `steps.<name>.result` from each item's invoked resource.",
|
|
272
|
+
type: "object",
|
|
273
|
+
},
|
|
274
|
+
"x-telo-schema-from": {
|
|
275
|
+
title: "Schema from",
|
|
276
|
+
description: "Derive this field's validation schema from a sibling ref's definition schema.",
|
|
277
|
+
type: "string",
|
|
278
|
+
},
|
|
279
|
+
"x-telo-value-schema-from": {
|
|
280
|
+
title: "Value schema from",
|
|
281
|
+
description:
|
|
282
|
+
"The value here must satisfy the type declared at the named field — checked for EVERY such slot, not only the branch a given input selects.",
|
|
283
|
+
type: "string",
|
|
284
|
+
},
|
|
285
|
+
"x-telo-bindings-from": {
|
|
286
|
+
title: "Bindings from",
|
|
287
|
+
description: "Merge the names declared in the named field's map into this field's CEL scope.",
|
|
288
|
+
type: "string",
|
|
289
|
+
},
|
|
290
|
+
"x-telo-context-from": {
|
|
291
|
+
title: "Context from",
|
|
292
|
+
description: "Merge the navigated value as a property map into this context node.",
|
|
293
|
+
type: "string",
|
|
294
|
+
},
|
|
295
|
+
"x-telo-context-from-root": {
|
|
296
|
+
title: "Context from root",
|
|
297
|
+
description: "Replace this context node's schema with the value navigated from the manifest root.",
|
|
298
|
+
type: "string",
|
|
299
|
+
},
|
|
300
|
+
"x-telo-context-from-ref-kind": {
|
|
301
|
+
title: "Context from ref kind",
|
|
302
|
+
description: "Type this node from a referenced kind's declared `inputType` / `outputType`.",
|
|
303
|
+
type: "string",
|
|
304
|
+
},
|
|
305
|
+
"x-telo-context-ref-from": {
|
|
306
|
+
title: "Context ref from",
|
|
307
|
+
description: "Type this node from the named manifest's field, falling back to its kind's.",
|
|
308
|
+
type: "string",
|
|
309
|
+
},
|
|
310
|
+
"x-telo-context-element-from": {
|
|
311
|
+
title: "Context element from",
|
|
312
|
+
description: "Type this binding from the ELEMENT of a sibling collection expression.",
|
|
313
|
+
type: "string",
|
|
314
|
+
},
|
|
315
|
+
"x-telo-context-collection-from": {
|
|
316
|
+
title: "Context collection from",
|
|
317
|
+
description:
|
|
318
|
+
"Type this binding from a sibling collection expression itself. Withheld when the collection is live — re-exposing a cursor being drained is what no member-access rule catches.",
|
|
319
|
+
type: "string",
|
|
320
|
+
},
|
|
321
|
+
"x-telo-provides-zone": {
|
|
322
|
+
title: "Provides zone",
|
|
323
|
+
description:
|
|
324
|
+
"This resource opens an execution zone; the annotated value is the CORRELATION KEY, never the zone (a zone is identified by the kind that provides it).",
|
|
325
|
+
anyOf: [{ type: "boolean" }, { type: "string" }],
|
|
326
|
+
},
|
|
327
|
+
"x-telo-requires-zone": {
|
|
328
|
+
title: "Requires zone",
|
|
329
|
+
description:
|
|
330
|
+
"This resource must be reached through the named providing kind's body, optionally correlated through an ordered pointer list.",
|
|
331
|
+
anyOf: [{ type: "string" }, { type: "object" }],
|
|
332
|
+
},
|
|
333
|
+
"x-telo-widget": {
|
|
334
|
+
title: "Editor widget",
|
|
335
|
+
description: "Render this field with a richer control — `code` gives a Monaco editor.",
|
|
336
|
+
type: "string",
|
|
337
|
+
enum: ["code"],
|
|
338
|
+
},
|
|
339
|
+
"x-telo-topology-role": {
|
|
340
|
+
title: "Topology role",
|
|
341
|
+
description: "Names this field's part in the kind's topology, for the editor's graph view.",
|
|
342
|
+
type: "string",
|
|
343
|
+
},
|
|
344
|
+
"x-telo-inline": {
|
|
345
|
+
title: "Inline",
|
|
346
|
+
description: "Renders this field's value inline in the editor rather than behind an accordion.",
|
|
347
|
+
type: "boolean",
|
|
348
|
+
},
|
|
349
|
+
"x-telo-outcome-list": {
|
|
350
|
+
title: "Outcome list",
|
|
351
|
+
description: "Marks an array of conditional response-rendering entries (`returns:`).",
|
|
352
|
+
anyOf: [{ type: "boolean" }, { type: "string" }],
|
|
353
|
+
},
|
|
354
|
+
"x-telo-catches-for": {
|
|
355
|
+
title: "Catches for",
|
|
356
|
+
description: "Names the field whose failures this branch list handles.",
|
|
357
|
+
type: "string",
|
|
358
|
+
},
|
|
359
|
+
};
|
package/src/schema-region.ts
CHANGED
|
@@ -20,7 +20,18 @@
|
|
|
20
20
|
* Browser-safe: no Node built-ins.
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
|
-
/**
|
|
23
|
+
/**
|
|
24
|
+
* The kernel's schema-valued manifest keys.
|
|
25
|
+
*
|
|
26
|
+
* TWO CONTAINMENT RULES READ THIS SET, and they are not the same rule. This
|
|
27
|
+
* file's own {@link isInSchemaRegion} asks "is this node inside a schema" and
|
|
28
|
+
* answers by ANCESTRY, for the reason argued above. `expandManifestFragments`
|
|
29
|
+
* asks a different question — "which node will a validator compile, so a
|
|
30
|
+
* `#/$defs/…` pointer written below it resolves" — and answers by the TOP-LEVEL
|
|
31
|
+
* key alone, because that is the object handed to AJV; a nested occurrence is a
|
|
32
|
+
* slot describing a field named `schema`, not a compile root. Neither rule is a
|
|
33
|
+
* weaker version of the other, so do not "fix" one to match.
|
|
34
|
+
*/
|
|
24
35
|
export const SCHEMA_REGION_KEYS: readonly string[] = [
|
|
25
36
|
"schema",
|
|
26
37
|
"status",
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// GENERATED by scripts/generate-telo-version.mjs — do not edit.
|
|
2
|
+
//
|
|
3
|
+
// The manifest SURFACE GENERATION this build implements, read from the linked
|
|
4
|
+
// cli/kernel/sdk version. Distinct from this package's own npm version: that is
|
|
5
|
+
// its release identity, this is the scale a module's `requires.telo` range is
|
|
6
|
+
// written against, and every kernel in every language reports the same scale.
|
|
7
|
+
|
|
8
|
+
/** The surface generation this analyzer implements. */
|
|
9
|
+
export const TELO_SURFACE_VERSION = "0.78.0";
|
package/src/types.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { HostVersions } from "./requires-block.js";
|
|
2
|
+
|
|
1
3
|
import type { ZoneModuleDocuments } from "./zone-module-documents.js";
|
|
2
4
|
/** Matches LSP DiagnosticSeverity values exactly.
|
|
3
5
|
* https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#diagnosticSeverity */
|
|
@@ -156,6 +158,36 @@ export interface AnalysisOptions {
|
|
|
156
158
|
* (on-disk stamp) attests that the manifests passed a real analyze
|
|
157
159
|
* pass at the same analyzer / kernel version. */
|
|
158
160
|
skipValidation?: boolean;
|
|
161
|
+
/** The manifest SURFACE GENERATION the runtime performing this analysis
|
|
162
|
+
* implements, against which every module's `requires.telo` range is checked.
|
|
163
|
+
*
|
|
164
|
+
* Defaults to this build's own (`TELO_SURFACE_VERSION`), which is the right
|
|
165
|
+
* answer for the kernel, the CLI and the editor alike. In a browser no kernel
|
|
166
|
+
* is running, so the only well-posed question is whether the runtime *doing
|
|
167
|
+
* the analysis* can read the module — and that is exactly the case where the
|
|
168
|
+
* diagnostic is needed, since an editor too old to parse a construct
|
|
169
|
+
* otherwise produces vocabulary errors it cannot explain.
|
|
170
|
+
*
|
|
171
|
+
* The kernel deliberately does NOT override with its own package version.
|
|
172
|
+
* The constant is generated from the linked kernel version, so the two are
|
|
173
|
+
* the same number by construction; where they can drift — a kernel resolving
|
|
174
|
+
* an older analyzer than it was released with — the analyzer is the half that
|
|
175
|
+
* PARSES, and claiming a generation higher than the bundled parser implements
|
|
176
|
+
* would be a claim the stack cannot honour.
|
|
177
|
+
*
|
|
178
|
+
* So this exists for checking against a DIFFERENT target than oneself: a CI
|
|
179
|
+
* matrix, or an editor setting naming the version a team deploys on.
|
|
180
|
+
*
|
|
181
|
+
* Defaulted rather than optional-and-silent on purpose: there is no "caller
|
|
182
|
+
* forgot to pass it, check silently skipped" path. */
|
|
183
|
+
teloVersion?: string;
|
|
184
|
+
/** Versions of the HOST the analysis is being performed for, against which a
|
|
185
|
+
* module's `requires.host.*` ranges are checked.
|
|
186
|
+
*
|
|
187
|
+
* Absent in a browser, where there is no host to speak for, and an axis with
|
|
188
|
+
* no supplied version is skipped rather than guessed. The kernel and the CLI
|
|
189
|
+
* supply it because they are the host. */
|
|
190
|
+
hostVersions?: HostVersions;
|
|
159
191
|
}
|
|
160
192
|
|
|
161
193
|
/** Pre-seeded state for incremental analysis. Passed to StaticAnalyzer.analyze() so it does
|