@telorun/analyzer 0.57.0 → 0.59.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 +91 -9
- package/dist/cel-environment.d.ts.map +1 -1
- package/dist/cel-environment.js +6 -3
- package/dist/definition-registry.d.ts +5 -0
- package/dist/definition-registry.d.ts.map +1 -1
- package/dist/definition-registry.js +9 -0
- package/dist/index.d.ts +11 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -2
- package/dist/invocation-contract.d.ts +58 -13
- package/dist/invocation-contract.d.ts.map +1 -1
- package/dist/invocation-contract.js +123 -46
- package/dist/loaded-types.d.ts +13 -0
- package/dist/loaded-types.d.ts.map +1 -1
- package/dist/manifest-loader.d.ts +8 -1
- package/dist/manifest-loader.d.ts.map +1 -1
- package/dist/manifest-loader.js +33 -6
- package/dist/migrations/driver.d.ts +69 -0
- package/dist/migrations/driver.d.ts.map +1 -0
- package/dist/migrations/driver.js +200 -0
- package/dist/migrations/entries/index.d.ts +3 -0
- package/dist/migrations/entries/index.d.ts.map +1 -0
- package/dist/migrations/entries/index.js +9 -0
- package/dist/migrations/entries/normalize-value-types.json +48 -0
- package/dist/migrations/entries/ref-slot-scalar-type.json +20 -0
- package/dist/migrations/entry-data.d.ts +25 -0
- package/dist/migrations/entry-data.d.ts.map +1 -0
- package/dist/migrations/entry-data.js +171 -0
- package/dist/migrations/index.d.ts +22 -0
- package/dist/migrations/index.d.ts.map +1 -0
- package/dist/migrations/index.js +19 -0
- package/dist/migrations/match.d.ts +162 -0
- package/dist/migrations/match.d.ts.map +1 -0
- package/dist/migrations/match.js +295 -0
- package/dist/migrations/patch.d.ts +68 -0
- package/dist/migrations/patch.d.ts.map +1 -0
- package/dist/migrations/patch.js +236 -0
- package/dist/migrations/provenance.d.ts +25 -0
- package/dist/migrations/provenance.d.ts.map +1 -0
- package/dist/migrations/provenance.js +118 -0
- package/dist/migrations/registry.d.ts +35 -0
- package/dist/migrations/registry.d.ts.map +1 -0
- package/dist/migrations/registry.js +35 -0
- package/dist/migrations/report.d.ts +28 -0
- package/dist/migrations/report.d.ts.map +1 -0
- package/dist/migrations/report.js +144 -0
- package/dist/migrations/types.d.ts +115 -0
- package/dist/migrations/types.d.ts.map +1 -0
- package/dist/migrations/types.js +24 -0
- package/dist/migrations/yaml-edit.d.ts +21 -0
- package/dist/migrations/yaml-edit.d.ts.map +1 -0
- package/dist/migrations/yaml-edit.js +245 -0
- package/dist/parse-loaded-file.d.ts +8 -0
- package/dist/parse-loaded-file.d.ts.map +1 -1
- package/dist/parse-loaded-file.js +22 -13
- package/dist/resolve-schema-type-refs.d.ts +29 -12
- package/dist/resolve-schema-type-refs.d.ts.map +1 -1
- package/dist/resolve-schema-type-refs.js +89 -16
- package/dist/schema-compat.d.ts +53 -9
- package/dist/schema-compat.d.ts.map +1 -1
- package/dist/schema-compat.js +245 -66
- package/dist/schema-region.d.ts +33 -0
- package/dist/schema-region.d.ts.map +1 -0
- package/dist/schema-region.js +45 -0
- package/dist/types.d.ts +17 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/validate-cel-context.d.ts.map +1 -1
- package/dist/validate-cel-context.js +97 -15
- package/dist/validate-step-inputs.d.ts +8 -1
- package/dist/validate-step-inputs.d.ts.map +1 -1
- package/dist/validate-step-inputs.js +47 -2
- package/dist/validate-value-type-slots.d.ts +43 -0
- package/dist/validate-value-type-slots.d.ts.map +1 -0
- package/dist/validate-value-type-slots.js +142 -0
- package/dist/value-type-keyword.d.ts +60 -0
- package/dist/value-type-keyword.d.ts.map +1 -0
- package/dist/value-type-keyword.js +119 -0
- package/dist/yaml-source-edit.d.ts +56 -0
- package/dist/yaml-source-edit.d.ts.map +1 -0
- package/dist/yaml-source-edit.js +93 -0
- package/package.json +3 -3
- package/src/analyzer.ts +99 -6
- package/src/cel-environment.ts +6 -3
- package/src/definition-registry.ts +10 -0
- package/src/index.ts +34 -3
- package/src/invocation-contract.ts +134 -48
- package/src/loaded-types.ts +13 -0
- package/src/manifest-loader.ts +37 -6
- package/src/migrations/driver.ts +262 -0
- package/src/migrations/entries/index.ts +10 -0
- package/src/migrations/entries/normalize-value-types.json +48 -0
- package/src/migrations/entries/ref-slot-scalar-type.json +20 -0
- package/src/migrations/entry-data.ts +207 -0
- package/src/migrations/index.ts +22 -0
- package/src/migrations/match.ts +382 -0
- package/src/migrations/patch.ts +265 -0
- package/src/migrations/provenance.ts +167 -0
- package/src/migrations/registry.ts +40 -0
- package/src/migrations/report.ts +169 -0
- package/src/migrations/types.ts +144 -0
- package/src/migrations/yaml-edit.ts +260 -0
- package/src/parse-loaded-file.ts +31 -12
- package/src/resolve-schema-type-refs.ts +93 -16
- package/src/schema-compat.ts +277 -75
- package/src/schema-region.ts +46 -0
- package/src/types.ts +17 -0
- package/src/validate-cel-context.ts +117 -15
- package/src/validate-step-inputs.ts +56 -1
- package/src/validate-value-type-slots.ts +182 -0
- package/src/value-type-keyword.ts +135 -0
- package/src/yaml-source-edit.ts +106 -0
- package/dist/binary-slot.d.ts +0 -39
- package/dist/binary-slot.d.ts.map +0 -1
- package/dist/binary-slot.js +0 -58
- package/src/binary-slot.ts +0 -71
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"value-type-keyword.d.ts","sourceRoot":"","sources":["../src/value-type-keyword.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;+EAqB+E;AAG/E,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,KAAK,CAAC;AAiB7C;;;;;;GAMG;AACH,eAAO,MAAM,mBAAmB,ueAsBtB,CAAC;AAEX;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,gBAAgB,IAAI,iBAAiB,CA+BpD;AAED;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,GAAG,EAAE;IACxC,UAAU,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE,UAAU,CAAC,EAAE,GAAG,KAAK,OAAO,CAAC;CACzD,GAAG,IAAI,CAGP"}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/** The AJV half of `x-telo-type`, and the single place any Telo keyword is
|
|
2
|
+
* registered on an AJV instance.
|
|
3
|
+
*
|
|
4
|
+
* WHY CODEGEN RATHER THAN A `validate` FUNCTION. The kernel compiles standalone
|
|
5
|
+
* validators and caches them on disk; a function-valued keyword does not survive
|
|
6
|
+
* that serialization, so the check would silently be absent from exactly the
|
|
7
|
+
* validators the runtime uses. Codegen inlines instead.
|
|
8
|
+
*
|
|
9
|
+
* WHY THE CODEGEN LIVES HERE AND THE VOCABULARY DOES NOT. The SDK cannot depend
|
|
10
|
+
* on ajv, and an entry declares a REPRESENTATION rather than a code fragment —
|
|
11
|
+
* which is what lets one JSON file be read by a Rust kernel that has no AJV at
|
|
12
|
+
* all. The split is the whole point: `sdk/value-types/*.json` says *what a value
|
|
13
|
+
* is*, `sdk/nodejs/src/value-type.ts` says *what that means in this runtime*,
|
|
14
|
+
* and this file turns the pair into a check.
|
|
15
|
+
*
|
|
16
|
+
* ONE REGISTRATION SITE. {@link registerTeloKeywords} replaced five drifted
|
|
17
|
+
* ones — the analyzer's `createAjv` and the kernel's `schema-validator`,
|
|
18
|
+
* `resource-context`, `observed-state` and `manifest-schemas` — which registered
|
|
19
|
+
* overlapping lists of twelve, four, one and one. Drift there is not cosmetic: a
|
|
20
|
+
* no-op registration is what keeps `strict` mode quiet about an annotation, and
|
|
21
|
+
* a keyword that emits code is missing entirely from any instance that forgot
|
|
22
|
+
* it, so the same schema validated two ways depending on which AJV saw it. */
|
|
23
|
+
import * as AjvNS from "ajv";
|
|
24
|
+
import { VALUE_TYPE_BINDINGS, X_TELO_TYPE, readValueTypeSlot, } from "@telorun/sdk";
|
|
25
|
+
const ajvExports = AjvNS.default ?? AjvNS;
|
|
26
|
+
const codegen = ajvExports._ ?? AjvNS._;
|
|
27
|
+
/**
|
|
28
|
+
* Annotations that carry no validation at all: analyzer, editor and topology
|
|
29
|
+
* metadata. Registered as no-ops so AJV does not treat them as unknown keywords,
|
|
30
|
+
* and listed in one place so an instance cannot know about half of them.
|
|
31
|
+
*
|
|
32
|
+
* `x-telo-type` is deliberately absent — it is the one that emits code.
|
|
33
|
+
*/
|
|
34
|
+
export const ANNOTATION_KEYWORDS = [
|
|
35
|
+
"x-telo-bindings-from",
|
|
36
|
+
"x-telo-catches-for",
|
|
37
|
+
"x-telo-context",
|
|
38
|
+
"x-telo-context-element-from",
|
|
39
|
+
"x-telo-context-from",
|
|
40
|
+
"x-telo-context-from-ref-kind",
|
|
41
|
+
"x-telo-context-from-root",
|
|
42
|
+
"x-telo-context-ref-from",
|
|
43
|
+
"x-telo-error-context",
|
|
44
|
+
"x-telo-eval",
|
|
45
|
+
"x-telo-inline",
|
|
46
|
+
"x-telo-outcome-list",
|
|
47
|
+
"x-telo-provides-zone",
|
|
48
|
+
"x-telo-ref",
|
|
49
|
+
"x-telo-requires-zone",
|
|
50
|
+
"x-telo-schema-from",
|
|
51
|
+
"x-telo-scope",
|
|
52
|
+
"x-telo-step-context",
|
|
53
|
+
"x-telo-topology-role",
|
|
54
|
+
"x-telo-value-schema-from",
|
|
55
|
+
"x-telo-widget",
|
|
56
|
+
];
|
|
57
|
+
/**
|
|
58
|
+
* The `x-telo-type` keyword.
|
|
59
|
+
*
|
|
60
|
+
* Three postures, decided by the entry and never by this code:
|
|
61
|
+
*
|
|
62
|
+
* - a `json` representation validates through its own declared schema, so the
|
|
63
|
+
* keyword emits nothing — the name carries nominal identity for static wiring
|
|
64
|
+
* and has no runtime existence at all;
|
|
65
|
+
* - a `live` instance is EXEMPT: its value is never traversed, because iterating
|
|
66
|
+
* a stream to check it is precisely what the exemption is for;
|
|
67
|
+
* - every other instance is ASSERTED against the constructor its binding names.
|
|
68
|
+
*
|
|
69
|
+
* An unknown name emits nothing here. It is a hard diagnostic in the analyzer
|
|
70
|
+
* (`X_TELO_TYPE_UNKNOWN`), which is where a name can be reported against the
|
|
71
|
+
* manifest that wrote it; failing compilation instead would take out every
|
|
72
|
+
* validator in a module for one typo in one slot.
|
|
73
|
+
*/
|
|
74
|
+
export function valueTypeKeyword() {
|
|
75
|
+
return {
|
|
76
|
+
keyword: X_TELO_TYPE,
|
|
77
|
+
// Both spellings: a bare name, or the object form carrying type arguments.
|
|
78
|
+
schemaType: ["string", "object"],
|
|
79
|
+
code(cxt) {
|
|
80
|
+
const entry = readValueTypeSlot({
|
|
81
|
+
[X_TELO_TYPE]: cxt.schema,
|
|
82
|
+
})?.entry;
|
|
83
|
+
if (!entry || entry.representation !== "instance" || entry.live)
|
|
84
|
+
return;
|
|
85
|
+
const binding = VALUE_TYPE_BINDINGS[entry.binding];
|
|
86
|
+
if (!binding)
|
|
87
|
+
return;
|
|
88
|
+
// The constructor reaches generated code through AJV's value scope, which
|
|
89
|
+
// is what keeps this general: `Uint8Array` happens to be a global, but a
|
|
90
|
+
// binding may name a class that is not, and inlining a bare identifier
|
|
91
|
+
// would compile to a reference that does not resolve.
|
|
92
|
+
const ctor = cxt.gen.scopeValue("obj", {
|
|
93
|
+
ref: binding.constructor,
|
|
94
|
+
code: codegen `require("@telorun/sdk").VALUE_TYPE_BINDINGS[${entry.binding}].constructor`,
|
|
95
|
+
});
|
|
96
|
+
cxt.pass(codegen `${cxt.data} instanceof ${ctor}`);
|
|
97
|
+
},
|
|
98
|
+
error: {
|
|
99
|
+
message: (cxt) => {
|
|
100
|
+
const entry = readValueTypeSlot({ [X_TELO_TYPE]: cxt.schema })?.entry;
|
|
101
|
+
return entry?.binding === "bytes"
|
|
102
|
+
? "must be raw bytes (a Uint8Array) — bytes cannot be written inline in a manifest"
|
|
103
|
+
: `must be a ${entry?.name ?? "declared value type"} — this value is not writable inline in a manifest`;
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Register every Telo keyword on an AJV instance: the annotations as no-ops and
|
|
110
|
+
* `x-telo-type` as the one that checks.
|
|
111
|
+
*
|
|
112
|
+
* Every AJV instance in the runtime and the analyzer goes through this, so a
|
|
113
|
+
* schema means the same thing wherever it is validated.
|
|
114
|
+
*/
|
|
115
|
+
export function registerTeloKeywords(ajv) {
|
|
116
|
+
for (const keyword of ANNOTATION_KEYWORDS)
|
|
117
|
+
ajv.addKeyword(keyword);
|
|
118
|
+
ajv.addKeyword(valueTypeKeyword());
|
|
119
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/** Writing a value back into YAML SOURCE — the two primitives every in-place
|
|
2
|
+
* repair is built from, and the one place their rules are stated.
|
|
3
|
+
*
|
|
4
|
+
* A repair is a byte splice over the author's own text, never
|
|
5
|
+
* `Document.toString()`: re-serializing re-folds block scalars, drops
|
|
6
|
+
* quote-style hints and reflows long strings, so a one-key edit would arrive
|
|
7
|
+
* as a whole-file diff. That leaves two questions every writer has to answer
|
|
8
|
+
* identically — how a value is re-quoted to occupy a span, and how overlapping
|
|
9
|
+
* spans are spliced — and three surfaces asking them: the editors' quick fix
|
|
10
|
+
* (`@telorun/ide-support`), `telo migrate` (the migration driver), and
|
|
11
|
+
* `telo upgrade`'s pin rewrite.
|
|
12
|
+
*
|
|
13
|
+
* They live HERE, in the lowest package of the three, because they are pure
|
|
14
|
+
* string work with no Node dependency and because a copy per surface is a
|
|
15
|
+
* silent-divergence risk of exactly the kind Telo's cross-cutting primitives
|
|
16
|
+
* rule exists to prevent: two writers editing the same file from two copies of
|
|
17
|
+
* a subtle quoting rule will eventually quote one value two ways, and nothing
|
|
18
|
+
* would catch it. Same precedent as `ref-slot.ts` / `binary-slot.ts` — a rule
|
|
19
|
+
* several surfaces must agree on gets one reader. */
|
|
20
|
+
/** Whether `value` can be written as a plain scalar without changing meaning. */
|
|
21
|
+
export declare function isPlainSafe(value: string): boolean;
|
|
22
|
+
/** Quote style of the source text a repair is replacing. */
|
|
23
|
+
export type QuoteStyle = "double" | "single" | "plain";
|
|
24
|
+
export declare function quoteStyleOf(source: string): QuoteStyle;
|
|
25
|
+
/** Render `replacement` so it occupies `originalSource`'s span as the same
|
|
26
|
+
* scalar the author would have written by hand, or `undefined` when the span
|
|
27
|
+
* cannot be rewritten safely.
|
|
28
|
+
*
|
|
29
|
+
* A plain original is kept plain when it can be — rewriting `Run.Sequenc` to
|
|
30
|
+
* `"Run.Sequence"` would be a correct but noisy diff on a kind name — and
|
|
31
|
+
* promoted to double quotes when the new value would not survive unquoted.
|
|
32
|
+
*
|
|
33
|
+
* **A multi-line span is refused.** A block scalar's span covers its `|`/`>-`
|
|
34
|
+
* indicator AND its trailing newline, so writing a single-line scalar over it
|
|
35
|
+
* deletes the line break that ended the mapping entry and glues the next key
|
|
36
|
+
* onto the value — the document stops parsing. Re-emitting a block scalar
|
|
37
|
+
* correctly needs the node's indentation, which no consumer of this function
|
|
38
|
+
* has. A multi-line REPLACEMENT is refused for the mirror reason: its
|
|
39
|
+
* continuation lines would land at column 0, which is not a legal mapping
|
|
40
|
+
* value. A quick fix promises a repair that can be applied without review, so
|
|
41
|
+
* the only honest answer for these is no repair. */
|
|
42
|
+
export declare function renderFixReplacement(originalSource: string, replacement: string): string | undefined;
|
|
43
|
+
/** A splice over a source file: replace `[start, end)` with `newText`. An empty
|
|
44
|
+
* span is a pure insertion. */
|
|
45
|
+
export interface TextEdit {
|
|
46
|
+
/** Byte offset of the first replaced character (inclusive). */
|
|
47
|
+
readonly start: number;
|
|
48
|
+
/** Byte offset one past the last replaced character. */
|
|
49
|
+
readonly end: number;
|
|
50
|
+
readonly newText: string;
|
|
51
|
+
}
|
|
52
|
+
/** Splice `edits` into `text`, right to left so earlier offsets stay valid.
|
|
53
|
+
* Callers are responsible for edits not overlapping; two splices contending
|
|
54
|
+
* for the same bytes cannot both be honoured, and this does not arbitrate. */
|
|
55
|
+
export declare function applyTextEdits(text: string, edits: readonly TextEdit[]): string;
|
|
56
|
+
//# sourceMappingURL=yaml-source-edit.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"yaml-source-edit.d.ts","sourceRoot":"","sources":["../src/yaml-source-edit.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;sDAkBsD;AAStD,iFAAiF;AACjF,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAUlD;AAED,4DAA4D;AAC5D,MAAM,MAAM,UAAU,GAAG,QAAQ,GAAG,QAAQ,GAAG,OAAO,CAAC;AAEvD,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,UAAU,CAIvD;AAED;;;;;;;;;;;;;;;;qDAgBqD;AACrD,wBAAgB,oBAAoB,CAClC,cAAc,EAAE,MAAM,EACtB,WAAW,EAAE,MAAM,GAClB,MAAM,GAAG,SAAS,CAcpB;AAED;gCACgC;AAChC,MAAM,WAAW,QAAQ;IACvB,+DAA+D;IAC/D,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,wDAAwD;IACxD,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAED;;+EAE+E;AAC/E,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,QAAQ,EAAE,GAAG,MAAM,CAO/E"}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/** Writing a value back into YAML SOURCE — the two primitives every in-place
|
|
2
|
+
* repair is built from, and the one place their rules are stated.
|
|
3
|
+
*
|
|
4
|
+
* A repair is a byte splice over the author's own text, never
|
|
5
|
+
* `Document.toString()`: re-serializing re-folds block scalars, drops
|
|
6
|
+
* quote-style hints and reflows long strings, so a one-key edit would arrive
|
|
7
|
+
* as a whole-file diff. That leaves two questions every writer has to answer
|
|
8
|
+
* identically — how a value is re-quoted to occupy a span, and how overlapping
|
|
9
|
+
* spans are spliced — and three surfaces asking them: the editors' quick fix
|
|
10
|
+
* (`@telorun/ide-support`), `telo migrate` (the migration driver), and
|
|
11
|
+
* `telo upgrade`'s pin rewrite.
|
|
12
|
+
*
|
|
13
|
+
* They live HERE, in the lowest package of the three, because they are pure
|
|
14
|
+
* string work with no Node dependency and because a copy per surface is a
|
|
15
|
+
* silent-divergence risk of exactly the kind Telo's cross-cutting primitives
|
|
16
|
+
* rule exists to prevent: two writers editing the same file from two copies of
|
|
17
|
+
* a subtle quoting rule will eventually quote one value two ways, and nothing
|
|
18
|
+
* would catch it. Same precedent as `ref-slot.ts` / `binary-slot.ts` — a rule
|
|
19
|
+
* several surfaces must agree on gets one reader. */
|
|
20
|
+
/** Characters that make a plain (unquoted) YAML scalar reparse as something
|
|
21
|
+
* else. `-` and `?` are indicators only when followed by a space, so they are
|
|
22
|
+
* handled by the leading-token check rather than listed here. */
|
|
23
|
+
const PLAIN_UNSAFE_LEAD = new Set([
|
|
24
|
+
"&", "*", "!", "|", ">", "%", "@", "`", "#", "'", '"', "{", "[", "}", "]", ",",
|
|
25
|
+
]);
|
|
26
|
+
/** Whether `value` can be written as a plain scalar without changing meaning. */
|
|
27
|
+
export function isPlainSafe(value) {
|
|
28
|
+
if (value === "" || value.trim() !== value)
|
|
29
|
+
return false;
|
|
30
|
+
if (PLAIN_UNSAFE_LEAD.has(value[0]))
|
|
31
|
+
return false;
|
|
32
|
+
// `-`/`?`/`:` lead only matter when a space follows — `-x` is a scalar,
|
|
33
|
+
// `- x` is a sequence entry.
|
|
34
|
+
if (/^[-?:]\s/.test(value))
|
|
35
|
+
return false;
|
|
36
|
+
// A colon-space anywhere opens a mapping; a space-hash opens a comment.
|
|
37
|
+
if (value.includes(": ") || value.includes(" #"))
|
|
38
|
+
return false;
|
|
39
|
+
if (value.endsWith(":"))
|
|
40
|
+
return false;
|
|
41
|
+
return !/[\n\r]/.test(value);
|
|
42
|
+
}
|
|
43
|
+
export function quoteStyleOf(source) {
|
|
44
|
+
if (source.length >= 2 && source.startsWith('"') && source.endsWith('"'))
|
|
45
|
+
return "double";
|
|
46
|
+
if (source.length >= 2 && source.startsWith("'") && source.endsWith("'"))
|
|
47
|
+
return "single";
|
|
48
|
+
return "plain";
|
|
49
|
+
}
|
|
50
|
+
/** Render `replacement` so it occupies `originalSource`'s span as the same
|
|
51
|
+
* scalar the author would have written by hand, or `undefined` when the span
|
|
52
|
+
* cannot be rewritten safely.
|
|
53
|
+
*
|
|
54
|
+
* A plain original is kept plain when it can be — rewriting `Run.Sequenc` to
|
|
55
|
+
* `"Run.Sequence"` would be a correct but noisy diff on a kind name — and
|
|
56
|
+
* promoted to double quotes when the new value would not survive unquoted.
|
|
57
|
+
*
|
|
58
|
+
* **A multi-line span is refused.** A block scalar's span covers its `|`/`>-`
|
|
59
|
+
* indicator AND its trailing newline, so writing a single-line scalar over it
|
|
60
|
+
* deletes the line break that ended the mapping entry and glues the next key
|
|
61
|
+
* onto the value — the document stops parsing. Re-emitting a block scalar
|
|
62
|
+
* correctly needs the node's indentation, which no consumer of this function
|
|
63
|
+
* has. A multi-line REPLACEMENT is refused for the mirror reason: its
|
|
64
|
+
* continuation lines would land at column 0, which is not a legal mapping
|
|
65
|
+
* value. A quick fix promises a repair that can be applied without review, so
|
|
66
|
+
* the only honest answer for these is no repair. */
|
|
67
|
+
export function renderFixReplacement(originalSource, replacement) {
|
|
68
|
+
if (/[\n\r]/.test(originalSource) || /[\n\r]/.test(replacement))
|
|
69
|
+
return undefined;
|
|
70
|
+
const style = quoteStyleOf(originalSource);
|
|
71
|
+
if (style === "single") {
|
|
72
|
+
// A single-quoted YAML scalar escapes only the quote, by doubling it. CEL
|
|
73
|
+
// string literals use single quotes constantly, so this is the common case
|
|
74
|
+
// for an expression written in a single-quoted scalar.
|
|
75
|
+
return `'${replacement.replaceAll("'", "''")}'`;
|
|
76
|
+
}
|
|
77
|
+
if (style === "double" || !isPlainSafe(replacement)) {
|
|
78
|
+
return `"${replacement.replaceAll("\\", "\\\\").replaceAll('"', '\\"')}"`;
|
|
79
|
+
}
|
|
80
|
+
return replacement;
|
|
81
|
+
}
|
|
82
|
+
/** Splice `edits` into `text`, right to left so earlier offsets stay valid.
|
|
83
|
+
* Callers are responsible for edits not overlapping; two splices contending
|
|
84
|
+
* for the same bytes cannot both be honoured, and this does not arbitrate. */
|
|
85
|
+
export function applyTextEdits(text, edits) {
|
|
86
|
+
if (edits.length === 0)
|
|
87
|
+
return text;
|
|
88
|
+
let out = text;
|
|
89
|
+
for (const edit of [...edits].sort((a, b) => b.start - a.start)) {
|
|
90
|
+
out = out.slice(0, edit.start) + edit.newText + out.slice(edit.end);
|
|
91
|
+
}
|
|
92
|
+
return out;
|
|
93
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@telorun/analyzer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.59.0",
|
|
4
4
|
"description": "Telo Analyzer - Static manifest validator for Telo manifests.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"telo",
|
|
@@ -43,13 +43,13 @@
|
|
|
43
43
|
"jsonpath-plus": "^10.3.0",
|
|
44
44
|
"packageurl-js": "^2.0.1",
|
|
45
45
|
"yaml": "^2.8.3",
|
|
46
|
-
"@telorun/templating": "0.
|
|
46
|
+
"@telorun/templating": "0.15.0"
|
|
47
47
|
},
|
|
48
48
|
"devDependencies": {
|
|
49
49
|
"@types/node": "^20.0.0",
|
|
50
50
|
"typescript": "^5.0.0",
|
|
51
51
|
"vitest": "^2.1.8",
|
|
52
|
-
"@telorun/sdk": "0.
|
|
52
|
+
"@telorun/sdk": "0.74.0"
|
|
53
53
|
},
|
|
54
54
|
"peerDependencies": {
|
|
55
55
|
"@telorun/sdk": "*"
|
package/src/analyzer.ts
CHANGED
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
defaultRegistry,
|
|
6
6
|
isRefSentinel,
|
|
7
7
|
isTaggedSentinel,
|
|
8
|
+
plainChainOf,
|
|
8
9
|
type CelSurface,
|
|
9
10
|
} from "@telorun/templating";
|
|
10
11
|
import type { DiagnosticFix } from "./types.js";
|
|
@@ -56,11 +57,17 @@ import {
|
|
|
56
57
|
validateRefSlotDeclarations,
|
|
57
58
|
type RefSlotIssue,
|
|
58
59
|
} from "./validate-ref-slots.js";
|
|
60
|
+
import {
|
|
61
|
+
validateValueTypeSlots,
|
|
62
|
+
type ValueTypeSlotIssue,
|
|
63
|
+
} from "./validate-value-type-slots.js";
|
|
59
64
|
import { resolveSchemaTypeRefs } from "./resolve-schema-type-refs.js";
|
|
60
65
|
import { validateSchemaTypeRefs } from "./validate-schema-type-refs.js";
|
|
61
66
|
import { rewriteSyntheticOrigins } from "./rewrite-synthetic-origins.js";
|
|
62
67
|
import {
|
|
63
68
|
celTypeSatisfiesJsonSchema,
|
|
69
|
+
checkSchemaCompatibility,
|
|
70
|
+
navigateSchemaToExprPath,
|
|
64
71
|
substituteCelFields,
|
|
65
72
|
validateAgainstSchema,
|
|
66
73
|
type SchemaIssue,
|
|
@@ -1201,6 +1208,18 @@ export class StaticAnalyzer {
|
|
|
1201
1208
|
const refConstraintIssues: RefConstraintIssue[] = [];
|
|
1202
1209
|
const refSlotIssues: RefSlotIssue[] = [];
|
|
1203
1210
|
const zoneSlotIssues: ZoneSlotIssue[] = [];
|
|
1211
|
+
// `x-telo-type` is checked on EVERY manifest, not only on definition docs: a
|
|
1212
|
+
// schema fragment is written wherever a kind declares a schema-valued field,
|
|
1213
|
+
// so an inline `inputType:` on an ordinary resource carries one just as a
|
|
1214
|
+
// definition's `schema:` does. Same scoping as every other schema issue —
|
|
1215
|
+
// the entry's own modules, since a dependency is not the consumer's to fix.
|
|
1216
|
+
const valueTypeSlotIssues: ValueTypeSlotIssue[] = [];
|
|
1217
|
+
for (const m of manifests) {
|
|
1218
|
+
const declaringModule = (m.metadata as { module?: string } | undefined)?.module;
|
|
1219
|
+
if (!declaringModule || rootModules.has(declaringModule)) {
|
|
1220
|
+
valueTypeSlotIssues.push(...validateValueTypeSlots(m as unknown as ResourceManifest));
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1204
1223
|
for (const m of manifests) {
|
|
1205
1224
|
if (m.kind !== "Telo.Definition" && m.kind !== "Telo.Abstract") continue;
|
|
1206
1225
|
const def = m as unknown as ResourceDefinition;
|
|
@@ -1340,6 +1359,26 @@ export class StaticAnalyzer {
|
|
|
1340
1359
|
// accessor split; `readRefSlot` stays lenient so surfaces keep working
|
|
1341
1360
|
// mid-migration, and this reports what leniency would silently absorb.
|
|
1342
1361
|
for (const issue of refSlotIssues) diagnostics.push(refSlotIssueDiagnostic(issue));
|
|
1362
|
+
// The same split for `x-telo-type`. Its reader returns a slot with no
|
|
1363
|
+
// entry for a name it does not know, which is what an unrecognized brand
|
|
1364
|
+
// used to do SILENTLY — the slot simply lost its identity.
|
|
1365
|
+
for (const issue of valueTypeSlotIssues) {
|
|
1366
|
+
diagnostics.push({
|
|
1367
|
+
severity: DiagnosticSeverity.Error,
|
|
1368
|
+
code: issue.code,
|
|
1369
|
+
source: SOURCE,
|
|
1370
|
+
message: issue.message,
|
|
1371
|
+
data: {
|
|
1372
|
+
resource: {
|
|
1373
|
+
kind: issue.manifest.kind,
|
|
1374
|
+
name: issue.manifest.metadata?.name as string,
|
|
1375
|
+
},
|
|
1376
|
+
filePath: (issue.manifest.metadata as { source?: string } | undefined)?.source,
|
|
1377
|
+
path: issue.path,
|
|
1378
|
+
...(issue.fix ? { fix: issue.fix } : {}),
|
|
1379
|
+
},
|
|
1380
|
+
});
|
|
1381
|
+
}
|
|
1343
1382
|
// Same split for the two zone annotations. Unreadable ones fail in
|
|
1344
1383
|
// OPPOSITE directions — a dropped requirement is silently unenforced, a
|
|
1345
1384
|
// dropped provision invents failures — so neither can be left to
|
|
@@ -1625,6 +1664,11 @@ export class StaticAnalyzer {
|
|
|
1625
1664
|
filePath?: string;
|
|
1626
1665
|
})[] = [];
|
|
1627
1666
|
const celTypeByPath = new Map<ResourceManifest, Map<string, string>>();
|
|
1667
|
+
// The schema an expression RESOLVES TO, beside the CEL type it carries. Both
|
|
1668
|
+
// are needed and neither replaces the other: the CEL type answers "does this
|
|
1669
|
+
// fit the slot at all", the schema answers "do their type arguments agree",
|
|
1670
|
+
// which cel-js cannot express because it types by constructor identity.
|
|
1671
|
+
const celSourceSchemaByPath = new Map<ResourceManifest, Map<string, Record<string, any>>>();
|
|
1628
1672
|
// Context-free typed environments, one per manifest. Reused across every
|
|
1629
1673
|
// expression in it — see the build site for why a matched context opts out.
|
|
1630
1674
|
const typedEnvByManifest = new Map<ResourceManifest, Environment>();
|
|
@@ -1962,12 +2006,15 @@ export class StaticAnalyzer {
|
|
|
1962
2006
|
defs,
|
|
1963
2007
|
aliases,
|
|
1964
2008
|
{ aliasesByModule, rootModules },
|
|
2009
|
+
celStepContextSchema,
|
|
1965
2010
|
)) {
|
|
1966
2011
|
diagnostics.push({
|
|
1967
2012
|
severity: DiagnosticSeverity.Error,
|
|
1968
|
-
code: "CONTRACT_INPUTS_MISMATCH",
|
|
2013
|
+
code: issue.code ?? "CONTRACT_INPUTS_MISMATCH",
|
|
1969
2014
|
source: SOURCE,
|
|
1970
|
-
message:
|
|
2015
|
+
message: issue.code
|
|
2016
|
+
? `${m.kind}/${stepName}: inputs at '${issue.path}' flow into ${issue.targetLabel} with disagreeing type arguments: ${issue.message}`
|
|
2017
|
+
: `${m.kind}/${stepName}: inputs at '${issue.path}' do not satisfy ${issue.targetLabel}'s declared inputType: ${issue.message}`,
|
|
1971
2018
|
data: {
|
|
1972
2019
|
resource: { kind: m.kind, name: stepName ?? "" },
|
|
1973
2020
|
filePath: stepFile,
|
|
@@ -2260,6 +2307,30 @@ export class StaticAnalyzer {
|
|
|
2260
2307
|
byPath.set(path, result.type);
|
|
2261
2308
|
}
|
|
2262
2309
|
|
|
2310
|
+
// The producer half of the type-argument check. A CEL type is a bare
|
|
2311
|
+
// name — cel-js types by constructor identity, so a byte stream and a
|
|
2312
|
+
// string stream are one CEL type and always will be — so the argument
|
|
2313
|
+
// comparison is a parallel pass over the SCHEMAS the analyzer already
|
|
2314
|
+
// walks. This is where the producer's schema is in hand: navigating
|
|
2315
|
+
// the expression's chain into its context schema is exactly "the
|
|
2316
|
+
// producer's outputType at the expression's tail".
|
|
2317
|
+
//
|
|
2318
|
+
// Only a PLAIN CHAIN is navigated — `steps.encode.result.output`, the
|
|
2319
|
+
// shape a wiring site actually takes. An expression that computes
|
|
2320
|
+
// rather than names has no schema to read off the context, so it
|
|
2321
|
+
// records nothing and no argument check fires: silence where the
|
|
2322
|
+
// analyzer knows least is the conservative direction, and the same one
|
|
2323
|
+
// `x-telo-step-context`'s pure-`value` typing takes.
|
|
2324
|
+
const chain = effectiveContext ? plainChainOf(`\${{${expr}}}`) : undefined;
|
|
2325
|
+
if (chain) {
|
|
2326
|
+
const produced = navigateSchemaToExprPath(effectiveContext!, chain);
|
|
2327
|
+
if (produced) {
|
|
2328
|
+
let byPath = celSourceSchemaByPath.get(m);
|
|
2329
|
+
if (!byPath) celSourceSchemaByPath.set(m, (byPath = new Map()));
|
|
2330
|
+
byPath.set(path, produced);
|
|
2331
|
+
}
|
|
2332
|
+
}
|
|
2333
|
+
|
|
2263
2334
|
// A non-deterministic call in a compile-eval field is baked once at
|
|
2264
2335
|
// load: `nowIso()` there freezes at boot. Sometimes that is the
|
|
2265
2336
|
// intent (a boot timestamp, a run id), so it warns rather than
|
|
@@ -2331,13 +2402,35 @@ export class StaticAnalyzer {
|
|
|
2331
2402
|
for (const slot of celReturnSlots) {
|
|
2332
2403
|
const type = celTypeByPath.get(slot.manifest)?.get(slot.path);
|
|
2333
2404
|
if (type === undefined) continue;
|
|
2334
|
-
if (celTypeSatisfiesJsonSchema(type.split("<")[0]!, slot.schema))
|
|
2335
|
-
|
|
2405
|
+
if (!celTypeSatisfiesJsonSchema(type.split("<")[0]!, slot.schema)) {
|
|
2406
|
+
const expected = slot.schema["x-telo-type"] ?? slot.schema.type ?? "unknown";
|
|
2407
|
+
diagnostics.push({
|
|
2408
|
+
severity: DiagnosticSeverity.Error,
|
|
2409
|
+
code: "CEL_TYPE_ERROR",
|
|
2410
|
+
source: SOURCE,
|
|
2411
|
+
message: `${slot.resource.kind}/${slot.resource.name}: CEL at '${slot.path}' returns '${type}' but the field expects '${expected}'.`,
|
|
2412
|
+
data: { resource: slot.resource, filePath: slot.filePath, path: slot.path },
|
|
2413
|
+
});
|
|
2414
|
+
continue;
|
|
2415
|
+
}
|
|
2416
|
+
// The type fits; do its ARGUMENTS agree? Covariant and gradual — an
|
|
2417
|
+
// omitted argument is *any* in both directions, so an unmigrated producer
|
|
2418
|
+
// or consumer is never reported, and only a definite conflict is.
|
|
2419
|
+
const produced = celSourceSchemaByPath.get(slot.manifest)?.get(slot.path);
|
|
2420
|
+
if (!produced) continue;
|
|
2421
|
+
const { compatible, issues } = checkSchemaCompatibility(
|
|
2422
|
+
produced,
|
|
2423
|
+
slot.schema,
|
|
2424
|
+
(ref: string) => defs.schemaForId(ref),
|
|
2425
|
+
);
|
|
2426
|
+
if (compatible) continue;
|
|
2336
2427
|
diagnostics.push({
|
|
2337
2428
|
severity: DiagnosticSeverity.Error,
|
|
2338
|
-
code: "
|
|
2429
|
+
code: "CEL_TYPE_ARGUMENT_MISMATCH",
|
|
2339
2430
|
source: SOURCE,
|
|
2340
|
-
message:
|
|
2431
|
+
message:
|
|
2432
|
+
`${slot.resource.kind}/${slot.resource.name}: CEL at '${slot.path}' produces a value ` +
|
|
2433
|
+
`whose type arguments disagree with the field's: ${issues.join("; ")}.`,
|
|
2341
2434
|
data: { resource: slot.resource, filePath: slot.filePath, path: slot.path },
|
|
2342
2435
|
});
|
|
2343
2436
|
}
|
package/src/cel-environment.ts
CHANGED
|
@@ -3,10 +3,13 @@ import type { ResourceManifest } from "@telorun/sdk";
|
|
|
3
3
|
import { jsonSchemaToCelType, VALUE_BRAND_BASE } from "./schema-compat.js";
|
|
4
4
|
|
|
5
5
|
/** Transport protocol on a `ports` entry → the nominal CEL brand its resolved
|
|
6
|
-
* value carries. Mirrors the `protocol` enum in the Application schema
|
|
6
|
+
* value carries. Mirrors the `protocol` enum in the Application schema, and
|
|
7
|
+
* names the value types by their canonical `Telo.`-qualified names — the same
|
|
8
|
+
* spelling an author writes at `x-telo-type`, so a branded port and a branded
|
|
9
|
+
* field are comparable by name with nothing in between to translate. */
|
|
7
10
|
const PORT_PROTOCOL_BRAND: Record<string, string> = {
|
|
8
|
-
tcp: "TcpPort",
|
|
9
|
-
udp: "UdpPort",
|
|
11
|
+
tcp: "Telo.TcpPort",
|
|
12
|
+
udp: "Telo.UdpPort",
|
|
10
13
|
};
|
|
11
14
|
|
|
12
15
|
export { buildCelEnvironment } from "@telorun/templating";
|
|
@@ -127,6 +127,16 @@ export class DefinitionRegistry {
|
|
|
127
127
|
return this.registeredSchemaIds.has(id) || this.ajv.getSchema(id) !== undefined;
|
|
128
128
|
}
|
|
129
129
|
|
|
130
|
+
/** The schema registered under `id`, for a structural comparison that must see
|
|
131
|
+
* THROUGH a named shape. Declaring a shape once and referencing it is the
|
|
132
|
+
* sanctioned way to reuse one, so a comparator that cannot follow the
|
|
133
|
+
* reference judges two opaque nodes and learns nothing. */
|
|
134
|
+
schemaForId(id: string): Record<string, any> | undefined {
|
|
135
|
+
const compiled = this.ajv.getSchema(id);
|
|
136
|
+
const schema = compiled?.schema;
|
|
137
|
+
return schema && typeof schema === "object" ? (schema as Record<string, any>) : undefined;
|
|
138
|
+
}
|
|
139
|
+
|
|
130
140
|
/** Validates data against a schema using this registry's AJV instance, which has all
|
|
131
141
|
* registered definition schemas loaded — enabling cross-module $ref resolution.
|
|
132
142
|
* A compile failure returns `[]` here; it is surfaced loudly (once, on the
|
package/src/index.ts
CHANGED
|
@@ -62,12 +62,19 @@ export {
|
|
|
62
62
|
export type { ContractDirection, DefResolver } from "./extends-resolution.js";
|
|
63
63
|
export {
|
|
64
64
|
defaultBearingPaths,
|
|
65
|
+
declaredScalarPaths,
|
|
65
66
|
PERMISSIVE_CONTRACT,
|
|
66
67
|
resolveContract,
|
|
67
68
|
resolveContractSchema,
|
|
68
|
-
|
|
69
|
+
withLiveValuesSkipped,
|
|
70
|
+
} from "./invocation-contract.js";
|
|
71
|
+
export type {
|
|
72
|
+
ContractOrigin,
|
|
73
|
+
ContractScope,
|
|
74
|
+
DeclaredScalarForm,
|
|
75
|
+
DeclaredScalarPath,
|
|
76
|
+
ResolvedContract,
|
|
69
77
|
} from "./invocation-contract.js";
|
|
70
|
-
export type { ContractOrigin, ContractScope, ResolvedContract } from "./invocation-contract.js";
|
|
71
78
|
export {
|
|
72
79
|
hasIntermediateWildcard,
|
|
73
80
|
parseRedactionPath,
|
|
@@ -97,7 +104,18 @@ export {
|
|
|
97
104
|
transfersControl,
|
|
98
105
|
} from "./ref-slot.js";
|
|
99
106
|
export type { RefSlot, RefUse, RefUseCases } from "./ref-slot.js";
|
|
100
|
-
export {
|
|
107
|
+
export {
|
|
108
|
+
ANNOTATION_KEYWORDS,
|
|
109
|
+
registerTeloKeywords,
|
|
110
|
+
valueTypeKeyword,
|
|
111
|
+
} from "./value-type-keyword.js";
|
|
112
|
+
export {
|
|
113
|
+
applyTextEdits,
|
|
114
|
+
isPlainSafe,
|
|
115
|
+
quoteStyleOf,
|
|
116
|
+
renderFixReplacement,
|
|
117
|
+
} from "./yaml-source-edit.js";
|
|
118
|
+
export type { QuoteStyle, TextEdit } from "./yaml-source-edit.js";
|
|
101
119
|
export {
|
|
102
120
|
hasProvidesZone,
|
|
103
121
|
hasRequiresZone,
|
|
@@ -123,6 +141,10 @@ export { validateZoneSlotDeclarations } from "./validate-zone-slots.js";
|
|
|
123
141
|
export type { ZoneSlotIssue } from "./validate-zone-slots.js";
|
|
124
142
|
export { validateDynamicSelectors, validateRefSlotDeclarations } from "./validate-ref-slots.js";
|
|
125
143
|
export type { RefSlotIssue } from "./validate-ref-slots.js";
|
|
144
|
+
export { validateValueTypeSlots } from "./validate-value-type-slots.js";
|
|
145
|
+
export type { ValueTypeSlotIssue } from "./validate-value-type-slots.js";
|
|
146
|
+
export { checkSchemaCompatibility, selectUnionBranch } from "./schema-compat.js";
|
|
147
|
+
export type { CompatibilityResult } from "./schema-compat.js";
|
|
126
148
|
export { visitManifest } from "./manifest-visitor.js";
|
|
127
149
|
export type {
|
|
128
150
|
CelSiteEvent,
|
|
@@ -139,6 +161,15 @@ export { isModuleKind, MODULE_KINDS } from "./module-kinds.js";
|
|
|
139
161
|
export type { ModuleKind } from "./module-kinds.js";
|
|
140
162
|
export { parseLoadedFile } from "./parse-loaded-file.js";
|
|
141
163
|
export type { ParseOptions } from "./parse-loaded-file.js";
|
|
164
|
+
export {
|
|
165
|
+
CORE_MIGRATIONS,
|
|
166
|
+
migrateFileText,
|
|
167
|
+
migrateManifests,
|
|
168
|
+
NO_MIGRATIONS,
|
|
169
|
+
parseMigrationEntry,
|
|
170
|
+
remapMigratedPaths,
|
|
171
|
+
} from "./migrations/index.js";
|
|
172
|
+
export type { FileMigrations, MigrationEntry, MigrationRewrite } from "./migrations/index.js";
|
|
142
173
|
export { desugarLoadedFile, inlineImportManifests } from "./inline-imports.js";
|
|
143
174
|
export type { SyntheticImport } from "./inline-imports.js";
|
|
144
175
|
export {
|